package services import ( "bookhoard/internal/database" "context" "os" "path/filepath" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // Helper function to setup test database func setupTestDB(t *testing.T) *database.Queries { // Use existing test database setup // This would connect to the test database return &database.Queries{} // Placeholder - use your actual test DB setup } func TestMarkDirectoryDirty(t *testing.T) { db := setupTestDB(t) scanner := NewMediaScanner(db) scanner.folders = []string{"/test/folder"} scanner.markDirectoryDirty("/test/folder/subdir") scanner.dirtyDirsMu.RLock() _, exists := scanner.dirtyDirs["/test/folder/subdir"] scanner.dirtyDirsMu.RUnlock() assert.True(t, exists, "Directory should be marked dirty") } func TestMarkDirectoryDirty_IgnoresNonWatchedPaths(t *testing.T) { db := setupTestDB(t) scanner := NewMediaScanner(db) scanner.folders = []string{"/test/folder"} scanner.markDirectoryDirty("/other/folder") scanner.dirtyDirsMu.RLock() _, exists := scanner.dirtyDirs["/other/folder"] scanner.dirtyDirsMu.RUnlock() assert.False(t, exists, "Non-watched directory should be ignored") } func TestMarkDirectoryDirty_SmartEventMerging(t *testing.T) { db := setupTestDB(t) scanner := NewMediaScanner(db) scanner.folders = []string{"/test/folder"} // Mark subdirectory first scanner.markDirectoryDirty("/test/folder/subdir1") scanner.dirtyDirsMu.RLock() _, exists1 := scanner.dirtyDirs["/test/folder/subdir1"] scanner.dirtyDirsMu.RUnlock() assert.True(t, exists1) // Mark parent directory - should replace subdirectory scanner.markDirectoryDirty("/test/folder") scanner.dirtyDirsMu.RLock() _, parentExists := scanner.dirtyDirs["/test/folder"] _, childExists := scanner.dirtyDirs["/test/folder/subdir1"] scanner.dirtyDirsMu.RUnlock() assert.True(t, parentExists, "Parent should exist") assert.False(t, childExists, "Child should be removed (consolidated)") } func TestWaitForFileStability_StableFile(t *testing.T) { db := setupTestDB(t) scanner := NewMediaScanner(db) // Create a stable file tmpDir := t.TempDir() filePath := filepath.Join(tmpDir, "stable.epub") err := os.WriteFile(filePath, []byte("test content"), 0644) require.NoError(t, err) // Should return true immediately (file already stable) assert.True(t, scanner.waitForFileStability(filePath)) } func TestWaitForFileStability_UnstableFile(t *testing.T) { db := setupTestDB(t) scanner := NewMediaScanner(db) // Create a file tmpDir := t.TempDir() filePath := filepath.Join(tmpDir, "unstable.epub") file, err := os.Create(filePath) require.NoError(t, err) defer file.Close() // Start stability check in background stableChan := make(chan bool) go func() { stableChan <- scanner.waitForFileStability(filePath) }() // Modify file repeatedly for i := 0; i < 3; i++ { time.Sleep(100 * time.Millisecond) file.WriteString("more data\n") } file.Close() // Should eventually return true select { case stable := <-stableChan: assert.True(t, stable) case <-time.After(5 * time.Second): t.Fatal("waitForFileStability timeout") } } func TestProcessDirtyDirectories_BatchesScans(t *testing.T) { db := setupTestDB(t) scanner := NewMediaScanner(db) ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() // Mark directory dirty multiple times rapidly for i := 0; i < 5; i++ { scanner.markDirectoryDirty("/test/folder/subdir") time.Sleep(100 * time.Millisecond) } go scanner.processDirtyDirectories(ctx) // Should wait 10 seconds before processing scanner.dirtyDirsMu.RLock() count := len(scanner.dirtyDirs) scanner.dirtyDirsMu.RUnlock() assert.Equal(t, 1, count, "Directory should still be in dirty list") // Wait for batch to complete time.Sleep(15 * time.Second) scanner.dirtyDirsMu.RLock() count = len(scanner.dirtyDirs) scanner.dirtyDirsMu.RUnlock() assert.Equal(t, 0, count, "All dirty directories should be processed after 10s") }