Files
minstrel/internal/importer/importer_test.go
T
bvandeusenandClaude Opus 5.5 edcb3a57ef
release / govulncheck (push) Successful in 15s
release / web (push) Successful in 1m20s
release / go (push) Failing after 1m37s
release / integration (push) Failing after 4m56s
release / android (push) Successful in 5m1s
release / Build signed APK (releases and dev) (push) Successful in 5m13s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Skipped
release / Verify release artifacts (tag releases only) (push) Skipped
feat: the library's folders are set in Admin › Library, and changing a folder moves its tracks with it, keeping their plays, likes and playlists (#6129)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:37:03 -04:00

269 lines
9.2 KiB
Go

package importer
import (
"context"
"io"
"log/slog"
"os"
"path/filepath"
"strings"
"testing"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
"git.fabledsword.com/bvandeusen/minstrel/internal/acquire"
"git.fabledsword.com/bvandeusen/minstrel/internal/catalogue"
"git.fabledsword.com/bvandeusen/minstrel/internal/db"
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
"git.fabledsword.com/bvandeusen/minstrel/internal/dbtest"
"git.fabledsword.com/bvandeusen/minstrel/internal/downloader"
"git.fabledsword.com/bvandeusen/minstrel/internal/wanted"
)
func newPool(t *testing.T) *pgxpool.Pool {
t.Helper()
if testing.Short() {
t.Skip("skipping integration test in -short mode")
}
dsn := os.Getenv("MINSTREL_TEST_DATABASE_URL")
if dsn == "" {
t.Skip("MINSTREL_TEST_DATABASE_URL not set")
}
if err := db.Migrate(dsn, slog.New(slog.NewTextHandler(io.Discard, nil))); err != nil {
t.Fatalf("migrate: %v", err)
}
pool, err := pgxpool.New(context.Background(), dsn)
if err != nil {
t.Fatalf("pool: %v", err)
}
t.Cleanup(pool.Close)
dbtest.ResetDB(t, pool)
return pool
}
const hash = "0123456789abcdef0123456789abcdef01234567"
// fakeClient is a download client holding one finished torrent.
type fakeClient struct {
downloader.Driver
content string
deleted []string
}
func (c *fakeClient) Torrents(_ context.Context, _ ...string) ([]downloader.Torrent, error) {
return []downloader.Torrent{{Hash: hash, Name: "Daft Punk - RAM [FLAC]", Progress: 1, State: "uploading",
ContentPath: c.content}}, nil
}
func (c *fakeClient) Delete(_ context.Context, _ bool, hashes ...string) error {
c.deleted = append(c.deleted, hashes...)
return nil
}
// fakeCatalogue knows RAM as one three-track edition.
type fakeCatalogue struct{}
func (fakeCatalogue) Releases(_ context.Context, _ string) (catalogue.Editions, error) {
return catalogue.Editions{Editions: []catalogue.Edition{{
Release: dbq.MbRelease{Mbid: "rel-ram", Title: "Random Access Memories", Status: "Official", ReleaseDate: "2013-05-17"},
Tracks: []dbq.MbReleaseTrack{
{MediumPosition: 1, MediumFormat: "CD", Position: 1, Title: "Give Life Back to Music", RecordingMbid: "rec-01"},
{MediumPosition: 1, MediumFormat: "CD", Position: 2, Title: "The Game of Love", RecordingMbid: "rec-02"},
{MediumPosition: 1, MediumFormat: "CD", Position: 3, Title: "Giorgio by Moroder", RecordingMbid: "rec-03"},
},
}}}, nil
}
// fakePrinter names each file's recording by its file name: 01.flac is
// rec-01, unless wrong is set.
type fakePrinter struct{ wrong bool }
func (p fakePrinter) Recordings(_ context.Context, path string) ([]string, error) {
if p.wrong {
return []string{"rec-other"}, nil
}
return []string{"rec-" + strings.TrimSuffix(filepath.Base(path), ".flac")}, nil
}
type fakeScanner struct{ scanned []string }
func (s *fakeScanner) ScanFiles(_ context.Context, paths []string) ([]pgtype.UUID, error) {
s.scanned = append(s.scanned, paths...)
return nil, nil
}
type stack struct {
pool *pgxpool.Pool
q *dbq.Queries
im *Importer
client *fakeClient
scanner *fakeScanner
library string
want dbq.Want
grab dbq.Grab
}
// newStack wants RAM, has a finished download of it waiting in a fake
// client, and an importer filing into a temp library.
func newStack(t *testing.T, printer Printer) *stack {
t.Helper()
ctx := context.Background()
pool := newPool(t)
q := dbq.New(pool)
content := t.TempDir()
for _, n := range []string{"01", "02", "03"} {
if err := os.WriteFile(filepath.Join(content, n+".flac"), []byte("not really flac "+n), 0o644); err != nil {
t.Fatal(err)
}
}
w, err := wanted.WantWith(ctx, q, wanted.WantParams{GroupMBID: "rg-ram", ArtistMBID: "mb-dp", ArtistName: "Daft Punk",
Title: "Random Access Memories", PrimaryType: "Album", FirstReleaseDate: "2013-05-17", Reason: wanted.ReasonRequest})
if err != nil {
t.Fatal(err)
}
g, err := acquire.NewMemory(pool).Grabbed(ctx, dbq.InsertGrabParams{WantID: w.ID, InfoHash: hash,
Name: "Daft Punk - RAM [FLAC]", Key: hash, Client: "qbittorrent", SavePath: content, Origin: acquire.OriginSchedule},
pgtype.UUID{}, "")
if err != nil {
t.Fatal(err)
}
s := &stack{pool: pool, q: q, client: &fakeClient{content: content}, scanner: &fakeScanner{},
library: t.TempDir(), want: w, grab: g}
s.im = New(pool, s.scanner, fakeCatalogue{}, printer, func() []string { return []string{s.library} }, slog.New(slog.NewTextHandler(io.Discard, nil)))
s.im.ClientFor = func(context.Context, string) (downloader.Driver, error) { return s.client, nil }
return s
}
func (s *stack) grabNow(t *testing.T) dbq.Grab {
t.Helper()
g, err := s.q.GetGrab(context.Background(), s.grab.ID)
if err != nil {
t.Fatal(err)
}
return g
}
func TestImport_AFinishedDownloadOfTheAlbumIsFiled_Integration(t *testing.T) {
s := newStack(t, fakePrinter{})
rep := s.im.Pass(context.Background())
if rep.Imported != 1 {
t.Fatalf("pass = %+v", rep)
}
g := s.grabNow(t)
if g.ImportState != StateImported || g.EndReason != StateImported || !g.EndedAt.Valid || g.ImportedFiles != 3 {
t.Fatalf("grab = %s %s ended=%v files=%d: %s", g.ImportState, g.EndReason, g.EndedAt.Valid, g.ImportedFiles, g.ImportReason)
}
dir := filepath.Join(s.library, "Daft Punk", "Random Access Memories (2013)")
if g.Destination != dir {
t.Errorf("destination %q", g.Destination)
}
got, err := os.ReadFile(filepath.Join(dir, "Daft Punk - Random Access Memories - 02 - The Game of Love.flac"))
if err != nil || string(got) != "not really flac 02" {
t.Errorf("filed copy: %q %v", got, err)
}
if _, err := os.Stat(filepath.Join(s.client.content, "02.flac")); err != nil {
t.Errorf("the download's own file went: %v", err)
}
if len(s.scanner.scanned) != 3 || len(s.client.deleted) != 0 {
t.Errorf("scanned %v, deleted %v", s.scanner.scanned, s.client.deleted)
}
w, _ := s.q.GetWantByReleaseGroup(context.Background(), "rg-ram")
if !w.SearchedAt.Valid {
t.Error("an imported want is due for search again")
}
if s.im.Pass(context.Background()).Imported != 0 {
t.Error("imported twice")
}
}
func TestImport_TheWrongAlbumIsRefusedBlockedAndSearchedAgain_Integration(t *testing.T) {
s := newStack(t, fakePrinter{wrong: true})
ctx := context.Background()
if err := s.q.MarkWantSearched(ctx, s.want.ID); err != nil {
t.Fatal(err)
}
if rep := s.im.Pass(ctx); rep.Refused != 1 {
t.Fatalf("pass = %+v", rep)
}
g := s.grabNow(t)
if g.ImportState != StateRefused || g.EndReason != StateRefused || !strings.Contains(g.ImportReason, "recordings") {
t.Fatalf("grab = %s %s: %s", g.ImportState, g.EndReason, g.ImportReason)
}
blocks, err := s.q.ListReleaseBlocks(ctx)
if err != nil || len(blocks) != 1 || blocks[0].InfoHash != hash || blocks[0].Reason != acquire.BlockImport {
t.Errorf("blocks = %+v %v", blocks, err)
}
if len(s.client.deleted) != 1 {
t.Errorf("the refused download was not removed: %v", s.client.deleted)
}
w, _ := s.q.GetWantByReleaseGroup(ctx, "rg-ram")
if w.SearchedAt.Valid {
t.Error("a refused want is not due for search")
}
entries, _ := os.ReadDir(s.library)
if len(entries) != 0 {
t.Errorf("the library got %d entries", len(entries))
}
}
func TestImport_ADoubtfulDownloadWaitsForAPersonThenImportsAnyway_Integration(t *testing.T) {
s := newStack(t, nil) // no AcoustID; untagged files: nothing but lengths
ctx := context.Background()
if rep := s.im.Pass(ctx); rep.Review != 1 {
t.Fatalf("pass = %+v", rep)
}
g := s.grabNow(t)
if g.ImportState != StateReview || g.EndedAt.Valid || !strings.Contains(g.ImportReason, "A person decides") {
t.Fatalf("grab = %s ended=%v: %s", g.ImportState, g.EndedAt.Valid, g.ImportReason)
}
if rep := s.im.Pass(ctx); rep.Review+rep.Imported != 0 {
t.Errorf("a held download was looked at again: %+v", rep)
}
if _, err := s.q.ForceGrabImport(ctx, g.ID); err != nil {
t.Fatal(err)
}
if rep := s.im.Pass(ctx); rep.Imported != 1 {
t.Fatalf("after import anyway: %+v", rep)
}
if g := s.grabNow(t); g.ImportState != StateImported || !strings.Contains(g.ImportReason, "anyway") {
t.Errorf("grab = %s: %s", g.ImportState, g.ImportReason)
}
}
func TestImport_APersonRefusesADoubtfulDownload_Integration(t *testing.T) {
s := newStack(t, nil)
ctx := context.Background()
s.im.Pass(ctx)
g, err := s.im.Refuse(ctx, s.grab.ID, pgtype.UUID{})
if err != nil {
t.Fatal(err)
}
if g.ImportState != StateRefused || !g.EndedAt.Valid || len(s.client.deleted) != 1 {
t.Errorf("grab = %s ended=%v deleted=%v", g.ImportState, g.EndedAt.Valid, s.client.deleted)
}
if _, err := s.im.Refuse(ctx, s.grab.ID, pgtype.UUID{}); err == nil {
t.Error("refused twice")
}
}
func TestImport_AnUnfinishedDownloadIsOnlyWatched_Integration(t *testing.T) {
s := newStack(t, fakePrinter{})
s.im.ClientFor = func(context.Context, string) (downloader.Driver, error) { return &halfClient{s.client}, nil }
if rep := s.im.Pass(context.Background()); rep.Downloading != 1 {
t.Fatalf("pass = %+v", rep)
}
g := s.grabNow(t)
if g.ImportState != StateDownloading || g.Progress < 0.49 || g.Progress > 0.51 {
t.Errorf("grab = %s %.2f", g.ImportState, g.Progress)
}
}
type halfClient struct{ *fakeClient }
func (c *halfClient) Torrents(ctx context.Context, h ...string) ([]downloader.Torrent, error) {
ts, _ := c.fakeClient.Torrents(ctx, h...)
ts[0].Progress, ts[0].State = 0.5, "downloading"
return ts, nil
}