Add unit tests for importTransaction (internal/config)
24 test cases covering transaction lifecycle: staging, commit, rollback, and cleanup. Tests include atomic commit behavior, backup/restore on failure, directory creation, permission handling, and idempotent cleanup. First test file for import_transaction.go. Coverage 42.4% → 42.7%.
This commit is contained in:
parent
30c8440e17
commit
94952577e2
1 changed files with 667 additions and 0 deletions
667
internal/config/import_transaction_test.go
Normal file
667
internal/config/import_transaction_test.go
Normal file
|
|
@ -0,0 +1,667 @@
|
|||
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")
|
||||
}
|
||||
})
|
||||
}
|
||||
Loading…
Reference in a new issue