package backup_test import ( "context" "path/filepath" "testing" "time" "git.eeqj.de/sneak/vaultik/internal/backup" "git.eeqj.de/sneak/vaultik/internal/database" "github.com/spf13/afero" ) func TestScannerSimpleDirectory(t *testing.T) { // Create in-memory filesystem fs := afero.NewMemMapFs() // Create test directory structure testFiles := map[string]string{ "/source/file1.txt": "Hello, world!", // 13 bytes "/source/file2.txt": "This is another file", // 20 bytes "/source/subdir/file3.txt": "File in subdirectory", // 20 bytes "/source/subdir/file4.txt": "Another file in subdirectory", // 28 bytes "/source/empty.txt": "", // 0 bytes "/source/subdir2/file5.txt": "Yet another file", // 16 bytes } // Create files with specific times testTime := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) for path, content := range testFiles { dir := filepath.Dir(path) if err := fs.MkdirAll(dir, 0755); err != nil { t.Fatalf("failed to create directory %s: %v", dir, err) } if err := afero.WriteFile(fs, path, []byte(content), 0644); err != nil { t.Fatalf("failed to write file %s: %v", path, err) } // Set times if err := fs.Chtimes(path, testTime, testTime); err != nil { t.Fatalf("failed to set times for %s: %v", path, err) } } // Create test database db, err := database.NewTestDB() if err != nil { t.Fatalf("failed to create test database: %v", err) } defer func() { if err := db.Close(); err != nil { t.Errorf("failed to close database: %v", err) } }() repos := database.NewRepositories(db) // Create scanner scanner := backup.NewScanner(backup.ScannerConfig{ FS: fs, ChunkSize: 1024 * 16, // 16KB chunks for testing Repositories: repos, }) // Scan the directory ctx := context.Background() result, err := scanner.Scan(ctx, "/source") if err != nil { t.Fatalf("scan failed: %v", err) } // Verify results if result.FilesScanned != 6 { t.Errorf("expected 6 files scanned, got %d", result.FilesScanned) } if result.BytesScanned != 97 { // Total size of all test files: 13 + 20 + 20 + 28 + 0 + 16 = 97 t.Errorf("expected 97 bytes scanned, got %d", result.BytesScanned) } // Verify files in database files, err := repos.Files.ListByPrefix(ctx, "/source") if err != nil { t.Fatalf("failed to list files: %v", err) } if len(files) != 6 { t.Errorf("expected 6 files in database, got %d", len(files)) } // Verify specific file file1, err := repos.Files.GetByPath(ctx, "/source/file1.txt") if err != nil { t.Fatalf("failed to get file1.txt: %v", err) } if file1.Size != 13 { t.Errorf("expected file1.txt size 13, got %d", file1.Size) } if file1.Mode != 0644 { t.Errorf("expected file1.txt mode 0644, got %o", file1.Mode) } // Verify chunks were created chunks, err := repos.FileChunks.GetByFile(ctx, "/source/file1.txt") if err != nil { t.Fatalf("failed to get chunks for file1.txt: %v", err) } if len(chunks) != 1 { // Small file should be one chunk t.Errorf("expected 1 chunk for file1.txt, got %d", len(chunks)) } // Verify deduplication - file3.txt and file4.txt have different content // but we should still have the correct number of unique chunks allChunks, err := repos.Chunks.List(ctx) if err != nil { t.Fatalf("failed to list all chunks: %v", err) } // We should have at most 6 chunks (one per unique file content) // Empty file might not create a chunk if len(allChunks) > 6 { t.Errorf("expected at most 6 chunks, got %d", len(allChunks)) } } func TestScannerWithSymlinks(t *testing.T) { // Create in-memory filesystem fs := afero.NewMemMapFs() // Create test files if err := fs.MkdirAll("/source", 0755); err != nil { t.Fatal(err) } if err := afero.WriteFile(fs, "/source/target.txt", []byte("target content"), 0644); err != nil { t.Fatal(err) } if err := afero.WriteFile(fs, "/outside/file.txt", []byte("outside content"), 0644); err != nil { t.Fatal(err) } // Create symlinks (if supported by the filesystem) linker, ok := fs.(afero.Symlinker) if !ok { t.Skip("filesystem does not support symlinks") } // Symlink to file in source if err := linker.SymlinkIfPossible("target.txt", "/source/link1.txt"); err != nil { t.Fatal(err) } // Symlink to file outside source if err := linker.SymlinkIfPossible("/outside/file.txt", "/source/link2.txt"); err != nil { t.Fatal(err) } // Create test database db, err := database.NewTestDB() if err != nil { t.Fatalf("failed to create test database: %v", err) } defer func() { if err := db.Close(); err != nil { t.Errorf("failed to close database: %v", err) } }() repos := database.NewRepositories(db) // Create scanner scanner := backup.NewScanner(backup.ScannerConfig{ FS: fs, ChunkSize: 1024 * 16, Repositories: repos, }) // Scan the directory ctx := context.Background() result, err := scanner.Scan(ctx, "/source") if err != nil { t.Fatalf("scan failed: %v", err) } // Should have scanned 3 files (target + 2 symlinks) if result.FilesScanned != 3 { t.Errorf("expected 3 files scanned, got %d", result.FilesScanned) } // Check symlinks in database link1, err := repos.Files.GetByPath(ctx, "/source/link1.txt") if err != nil { t.Fatalf("failed to get link1.txt: %v", err) } if link1.LinkTarget != "target.txt" { t.Errorf("expected link1.txt target 'target.txt', got %q", link1.LinkTarget) } link2, err := repos.Files.GetByPath(ctx, "/source/link2.txt") if err != nil { t.Fatalf("failed to get link2.txt: %v", err) } if link2.LinkTarget != "/outside/file.txt" { t.Errorf("expected link2.txt target '/outside/file.txt', got %q", link2.LinkTarget) } } func TestScannerLargeFile(t *testing.T) { // Create in-memory filesystem fs := afero.NewMemMapFs() // Create a large file that will require multiple chunks largeContent := make([]byte, 1024*1024) // 1MB for i := range largeContent { largeContent[i] = byte(i % 256) } if err := fs.MkdirAll("/source", 0755); err != nil { t.Fatal(err) } if err := afero.WriteFile(fs, "/source/large.bin", largeContent, 0644); err != nil { t.Fatal(err) } // Create test database db, err := database.NewTestDB() if err != nil { t.Fatalf("failed to create test database: %v", err) } defer func() { if err := db.Close(); err != nil { t.Errorf("failed to close database: %v", err) } }() repos := database.NewRepositories(db) // Create scanner with 64KB chunks scanner := backup.NewScanner(backup.ScannerConfig{ FS: fs, ChunkSize: 1024 * 64, // 64KB chunks Repositories: repos, }) // Scan the directory ctx := context.Background() result, err := scanner.Scan(ctx, "/source") if err != nil { t.Fatalf("scan failed: %v", err) } if result.BytesScanned != 1024*1024 { t.Errorf("expected %d bytes scanned, got %d", 1024*1024, result.BytesScanned) } // Verify chunks chunks, err := repos.FileChunks.GetByFile(ctx, "/source/large.bin") if err != nil { t.Fatalf("failed to get chunks: %v", err) } expectedChunks := 16 // 1MB / 64KB if len(chunks) != expectedChunks { t.Errorf("expected %d chunks, got %d", expectedChunks, len(chunks)) } // Verify chunk sequence for i, fc := range chunks { if fc.Idx != i { t.Errorf("chunk %d has incorrect sequence %d", i, fc.Idx) } } }