Adds a flexible hook system to the `borg collect` commands, allowing users to run custom scripts at various stages of the collection lifecycle. This feature introduces a new `pkg/hooks` package that encapsulates the core logic for parsing a `.borg-hooks.yaml` configuration file and executing external scripts. Key features: - Four hook events are supported: `on_file_collected`, `on_url_found`, `on_collection_complete`, and `on_error`. - A `--hooks` flag has been added to the `collect website` and `collect pwa` commands. - The system automatically detects and loads a `.borg-hooks.yaml` file from the current directory if the `--hooks` flag is not provided. - File-based hooks (`on_file_collected`) support glob pattern matching against the base filename. - Hook scripts receive a JSON payload on stdin with relevant event context. - Commands with arguments are correctly handled by executing them through `sh -c`. The implementation includes a comprehensive test suite with both unit tests for the new `hooks` package and integration tests to validate the end-to-end functionality. All existing tests and examples have been updated to reflect the necessary function signature changes. Co-authored-by: Snider <631881+Snider@users.noreply.github.com>
218 lines
5.1 KiB
Go
218 lines
5.1 KiB
Go
package hooks
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func setupTest(t *testing.T) (string, func()) {
|
|
t.Helper()
|
|
|
|
// Create a temporary directory for test artifacts
|
|
tmpDir, err := os.MkdirTemp("", "hooks-test-")
|
|
require.NoError(t, err)
|
|
|
|
// Create test hook script
|
|
scriptContent := "#!/bin/sh\ncat > " + filepath.Join(tmpDir, "testhook.output")
|
|
scriptPath := filepath.Join(tmpDir, "testhook.sh")
|
|
err = os.WriteFile(scriptPath, []byte(scriptContent), 0755)
|
|
require.NoError(t, err)
|
|
|
|
// Create test hooks config
|
|
hooksYAML := `
|
|
hooks:
|
|
on_file_collected:
|
|
- pattern: "*.pdf"
|
|
run: "` + scriptPath + `"
|
|
- pattern: "*.txt"
|
|
run: "` + scriptPath + `"
|
|
on_url_found:
|
|
- run: "` + scriptPath + `"
|
|
on_collection_complete:
|
|
- run: "` + scriptPath + `"
|
|
on_error:
|
|
- run: "` + scriptPath + `"
|
|
`
|
|
configPath := filepath.Join(tmpDir, ".borg-hooks.yaml")
|
|
err = os.WriteFile(configPath, []byte(hooksYAML), 0644)
|
|
require.NoError(t, err)
|
|
|
|
return tmpDir, func() {
|
|
os.RemoveAll(tmpDir)
|
|
}
|
|
}
|
|
|
|
func TestNewHookRunner(t *testing.T) {
|
|
tmpDir, cleanup := setupTest(t)
|
|
defer cleanup()
|
|
configPath := filepath.Join(tmpDir, ".borg-hooks.yaml")
|
|
|
|
// Test with a valid config file
|
|
runner, err := NewHookRunner(configPath)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, runner)
|
|
assert.NotNil(t, runner.config)
|
|
|
|
// Test with a non-existent file
|
|
_, err = NewHookRunner("non-existent-file.yaml")
|
|
assert.Error(t, err)
|
|
|
|
// Test with a malformed file
|
|
malformedConfigPath := filepath.Join(tmpDir, "malformed.yaml")
|
|
err = os.WriteFile(malformedConfigPath, []byte("hooks: \n - invalid"), 0644)
|
|
require.NoError(t, err)
|
|
_, err = NewHookRunner(malformedConfigPath)
|
|
assert.Error(t, err)
|
|
|
|
// Test with an empty config file path (should not error)
|
|
runner, err = NewHookRunner("")
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, runner)
|
|
assert.NotNil(t, runner.config)
|
|
}
|
|
|
|
func TestHookRunner_Trigger(t *testing.T) {
|
|
tmpDir, cleanup := setupTest(t)
|
|
defer cleanup()
|
|
configPath := filepath.Join(tmpDir, ".borg-hooks.yaml")
|
|
runner, err := NewHookRunner(configPath)
|
|
require.NoError(t, err)
|
|
|
|
outputFile := filepath.Join(tmpDir, "testhook.output")
|
|
|
|
tests := []struct {
|
|
name string
|
|
event Event
|
|
shouldTrigger bool
|
|
expectedEvent Event
|
|
}{
|
|
{
|
|
name: "OnFileCollected - PDF Match with full path",
|
|
event: Event{
|
|
Event: OnFileCollected,
|
|
File: "assets/document.pdf",
|
|
URL: "http://example.com/assets/document.pdf",
|
|
Type: "application/pdf",
|
|
},
|
|
shouldTrigger: true,
|
|
expectedEvent: Event{
|
|
Event: OnFileCollected,
|
|
File: "assets/document.pdf",
|
|
URL: "http://example.com/assets/document.pdf",
|
|
Type: "application/pdf",
|
|
},
|
|
},
|
|
{
|
|
name: "OnFileCollected - TXT Match with full path",
|
|
event: Event{
|
|
Event: OnFileCollected,
|
|
File: "notes/notes.txt",
|
|
},
|
|
shouldTrigger: true,
|
|
expectedEvent: Event{
|
|
Event: OnFileCollected,
|
|
File: "notes/notes.txt",
|
|
},
|
|
},
|
|
{
|
|
name: "OnFileCollected - No Match",
|
|
event: Event{
|
|
Event: OnFileCollected,
|
|
File: "image.jpg",
|
|
},
|
|
shouldTrigger: false,
|
|
},
|
|
{
|
|
name: "OnURLFound",
|
|
event: Event{
|
|
Event: OnURLFound,
|
|
URL: "http://example.com/page2",
|
|
},
|
|
shouldTrigger: true,
|
|
expectedEvent: Event{
|
|
Event: OnURLFound,
|
|
URL: "http://example.com/page2",
|
|
},
|
|
},
|
|
{
|
|
name: "OnCollectionComplete",
|
|
event: Event{
|
|
Event: OnCollectionComplete,
|
|
},
|
|
shouldTrigger: true,
|
|
expectedEvent: Event{
|
|
Event: OnCollectionComplete,
|
|
},
|
|
},
|
|
{
|
|
name: "OnError",
|
|
event: Event{
|
|
Event: OnError,
|
|
Error: "something went wrong",
|
|
},
|
|
shouldTrigger: true,
|
|
expectedEvent: Event{
|
|
Event: OnError,
|
|
Error: "something went wrong",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Clean up previous output
|
|
_ = os.Remove(outputFile)
|
|
|
|
err := runner.Trigger(tt.event)
|
|
require.NoError(t, err)
|
|
|
|
if !tt.shouldTrigger {
|
|
_, err := os.Stat(outputFile)
|
|
assert.True(t, os.IsNotExist(err), "Hook should not have been triggered")
|
|
return
|
|
}
|
|
|
|
// Verify the output file was created and contains the correct JSON
|
|
content, err := os.ReadFile(outputFile)
|
|
require.NoError(t, err)
|
|
|
|
var receivedEvent Event
|
|
err = json.Unmarshal(content, &receivedEvent)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, tt.expectedEvent, receivedEvent)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHookRunner_FailingHook(t *testing.T) {
|
|
tmpDir, cleanup := setupTest(t)
|
|
defer cleanup()
|
|
|
|
// Create a failing script
|
|
scriptContent := "#!/bin/sh\nexit 1"
|
|
scriptPath := filepath.Join(tmpDir, "failing-hook.sh")
|
|
err := os.WriteFile(scriptPath, []byte(scriptContent), 0755)
|
|
require.NoError(t, err)
|
|
|
|
// Create hooks config with the failing script
|
|
hooksYAML := `
|
|
hooks:
|
|
on_error:
|
|
- run: "` + scriptPath + `"
|
|
`
|
|
configPath := filepath.Join(tmpDir, "failing-hooks.yaml")
|
|
err = os.WriteFile(configPath, []byte(hooksYAML), 0644)
|
|
require.NoError(t, err)
|
|
|
|
runner, err := NewHookRunner(configPath)
|
|
require.NoError(t, err)
|
|
|
|
err = runner.Trigger(Event{Event: OnError, Error: "test error"})
|
|
assert.Error(t, err)
|
|
}
|