vaultik/internal/backup/scanner_test.go
sneak 26db096913 Move StartTime initialization to application startup hook
- Remove StartTime initialization from globals.New()
- Add setupGlobals function in app.go to set StartTime during fx OnStart
- Simplify globals package to be just a key/value store
- Remove fx dependencies from globals test
2025-07-20 12:05:24 +02:00

277 lines
7.2 KiB
Go

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)
}
}
}