mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 19:41:17 +00:00
Cover success path, already committed error, replacing existing staged file, and empty basename edge case.
761 lines
19 KiB
Go
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)
|
|
}
|
|
}
|