package harness
import (
"context"
"errors"
"io"
"os"
"path/filepath"
"strings"
"testing/fstest"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/core/auth"
"github.com/navidrome/navidrome/core/ffmpeg"
"github.com/navidrome/navidrome/core/metrics"
"github.com/navidrome/navidrome/core/playlists"
"github.com/navidrome/navidrome/core/storage/storagetest"
"github.com/navidrome/navidrome/core/stream"
"github.com/navidrome/navidrome/db"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/persistence"
"github.com/navidrome/navidrome/scanner"
"github.com/navidrome/navidrome/server/events"
"github.com/navidrome/navidrome/tests"
"github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
type DB struct {
FilePath string
SnapshotPath string
Library model.Library
}
func CreateFS(files fstest.MapFS) storagetest.FakeFS {
fs := storagetest.FakeFS{}
fs.SetFiles(files)
storagetest.Register("fake", &fs)
return fs
}
func SetupDB(ctx context.Context, users ...*model.User) *DB {
tmpDir := ginkgo.GinkgoT().TempDir()
h := &DB{FilePath: filepath.Join(tmpDir, "test-e2e.db")}
h.SnapshotPath = h.FilePath + ".snapshot"
conf.Server.DbPath = h.FilePath + "?_journal_mode=WAL"
db.Db().SetMaxOpenConns(1)
db.Init(ctx)
ds := &tests.MockDataStore{RealDS: persistence.New(db.Db())}
auth.Init(ds)
h.Library = model.Library{ID: 1, Name: "Music Library", Path: "fake:///music"}
Expect(ds.Library().Put(ctx, &h.Library)).To(Succeed())
for _, u := range users {
seeded := *u
seeded.NewPassword = "password"
Expect(ds.User().Put(ctx, &seeded)).To(Succeed())
Expect(ds.User().SetUserLibraries(ctx, u.ID, []int{h.Library.ID})).To(Succeed())
loaded, err := ds.User().FindByUsername(ctx, u.UserName)
Expect(err).ToNot(HaveOccurred())
u.Libraries = loaded.Libraries
}
s := scanner.New(ctx, ds, events.NoopBroker(),
playlists.NewPlaylists(ds, artwork.NewUploader(ds)), metrics.NewNoopInstance())
_, err := s.ScanAll(ctx, true)
Expect(err).ToNot(HaveOccurred())
_, err = db.Db().Exec("PRAGMA wal_checkpoint(TRUNCATE)")
Expect(err).ToNot(HaveOccurred())
data, err := os.ReadFile(h.FilePath)
Expect(err).ToNot(HaveOccurred())
Expect(os.WriteFile(h.SnapshotPath, data, 0o600)).To(Succeed())
return h
}
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
Expect(rows.Scan(&name)).To(Succeed())
tables = append(tables, name)
}
Expect(rows.Err()).ToNot(HaveOccurred())
return tables
}
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 {
_, err = sqlDB.Exec(`DELETE FROM main."` + table + `"`)
Expect(err).ToNot(HaveOccurred())
}
}
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() {
_, err = sqlDB.Exec(`DELETE FROM main."` + table + `"`)
Expect(err).ToNot(HaveOccurred())
_, err = sqlDB.Exec(`INSERT INTO main."` + table + `" SELECT * FROM snapshot."` + table + `"`)
Expect(err).ToNot(HaveOccurred())
}
_, err = sqlDB.Exec("DETACH DATABASE snapshot")
Expect(err).ToNot(HaveOccurred())
_, err = sqlDB.Exec("PRAGMA foreign_keys = ON")
Expect(err).ToNot(HaveOccurred())
}
type SpyStreamer struct {
LastRequest stream.Request
LastMediaFile *model.MediaFile
SimulateError error
SimulateEmptyStream bool
}
func (s *SpyStreamer) NewStream(_ context.Context, mf *model.MediaFile, req stream.Request) (*stream.Stream, error) {
s.LastRequest = req
s.LastMediaFile = mf
if s.SimulateError != nil {
return nil, s.SimulateError
}
format := req.Format
if format == "" || format == "raw" {
format = mf.Suffix
}
content := "fake audio data"
if s.SimulateEmptyStream {
content = ""
}
return stream.NewStream(mf, format, req.BitRate, io.NopCloser(strings.NewReader(content))), nil
}
type NoopFFmpeg struct{}
func (NoopFFmpeg) Transcode(context.Context, ffmpeg.TranscodeOptions) (io.ReadCloser, error) {
return nil, errors.New("noop ffmpeg: transcode not supported")
}
func (NoopFFmpeg) ExtractImage(context.Context, string) (io.ReadCloser, error) {
return nil, errors.New("noop ffmpeg: extract image not supported")
}
func (NoopFFmpeg) ProbeAudioStream(context.Context, string) (*ffmpeg.AudioProbeResult, error) {
return nil, errors.New("noop ffmpeg: probe not supported")
}
func (NoopFFmpeg) ConvertAnimatedImage(context.Context, io.Reader, int, int) (io.ReadCloser, error) {
return nil, errors.New("noop ffmpeg: convert animated image not supported")
}
func (NoopFFmpeg) CmdPath() (string, error) { return "", nil }
func (NoopFFmpeg) IsAvailable() bool { return false }
func (NoopFFmpeg) IsProbeAvailable() bool { return true }
func (NoopFFmpeg) Version() string { return "noop" }
var (
_ stream.MediaStreamer = &SpyStreamer{}
_ ffmpeg.FFmpeg = NoopFFmpeg{}
)