package store import ( "context" "database/sql" "fmt" "os" "path/filepath" "strings" "sync" "testing" ) func TestSQLiteStore(t *testing.T) { ctx := context.Background() path := filepath.Join(t.TempDir(), "test.db") s, err := NewSQLite(path) if err != nil { t.Fatalf("NewSQLite() error = %v", err) } defer func() { _ = s.Close() }() ok, err := s.IsDownloaded(ctx, "qobuz", "a") if err != nil { t.Fatalf("IsDownloaded() error = %v", err) } if ok { t.Fatalf("expected not downloaded") } if err = s.MarkDownloaded(ctx, "qobuz", "a"); err != nil { t.Fatalf("MarkDownloaded() error = %v", err) } ok, err = s.IsDownloaded(ctx, "qobuz", "a") if err != nil { t.Fatalf("IsDownloaded() error = %v", err) } if !ok { t.Fatalf("expected downloaded") } ok, err = s.IsDownloaded(ctx, "tidal", "a") if err != nil { t.Fatalf("IsDownloaded() error = %v", err) } if ok { t.Fatalf("expected source-specific download tracking") } if err = s.MarkFailed(ctx, "qobuz", "track", "1"); err != nil { t.Fatalf("MarkFailed() error = %v", err) } } // makeLegacyDB creates a database with the pre-source-column schema and the // given ids, as a pre-migration install would have on disk. func makeLegacyDB(t *testing.T, path string, ids ...string) { t.Helper() db, err := sql.Open("sqlite", path) if err != nil { t.Fatal(err) } defer func() { _ = db.Close() }() if _, err = db.Exec(`CREATE TABLE downloads (id TEXT NOT NULL PRIMARY KEY)`); err != nil { t.Fatal(err) } for _, id := range ids { if _, err = db.Exec(`INSERT INTO downloads(id) VALUES (?)`, id); err != nil { t.Fatal(err) } } } // TestLegacyMigrationPreservesHistory is the regression test for the migration // that silently wiped the dedupe state: rows were carried over with source=empty // while IsDownloaded matched source exactly, so every previously downloaded // track looked new and the whole library was re-downloaded. func TestLegacyMigrationPreservesHistory(t *testing.T) { ctx := context.Background() dir := t.TempDir() path := filepath.Join(dir, "downloads.db") makeLegacyDB(t, path, "old-1", "old-2") s, err := NewSQLite(path) if err != nil { t.Fatalf("NewSQLite() error = %v", err) } defer func() { _ = s.Close() }() for _, id := range []string{"old-1", "old-2"} { ok, err := s.IsDownloaded(ctx, "qobuz", id) if err != nil { t.Fatalf("IsDownloaded(%q) error = %v", id, err) } if !ok { t.Errorf("pre-migration download %q reported as not downloaded", id) } } ok, err := s.IsDownloaded(ctx, "qobuz", "never-seen") if err != nil { t.Fatal(err) } if ok { t.Error("unknown id reported as downloaded") } } // TestLegacyMigrationKeepsBackupAndLegacyTable asserts the migration is // recoverable: the original file is copied aside and downloads_legacy survives. func TestLegacyMigrationKeepsBackupAndLegacyTable(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "downloads.db") makeLegacyDB(t, path, "old-1") s, err := NewSQLite(path) if err != nil { t.Fatalf("NewSQLite() error = %v", err) } defer func() { _ = s.Close() }() entries, err := os.ReadDir(dir) if err != nil { t.Fatal(err) } backups := 0 for _, e := range entries { if strings.Contains(e.Name(), ".bak-") { backups++ } } if backups != 1 { t.Errorf("expected exactly 1 backup file, found %d in %v", backups, entries) } var n int if err = s.db.QueryRow(`SELECT COUNT(1) FROM downloads_legacy`).Scan(&n); err != nil { t.Fatalf("downloads_legacy was dropped: %v", err) } if n != 1 { t.Errorf("downloads_legacy row count = %d, want 1", n) } if v, err := s.userVersion(); err != nil || v != schemaVersion { t.Errorf("user_version = %d (err %v), want %d", v, err, schemaVersion) } } // TestLegacyRowsDetectedOnReopen covers a database already migrated by the // earlier, lossy code path: the sourceless rows are on disk but nothing in // this process migrated them. func TestLegacyRowsDetectedOnReopen(t *testing.T) { ctx := context.Background() path := filepath.Join(t.TempDir(), "downloads.db") makeLegacyDB(t, path, "old-1") s, err := NewSQLite(path) if err != nil { t.Fatal(err) } _ = s.Close() reopened, err := NewSQLite(path) if err != nil { t.Fatal(err) } defer func() { _ = reopened.Close() }() if !reopened.hasLegacyRows { t.Fatal("legacy rows not detected on reopen") } ok, err := reopened.IsDownloaded(ctx, "tidal", "old-1") if err != nil { t.Fatal(err) } if !ok { t.Error("legacy row not honored after reopen") } } // TestConcurrentAccess exercises the mutex and connection handling under load. func TestConcurrentAccess(t *testing.T) { ctx := context.Background() path := filepath.Join(t.TempDir(), "downloads.db") s, err := NewSQLite(path) if err != nil { t.Fatal(err) } defer func() { _ = s.Close() }() var wg sync.WaitGroup for i := 0; i < 32; i++ { wg.Add(1) go func(n int) { defer wg.Done() id := fmt.Sprintf("t%d", n) if err := s.MarkDownloaded(ctx, "qobuz", id); err != nil { t.Errorf("MarkDownloaded: %v", err) return } if _, err := s.IsDownloaded(ctx, "qobuz", id); err != nil { t.Errorf("IsDownloaded: %v", err) } if err := s.MarkFailed(ctx, "qobuz", "track", id); err != nil { t.Errorf("MarkFailed: %v", err) } }(i) } wg.Wait() } func TestSQLiteDSNPragmasApplied(t *testing.T) { // A path containing '?' must not be parsed as query parameters. dir := t.TempDir() path := filepath.Join(dir, "weird?name#x.db") s, err := NewSQLite(path) if err != nil { t.Fatalf("NewSQLite(%q) = %v", path, err) } defer func() { _ = s.Close() }() var mode string if err := s.db.QueryRow(`PRAGMA journal_mode`).Scan(&mode); err != nil { t.Fatal(err) } if mode != "wal" { t.Errorf("journal_mode = %q, want wal", mode) } var timeout int if err := s.db.QueryRow(`PRAGMA busy_timeout`).Scan(&timeout); err != nil { t.Fatal(err) } if timeout != 10000 { t.Errorf("busy_timeout = %d, want 10000", timeout) } if err := s.MarkDownloaded(context.Background(), "qobuz", "a"); err != nil { t.Fatalf("write to odd path failed: %v", err) } }