Pulse/internal/config/import_transaction_test.go
rcourtman c26fb97181 test: Add StageFile transaction tests
Cover success path, already committed error, replacing existing staged
file, and empty basename edge case.
2025-12-02 02:34:20 +00:00

761 lines
19 KiB
Go

package config
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestNewImportTransaction(t *testing.T) {
t.Run("creates staging directory", func(t *testing.T) {
tmpDir := t.TempDir()
tx, err := newImportTransaction(tmpDir)
if err != nil {
t.Fatalf("newImportTransaction() error = %v", err)
}
defer tx.Cleanup()
if tx.stagingDir == "" {
t.Error("stagingDir should not be empty")
}
// Verify staging directory exists
info, err := os.Stat(tx.stagingDir)
if err != nil {
t.Errorf("staging directory should exist: %v", err)
}
if !info.IsDir() {
t.Error("stagingDir should be a directory")
}
// Verify staging dir is under config dir
if !strings.HasPrefix(tx.stagingDir, tmpDir) {
t.Errorf("stagingDir %q should be under configDir %q", tx.stagingDir, tmpDir)
}
// Verify staging dir contains expected prefix
if !strings.Contains(tx.stagingDir, ".import-staging-") {
t.Errorf("stagingDir %q should contain .import-staging- prefix", tx.stagingDir)
}
})
t.Run("initializes maps", func(t *testing.T) {
tmpDir := t.TempDir()
tx, err := newImportTransaction(tmpDir)
if err != nil {
t.Fatalf("newImportTransaction() error = %v", err)
}
defer tx.Cleanup()
if tx.staged == nil {
t.Error("staged map should be initialized")
}
if tx.backups == nil {
t.Error("backups map should be initialized")
}
})
t.Run("sets configDir", func(t *testing.T) {
tmpDir := t.TempDir()
tx, err := newImportTransaction(tmpDir)
if err != nil {
t.Fatalf("newImportTransaction() error = %v", err)
}
defer tx.Cleanup()
if tx.configDir != tmpDir {
t.Errorf("configDir = %q, want %q", tx.configDir, tmpDir)
}
})
t.Run("sets timestamp", func(t *testing.T) {
tmpDir := t.TempDir()
tx, err := newImportTransaction(tmpDir)
if err != nil {
t.Fatalf("newImportTransaction() error = %v", err)
}
defer tx.Cleanup()
if tx.timestamp == "" {
t.Error("timestamp should not be empty")
}
// Verify timestamp format (YYYYMMDD-HHMMSS)
if len(tx.timestamp) != 15 {
t.Errorf("timestamp length = %d, expected 15", len(tx.timestamp))
}
if tx.timestamp[8] != '-' {
t.Errorf("timestamp format incorrect, expected dash at position 8: %q", tx.timestamp)
}
})
t.Run("fails with invalid directory", func(t *testing.T) {
_, err := newImportTransaction("/nonexistent/path/that/does/not/exist")
if err == nil {
t.Error("expected error for nonexistent directory")
}
})
}
func TestImportTransaction_StageFile(t *testing.T) {
t.Run("stages file successfully", func(t *testing.T) {
tmpDir := t.TempDir()
tx, err := newImportTransaction(tmpDir)
if err != nil {
t.Fatalf("newImportTransaction() error = %v", err)
}
defer tx.Cleanup()
targetPath := filepath.Join(tmpDir, "test.txt")
data := []byte("test content")
err = tx.StageFile(targetPath, data, 0o644)
if err != nil {
t.Fatalf("StageFile() error = %v", err)
}
// Verify file is staged
if _, ok := tx.staged[targetPath]; !ok {
t.Error("file should be recorded in staged map")
}
// Verify staged file exists and has correct content
stagedPath := tx.staged[targetPath]
content, err := os.ReadFile(stagedPath)
if err != nil {
t.Fatalf("failed to read staged file: %v", err)
}
if string(content) != "test content" {
t.Errorf("staged content = %q, want %q", string(content), "test content")
}
})
t.Run("replaces previously staged file", func(t *testing.T) {
tmpDir := t.TempDir()
tx, err := newImportTransaction(tmpDir)
if err != nil {
t.Fatalf("newImportTransaction() error = %v", err)
}
defer tx.Cleanup()
targetPath := filepath.Join(tmpDir, "test.txt")
// Stage first version
err = tx.StageFile(targetPath, []byte("version 1"), 0o644)
if err != nil {
t.Fatalf("StageFile() error = %v", err)
}
firstStagedPath := tx.staged[targetPath]
// Stage second version
err = tx.StageFile(targetPath, []byte("version 2"), 0o644)
if err != nil {
t.Fatalf("StageFile() error = %v", err)
}
secondStagedPath := tx.staged[targetPath]
// Verify old staged file was removed
if firstStagedPath == secondStagedPath {
t.Error("second staged file should have different path")
}
if _, err := os.Stat(firstStagedPath); !os.IsNotExist(err) {
t.Error("first staged file should be removed")
}
// Verify new content
content, err := os.ReadFile(secondStagedPath)
if err != nil {
t.Fatalf("failed to read staged file: %v", err)
}
if string(content) != "version 2" {
t.Errorf("staged content = %q, want %q", string(content), "version 2")
}
})
t.Run("sets file permissions", func(t *testing.T) {
tmpDir := t.TempDir()
tx, err := newImportTransaction(tmpDir)
if err != nil {
t.Fatalf("newImportTransaction() error = %v", err)
}
defer tx.Cleanup()
targetPath := filepath.Join(tmpDir, "test.txt")
err = tx.StageFile(targetPath, []byte("test"), 0o600)
if err != nil {
t.Fatalf("StageFile() error = %v", err)
}
stagedPath := tx.staged[targetPath]
info, err := os.Stat(stagedPath)
if err != nil {
t.Fatalf("failed to stat staged file: %v", err)
}
// Check permissions (mask with 0777 to ignore umask effects)
perm := info.Mode().Perm() & 0o777
if perm != 0o600 {
t.Errorf("file permissions = %o, want %o", perm, 0o600)
}
})
t.Run("fails after commit", func(t *testing.T) {
tmpDir := t.TempDir()
tx, err := newImportTransaction(tmpDir)
if err != nil {
t.Fatalf("newImportTransaction() error = %v", err)
}
defer tx.Cleanup()
// Commit empty transaction
err = tx.Commit()
if err != nil {
t.Fatalf("Commit() error = %v", err)
}
// Try to stage after commit
targetPath := filepath.Join(tmpDir, "test.txt")
err = tx.StageFile(targetPath, []byte("test"), 0o644)
if err == nil {
t.Error("expected error when staging after commit")
}
if !strings.Contains(err.Error(), "already committed") {
t.Errorf("error should mention already committed: %v", err)
}
})
t.Run("handles empty target base name", func(t *testing.T) {
tmpDir := t.TempDir()
tx, err := newImportTransaction(tmpDir)
if err != nil {
t.Fatalf("newImportTransaction() error = %v", err)
}
defer tx.Cleanup()
// Stage with target ending in separator (edge case)
targetPath := filepath.Join(tmpDir, "subdir") + string(os.PathSeparator)
err = tx.StageFile(targetPath, []byte("test"), 0o644)
// Should not panic
if err == nil {
// It staged something - verify it's recorded
if _, ok := tx.staged[targetPath]; !ok {
t.Error("file should be recorded even with edge case path")
}
}
})
}
func TestImportTransaction_Commit(t *testing.T) {
t.Run("applies staged files", func(t *testing.T) {
tmpDir := t.TempDir()
tx, err := newImportTransaction(tmpDir)
if err != nil {
t.Fatalf("newImportTransaction() error = %v", err)
}
defer tx.Cleanup()
targetPath := filepath.Join(tmpDir, "committed.txt")
err = tx.StageFile(targetPath, []byte("committed content"), 0o644)
if err != nil {
t.Fatalf("StageFile() error = %v", err)
}
err = tx.Commit()
if err != nil {
t.Fatalf("Commit() error = %v", err)
}
// Verify file exists at target
content, err := os.ReadFile(targetPath)
if err != nil {
t.Fatalf("failed to read committed file: %v", err)
}
if string(content) != "committed content" {
t.Errorf("committed content = %q, want %q", string(content), "committed content")
}
})
t.Run("creates backup of existing file", func(t *testing.T) {
tmpDir := t.TempDir()
// Create existing file
targetPath := filepath.Join(tmpDir, "existing.txt")
err := os.WriteFile(targetPath, []byte("original content"), 0o644)
if err != nil {
t.Fatalf("failed to create existing file: %v", err)
}
tx, err := newImportTransaction(tmpDir)
if err != nil {
t.Fatalf("newImportTransaction() error = %v", err)
}
defer tx.Cleanup()
err = tx.StageFile(targetPath, []byte("new content"), 0o644)
if err != nil {
t.Fatalf("StageFile() error = %v", err)
}
err = tx.Commit()
if err != nil {
t.Fatalf("Commit() error = %v", err)
}
// Verify new content
content, err := os.ReadFile(targetPath)
if err != nil {
t.Fatalf("failed to read committed file: %v", err)
}
if string(content) != "new content" {
t.Errorf("committed content = %q, want %q", string(content), "new content")
}
// Verify backup was removed on successful commit
matches, _ := filepath.Glob(targetPath + ".import-backup-*")
if len(matches) > 0 {
t.Error("backup file should be removed after successful commit")
}
})
t.Run("creates target directory if needed", func(t *testing.T) {
tmpDir := t.TempDir()
tx, err := newImportTransaction(tmpDir)
if err != nil {
t.Fatalf("newImportTransaction() error = %v", err)
}
defer tx.Cleanup()
// Target in non-existent subdirectory
targetPath := filepath.Join(tmpDir, "subdir", "nested", "file.txt")
err = tx.StageFile(targetPath, []byte("nested content"), 0o644)
if err != nil {
t.Fatalf("StageFile() error = %v", err)
}
err = tx.Commit()
if err != nil {
t.Fatalf("Commit() error = %v", err)
}
// Verify file exists
content, err := os.ReadFile(targetPath)
if err != nil {
t.Fatalf("failed to read committed file: %v", err)
}
if string(content) != "nested content" {
t.Errorf("committed content = %q, want %q", string(content), "nested content")
}
})
t.Run("fails if destination is directory", func(t *testing.T) {
tmpDir := t.TempDir()
// Create directory at target path
targetPath := filepath.Join(tmpDir, "targetdir")
err := os.Mkdir(targetPath, 0o755)
if err != nil {
t.Fatalf("failed to create directory: %v", err)
}
tx, err := newImportTransaction(tmpDir)
if err != nil {
t.Fatalf("newImportTransaction() error = %v", err)
}
defer tx.Cleanup()
err = tx.StageFile(targetPath, []byte("content"), 0o644)
if err != nil {
t.Fatalf("StageFile() error = %v", err)
}
err = tx.Commit()
if err == nil {
t.Error("expected error when committing to directory")
}
if !strings.Contains(err.Error(), "is a directory") {
t.Errorf("error should mention directory: %v", err)
}
})
t.Run("cannot commit twice", func(t *testing.T) {
tmpDir := t.TempDir()
tx, err := newImportTransaction(tmpDir)
if err != nil {
t.Fatalf("newImportTransaction() error = %v", err)
}
defer tx.Cleanup()
err = tx.Commit()
if err != nil {
t.Fatalf("first Commit() error = %v", err)
}
err = tx.Commit()
if err == nil {
t.Error("expected error on second commit")
}
if !strings.Contains(err.Error(), "already committed") {
t.Errorf("error should mention already committed: %v", err)
}
})
t.Run("commits multiple files atomically", func(t *testing.T) {
tmpDir := t.TempDir()
tx, err := newImportTransaction(tmpDir)
if err != nil {
t.Fatalf("newImportTransaction() error = %v", err)
}
defer tx.Cleanup()
// Stage multiple files
files := map[string]string{
filepath.Join(tmpDir, "file1.txt"): "content 1",
filepath.Join(tmpDir, "file2.txt"): "content 2",
filepath.Join(tmpDir, "file3.txt"): "content 3",
}
for path, content := range files {
err = tx.StageFile(path, []byte(content), 0o644)
if err != nil {
t.Fatalf("StageFile(%s) error = %v", path, err)
}
}
err = tx.Commit()
if err != nil {
t.Fatalf("Commit() error = %v", err)
}
// Verify all files exist with correct content
for path, expectedContent := range files {
content, err := os.ReadFile(path)
if err != nil {
t.Errorf("failed to read %s: %v", path, err)
continue
}
if string(content) != expectedContent {
t.Errorf("content of %s = %q, want %q", path, string(content), expectedContent)
}
}
})
}
func TestImportTransaction_Rollback(t *testing.T) {
t.Run("removes staged files", func(t *testing.T) {
tmpDir := t.TempDir()
tx, err := newImportTransaction(tmpDir)
if err != nil {
t.Fatalf("newImportTransaction() error = %v", err)
}
defer tx.Cleanup()
targetPath := filepath.Join(tmpDir, "test.txt")
err = tx.StageFile(targetPath, []byte("content"), 0o644)
if err != nil {
t.Fatalf("StageFile() error = %v", err)
}
stagedPath := tx.staged[targetPath]
// Verify staged file exists
if _, err := os.Stat(stagedPath); err != nil {
t.Fatalf("staged file should exist: %v", err)
}
tx.Rollback()
// Verify staged file is removed
if _, err := os.Stat(stagedPath); !os.IsNotExist(err) {
t.Error("staged file should be removed after rollback")
}
})
t.Run("restores backup files", func(t *testing.T) {
tmpDir := t.TempDir()
// Create existing file
targetPath := filepath.Join(tmpDir, "existing.txt")
err := os.WriteFile(targetPath, []byte("original content"), 0o644)
if err != nil {
t.Fatalf("failed to create existing file: %v", err)
}
tx, err := newImportTransaction(tmpDir)
if err != nil {
t.Fatalf("newImportTransaction() error = %v", err)
}
defer tx.Cleanup()
err = tx.StageFile(targetPath, []byte("new content"), 0o644)
if err != nil {
t.Fatalf("StageFile() error = %v", err)
}
// Manually simulate partial commit state by creating backup
backupPath := targetPath + ".import-backup-" + tx.timestamp
err = os.Rename(targetPath, backupPath)
if err != nil {
t.Fatalf("failed to create backup: %v", err)
}
tx.backups[targetPath] = backupPath
tx.Rollback()
// Verify original file is restored
content, err := os.ReadFile(targetPath)
if err != nil {
t.Fatalf("failed to read restored file: %v", err)
}
if string(content) != "original content" {
t.Errorf("restored content = %q, want %q", string(content), "original content")
}
})
t.Run("handles rollback with no backups", func(t *testing.T) {
tmpDir := t.TempDir()
tx, err := newImportTransaction(tmpDir)
if err != nil {
t.Fatalf("newImportTransaction() error = %v", err)
}
defer tx.Cleanup()
targetPath := filepath.Join(tmpDir, "new.txt")
err = tx.StageFile(targetPath, []byte("content"), 0o644)
if err != nil {
t.Fatalf("StageFile() error = %v", err)
}
// Rollback should not panic even with no backups
tx.Rollback()
// Target should not exist
if _, err := os.Stat(targetPath); !os.IsNotExist(err) {
t.Error("target should not exist after rollback of new file")
}
})
}
func TestImportTransaction_Cleanup(t *testing.T) {
t.Run("removes staging directory", func(t *testing.T) {
tmpDir := t.TempDir()
tx, err := newImportTransaction(tmpDir)
if err != nil {
t.Fatalf("newImportTransaction() error = %v", err)
}
stagingDir := tx.stagingDir
// Verify staging directory exists
if _, err := os.Stat(stagingDir); err != nil {
t.Fatalf("staging directory should exist: %v", err)
}
tx.Cleanup()
// Verify staging directory is removed
if _, err := os.Stat(stagingDir); !os.IsNotExist(err) {
t.Error("staging directory should be removed after cleanup")
}
})
t.Run("removes staging directory with files", func(t *testing.T) {
tmpDir := t.TempDir()
tx, err := newImportTransaction(tmpDir)
if err != nil {
t.Fatalf("newImportTransaction() error = %v", err)
}
// Stage a file
targetPath := filepath.Join(tmpDir, "test.txt")
err = tx.StageFile(targetPath, []byte("content"), 0o644)
if err != nil {
t.Fatalf("StageFile() error = %v", err)
}
stagingDir := tx.stagingDir
tx.Cleanup()
// Verify staging directory is removed even with files
if _, err := os.Stat(stagingDir); !os.IsNotExist(err) {
t.Error("staging directory should be removed after cleanup")
}
})
t.Run("cleanup is idempotent", func(t *testing.T) {
tmpDir := t.TempDir()
tx, err := newImportTransaction(tmpDir)
if err != nil {
t.Fatalf("newImportTransaction() error = %v", err)
}
// Call cleanup multiple times - should not panic
tx.Cleanup()
tx.Cleanup()
tx.Cleanup()
})
}
func TestImportTransaction_CommitRestoreOnFailure(t *testing.T) {
t.Run("restores backups on commit failure", func(t *testing.T) {
tmpDir := t.TempDir()
// Create existing file
existingPath := filepath.Join(tmpDir, "existing.txt")
err := os.WriteFile(existingPath, []byte("original"), 0o644)
if err != nil {
t.Fatalf("failed to create existing file: %v", err)
}
tx, err := newImportTransaction(tmpDir)
if err != nil {
t.Fatalf("newImportTransaction() error = %v", err)
}
defer tx.Cleanup()
// Stage replacement for existing file
err = tx.StageFile(existingPath, []byte("replacement"), 0o644)
if err != nil {
t.Fatalf("StageFile() error = %v", err)
}
// Stage file that will fail (target is a directory)
failPath := filepath.Join(tmpDir, "willFail")
err = os.Mkdir(failPath, 0o755)
if err != nil {
t.Fatalf("failed to create directory: %v", err)
}
err = tx.StageFile(failPath, []byte("content"), 0o644)
if err != nil {
t.Fatalf("StageFile() error = %v", err)
}
// Commit should fail
err = tx.Commit()
if err == nil {
t.Fatal("expected commit to fail")
}
// Verify original file is restored
content, err := os.ReadFile(existingPath)
if err != nil {
t.Fatalf("failed to read restored file: %v", err)
}
if string(content) != "original" {
t.Errorf("restored content = %q, want %q", string(content), "original")
}
})
}
func TestStageFile_Success(t *testing.T) {
tmpDir := t.TempDir()
tx, err := newImportTransaction(tmpDir)
if err != nil {
t.Fatalf("newImportTransaction: %v", err)
}
defer tx.Cleanup()
target := filepath.Join(tmpDir, "test.txt")
data := []byte("test content")
err = tx.StageFile(target, data, 0600)
if err != nil {
t.Fatalf("StageFile: %v", err)
}
// Verify file was staged
if _, ok := tx.staged[target]; !ok {
t.Error("target should be in staged map")
}
}
func TestStageFile_AlreadyCommitted(t *testing.T) {
tmpDir := t.TempDir()
tx, err := newImportTransaction(tmpDir)
if err != nil {
t.Fatalf("newImportTransaction: %v", err)
}
defer tx.Cleanup()
// Mark as committed
tx.committed = true
target := filepath.Join(tmpDir, "test.txt")
err = tx.StageFile(target, []byte("data"), 0600)
if err == nil {
t.Error("expected error for already committed transaction")
}
if err != nil && err.Error() != "transaction already committed" {
t.Errorf("expected 'transaction already committed' error, got: %v", err)
}
}
func TestStageFile_ReplacesExistingStaged(t *testing.T) {
tmpDir := t.TempDir()
tx, err := newImportTransaction(tmpDir)
if err != nil {
t.Fatalf("newImportTransaction: %v", err)
}
defer tx.Cleanup()
target := filepath.Join(tmpDir, "test.txt")
// Stage first version
err = tx.StageFile(target, []byte("first"), 0600)
if err != nil {
t.Fatalf("first StageFile: %v", err)
}
firstStaged := tx.staged[target]
// Stage second version (should replace)
err = tx.StageFile(target, []byte("second"), 0600)
if err != nil {
t.Fatalf("second StageFile: %v", err)
}
secondStaged := tx.staged[target]
// Verify different file was staged
if firstStaged == secondStaged {
t.Error("second staging should create new file")
}
// Verify first staged file was removed
if _, err := os.Stat(firstStaged); !os.IsNotExist(err) {
t.Error("first staged file should have been removed")
}
}
func TestStageFile_EmptyBasename(t *testing.T) {
tmpDir := t.TempDir()
tx, err := newImportTransaction(tmpDir)
if err != nil {
t.Fatalf("newImportTransaction: %v", err)
}
defer tx.Cleanup()
// Target with trailing slash gets empty basename
target := tmpDir + string(os.PathSeparator)
err = tx.StageFile(target, []byte("data"), 0600)
if err != nil {
t.Fatalf("StageFile with empty basename: %v", err)
}
}