diff --git a/.gitignore b/.gitignore index 8103f40..1b58775 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ test autorestic data -dist \ No newline at end of file +dist +coverage* diff --git a/internal/location_test.go b/internal/location_test.go index 7720f56..911ae7b 100644 --- a/internal/location_test.go +++ b/internal/location_test.go @@ -91,3 +91,30 @@ func TestBuildRestoreCommand(t *testing.T) { expected := []string{"restore", "--target", "to", "--tag", "ar:location:foo", "snapshot", "options"} assertSliceEqual(t, result, expected) } + +func TestLocationBackupWithMock(t *testing.T) { + // Backup original + originalExecutor := DefaultExecutor + defer func() { DefaultExecutor = originalExecutor }() + + // Inject mock + mock := &MockExecutor{ + ExecuteResticFunc: func(options ExecuteOptions, args ...string) (int, string, error) { + assertEqual(t, args[0], "backup") + return 0, "success", nil + }, + } + DefaultExecutor = mock + + loc := Location{ + name: "test-location", + To: []string{"test-backend"}, + From: []string{"/"}, + Type: "local", + } + + errs := loc.Backup(false, false, "") + if len(errs) != 0 { + t.Errorf("expected no error, got %v", errs) + } +} diff --git a/internal/mock_executor_test.go b/internal/mock_executor_test.go new file mode 100644 index 0000000..3408935 --- /dev/null +++ b/internal/mock_executor_test.go @@ -0,0 +1,20 @@ +package internal + +type MockExecutor struct { + ExecuteFunc func(options ExecuteOptions, args ...string) (int, string, error) + ExecuteResticFunc func(options ExecuteOptions, args ...string) (int, string, error) +} + +func (m *MockExecutor) Execute(options ExecuteOptions, args ...string) (int, string, error) { + if m.ExecuteFunc != nil { + return m.ExecuteFunc(options, args...) + } + return 0, "", nil +} + +func (m *MockExecutor) ExecuteRestic(options ExecuteOptions, args ...string) (int, string, error) { + if m.ExecuteResticFunc != nil { + return m.ExecuteResticFunc(options, args...) + } + return 0, "", nil +} diff --git a/internal/utils.go b/internal/utils.go index c8bf799..d523ce6 100644 --- a/internal/utils.go +++ b/internal/utils.go @@ -39,7 +39,14 @@ func (w ColoredWriter) Write(p []byte) (n int, err error) { return len(p), nil } -func ExecuteCommand(options ExecuteOptions, args ...string) (int, string, error) { +type Executor interface { + Execute(options ExecuteOptions, args ...string) (int, string, error) + ExecuteRestic(options ExecuteOptions, args ...string) (int, string, error) +} + +type RealExecutor struct{} + +func (e *RealExecutor) Execute(options ExecuteOptions, args ...string) (int, string, error) { cmd := exec.Command(options.Command, args...) env := os.Environ() for k, v := range options.Envs { @@ -76,12 +83,22 @@ func ExecuteCommand(options ExecuteOptions, args ...string) (int, string, error) return 0, out.String(), nil } -func ExecuteResticCommand(options ExecuteOptions, args ...string) (int, string, error) { +func (e *RealExecutor) ExecuteRestic(options ExecuteOptions, args ...string) (int, string, error) { options.Command = flags.RESTIC_BIN var c = GetConfig() var optionsAsString = getOptions(c.Global, []string{"all"}) args = append(optionsAsString, args...) - return ExecuteCommand(options, args...) + return e.Execute(options, args...) +} + +var DefaultExecutor Executor = &RealExecutor{} + +func ExecuteCommand(options ExecuteOptions, args ...string) (int, string, error) { + return DefaultExecutor.Execute(options, args...) +} + +func ExecuteResticCommand(options ExecuteOptions, args ...string) (int, string, error) { + return DefaultExecutor.ExecuteRestic(options, args...) } func CopyFile(from, to string) error { diff --git a/internal/utils_test.go b/internal/utils_test.go new file mode 100644 index 0000000..079bc88 --- /dev/null +++ b/internal/utils_test.go @@ -0,0 +1,27 @@ +package internal + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestExecuteCommandWithMock(t *testing.T) { + // Backup original + originalExecutor := DefaultExecutor + defer func() { DefaultExecutor = originalExecutor }() + + // Inject mock + mock := &MockExecutor{ + ExecuteFunc: func(options ExecuteOptions, args ...string) (int, string, error) { + assert.Equal(t, "docker", options.Command) + return 0, "mock output", nil + }, + } + DefaultExecutor = mock + + code, out, err := ExecuteCommand(ExecuteOptions{Command: "docker"}, "info") + + assert.NoError(t, err) + assert.Equal(t, 0, code) + assert.Equal(t, "mock output", out) +} diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..3020e57 --- /dev/null +++ b/mise.toml @@ -0,0 +1,13 @@ +[tools] +go = "latest" +restic = "latest" + +[tasks.test] +run = "go test -v ./..." + +[tasks.coverage] +run = "go test -coverprofile=coverage.out ./... && go tool cover -func=coverage.out && go tool cover -html=coverage.out -o coverage.html" +description = "Generate coverage report" + +[tasks.clean] +run = "rm -f coverage.out coverage.html" diff --git a/tests/integration_test.go b/tests/integration_test.go new file mode 100644 index 0000000..9452f70 --- /dev/null +++ b/tests/integration_test.go @@ -0,0 +1,223 @@ +package integration_test + +import ( + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func runAutorestic(t *testing.T, dir string, configPath string, args ...string) string { + // Find project root dynamically + wd, err := os.Getwd() + assert.NoError(t, err) + root := filepath.Dir(wd) + + mainGo := filepath.Join(root, "main.go") + + // Get restic path from mise + resticPath := "/Users/cupcakearmy/.local/share/mise/installs/restic/0.18.1/restic" + + // Convert configPath to absolute path + absConfigPath, err := filepath.Abs(configPath) + assert.NoError(t, err) + + cmd := exec.Command("go", append([]string{"run", mainGo, "--restic-bin", resticPath, "-c", absConfigPath}, args...)...) + + // Run from root to find go.mod + cmd.Dir = root + + // Add mise path to environment and set dummy password + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, "PATH="+os.Getenv("PATH")) + cmd.Env = append(cmd.Env, "RESTIC_PASSWORD=password") + + output, err := cmd.CombinedOutput() + // NOTE: We don't assert NoError here because tests might expect failures + return string(output) +} + +func initRepo(t *testing.T, repoPath string) { + resticPath := "/Users/cupcakearmy/.local/share/mise/installs/restic/0.18.1/restic" + cmd := exec.Command(resticPath, "-r", repoPath, "init") + cmd.Env = append(os.Environ(), "RESTIC_PASSWORD=password") + output, err := cmd.CombinedOutput() + assert.NoError(t, err, string(output)) +} + +func TestAutoresticCheck(t *testing.T) { + tempDir := t.TempDir() + + configContent := ` +version: 2 +locations: + my-data: + from: ` + tempDir + ` + to: local +backends: + local: + type: local + path: ` + filepath.Join(tempDir, "repo") + ` + key: password +` + configPath := filepath.Join(tempDir, "autorestic.yml") + err := os.WriteFile(configPath, []byte(configContent), 0644) + assert.NoError(t, err) + + _ = runAutorestic(t, tempDir, configPath, "check") +} + +func TestBackupRestore(t *testing.T) { + tempDir := t.TempDir() + + // 1. Create a source file + sourceFile := "source.txt" + err := os.WriteFile(filepath.Join(tempDir, sourceFile), []byte("hello world"), 0644) + assert.NoError(t, err) + + repoPath := filepath.Join(tempDir, "repo") + initRepo(t, repoPath) + + configContent := ` +version: 2 +locations: + my-data: + from: + - ` + sourceFile + ` + to: local +backends: + local: + type: local + path: ` + repoPath + ` + key: password +` + configPath := filepath.Join(tempDir, "autorestic.yml") + err = os.WriteFile(configPath, []byte(configContent), 0644) + assert.NoError(t, err) + + // 2. Backup + output := runAutorestic(t, tempDir, configPath, "backup", "-l", "my-data") + assert.Contains(t, output, "Done") + + // 3. Restore + restoreDir := filepath.Join(tempDir, "restore") + err = os.MkdirAll(restoreDir, 0755) + assert.NoError(t, err) + output = runAutorestic(t, tempDir, configPath, "restore", "-l", "my-data", "--to", restoreDir) + t.Logf("Restore output: %s", output) + + // DEBUG: List files in restoreDir and subdirectories + err = filepath.Walk(restoreDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + t.Logf("Found: %s", path) + return nil + }) + assert.NoError(t, err) + + // 4. Verify + // It might be nested depending on how it was backed up + // Let's look for source.txt in any subdirectory + var restoredFile string + err = filepath.Walk(restoreDir, func(path string, info os.FileInfo, err error) error { + if !info.IsDir() && info.Name() == "source.txt" { + restoredFile = path + } + return nil + }) + assert.NoError(t, err) + assert.NotEmpty(t, restoredFile, "source.txt not found") + + content, err := os.ReadFile(restoredFile) + assert.NoError(t, err) + assert.Equal(t, "hello world", string(content)) +} + +func TestHooks(t *testing.T) { + tempDir := t.TempDir() + + // Create a dummy file to back up + sourceFile := "source.txt" + err := os.WriteFile(filepath.Join(tempDir, sourceFile), []byte("data"), 0644) + assert.NoError(t, err) + + repoPath := filepath.Join(tempDir, "repo") + initRepo(t, repoPath) + + configContent := ` +version: 2 +locations: + my-data: + from: + - ` + sourceFile + ` + to: local + hooks: + before: + - touch before.txt + after: + - touch after.txt +backends: + local: + type: local + path: ` + repoPath + ` + key: password +` + configPath := filepath.Join(tempDir, "autorestic.yml") + err = os.WriteFile(configPath, []byte(configContent), 0644) + assert.NoError(t, err) + + // Run backup + runAutorestic(t, tempDir, configPath, "backup", "-l", "my-data") + + // Verify + assert.FileExists(t, filepath.Join(tempDir, "before.txt")) + assert.FileExists(t, filepath.Join(tempDir, "after.txt")) +} + +func TestCopy(t *testing.T) { + tempDir := t.TempDir() + + // Create a dummy file to back up + sourceFile := "source.txt" + err := os.WriteFile(filepath.Join(tempDir, sourceFile), []byte("data"), 0644) + assert.NoError(t, err) + + repoPath := filepath.Join(tempDir, "repo") + initRepo(t, repoPath) + + remoteRepoPath := filepath.Join(tempDir, "remote_repo") + initRepo(t, remoteRepoPath) + + configContent := ` +version: 2 +locations: + my-data: + from: + - ` + sourceFile + ` + to: local + copy: + local: + - remote +backends: + local: + type: local + path: ` + repoPath + ` + key: password + remote: + type: local + path: ` + remoteRepoPath + ` + key: password +` + configPath := filepath.Join(tempDir, "autorestic.yml") + err = os.WriteFile(configPath, []byte(configContent), 0644) + assert.NoError(t, err) + + // Run backup + output := runAutorestic(t, tempDir, configPath, "backup", "-l", "my-data") + + // Verify copy in output + assert.Contains(t, output, "Copying local → remote") +} diff --git a/tests/version_test.go b/tests/version_test.go new file mode 100644 index 0000000..8be0905 --- /dev/null +++ b/tests/version_test.go @@ -0,0 +1,14 @@ +package integration_test + +import ( + "github.com/stretchr/testify/assert" + "os/exec" + "testing" +) + +func TestVersion(t *testing.T) { + cmd := exec.Command("go", "run", "../main.go", "--version") + out, err := cmd.CombinedOutput() + assert.NoError(t, err) + assert.Contains(t, string(out), "autorestic") +}