refactor(tests): enhance database handling with resettable tables and truncation

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2026-07-26 15:59:32 -04:00
parent 04c00fa7ed
commit 9dba0e1106
2 changed files with 46 additions and 14 deletions

View File

@ -31,6 +31,7 @@ import (
"github.com/navidrome/navidrome/scanner"
"github.com/navidrome/navidrome/server/events"
"github.com/navidrome/navidrome/tests"
"github.com/navidrome/navidrome/tests/harness"
"github.com/navidrome/navidrome/utils/cache"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
@ -65,8 +66,19 @@ var (
// Windows. A suite-level tempdir plus an AfterSuite close avoids the lock conflict.
var suiteDBTempDir string
// Migrating the schema costs ~400ms, so it runs once per suite and specs reset by truncating.
var userTables []string
var _ = BeforeSuite(func() {
suiteDBTempDir = GinkgoT().TempDir()
DeferCleanup(configtest.SetupConfig())
conf.Server.DbPath = filepath.Join(suiteDBTempDir, "artwork-resolution-e2e.db") + "?_journal_mode=WAL"
conf.Server.DataFolder = conf.NewDir(GinkgoT().TempDir())
db.Db().SetMaxOpenConns(1)
db.Init(request.WithUser(context.Background(), model.User{ID: "admin-1", IsAdmin: true}))
userTables = harness.ResettableTables()
})
var _ = AfterSuite(func() {
@ -86,10 +98,8 @@ func setupResolutionHarness() {
conf.Server.EnableMediaFileCoverArt = true
conf.Server.ArtworkWorkerConcurrency = 1
db.Db().SetMaxOpenConns(1)
rctx = request.WithUser(GinkgoT().Context(), model.User{ID: "admin-1", UserName: "admin", IsAdmin: true})
db.Init(rctx)
DeferCleanup(func() { Expect(tests.ClearDB()).To(Succeed()) })
harness.TruncateDB(userTables)
rds = &tests.MockDataStore{RealDS: persistence.New(db.Db())}

View File

@ -86,17 +86,13 @@ func SetupDB(ctx context.Context, users ...*model.User) *DB {
return h
}
// Restore reloads every table from the golden snapshot via ATTACH DATABASE — much faster than a
// rescan. FTS shadow tables are skipped; they are kept in sync by their content tables' triggers.
func (h *DB) Restore() {
sqlDB := db.Db()
_, err := sqlDB.Exec("PRAGMA foreign_keys = OFF")
Expect(err).ToNot(HaveOccurred())
_, err = sqlDB.Exec("ATTACH DATABASE ? AS snapshot", h.SnapshotPath)
Expect(err).ToNot(HaveOccurred())
rows, err := sqlDB.Query("SELECT name FROM main.sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '%_fts' AND name NOT LIKE '%_fts_%'")
// ResettableTables lists the tables a per-spec reset may write. FTS shadow tables are excluded;
// they are kept in sync by their content tables' triggers, and writing them directly corrupts the
// index.
func ResettableTables() []string {
rows, err := db.Db().Query("SELECT name FROM main.sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '%_fts' AND name NOT LIKE '%_fts_%'")
Expect(err).ToNot(HaveOccurred())
defer rows.Close()
var tables []string
for rows.Next() {
var name string
@ -104,12 +100,38 @@ func (h *DB) Restore() {
tables = append(tables, name)
}
Expect(rows.Err()).ToNot(HaveOccurred())
rows.Close()
return tables
}
// TruncateDB empties every resettable table, leaving the migrated schema in place. Suites whose
// specs each build their own library use this instead of a golden snapshot, since re-migrating the
// schema per spec costs ~400ms.
func TruncateDB(tables []string) {
sqlDB := db.Db()
_, err := sqlDB.Exec("PRAGMA foreign_keys = OFF")
Expect(err).ToNot(HaveOccurred())
defer func() { _, _ = sqlDB.Exec("PRAGMA foreign_keys = ON") }()
for _, table := range tables {
// Table names come from sqlite_master, not user input.
_, err = sqlDB.Exec(`DELETE FROM main."` + table + `"`) //nolint:gosec
Expect(err).ToNot(HaveOccurred())
}
}
// Restore reloads every table from the golden snapshot via ATTACH DATABASE — much faster than a
// rescan.
func (h *DB) Restore() {
sqlDB := db.Db()
_, err := sqlDB.Exec("PRAGMA foreign_keys = OFF")
Expect(err).ToNot(HaveOccurred())
_, err = sqlDB.Exec("ATTACH DATABASE ? AS snapshot", h.SnapshotPath)
Expect(err).ToNot(HaveOccurred())
for _, table := range ResettableTables() {
// Table names come from sqlite_master, not user input.
_, err = sqlDB.Exec(`DELETE FROM main."` + table + `"`) //nolint:gosec
Expect(err).ToNot(HaveOccurred())
_, err = sqlDB.Exec(`INSERT INTO main."` + table + `" SELECT * FROM snapshot."` + table + `"`) //nolint:gosec
Expect(err).ToNot(HaveOccurred())
}