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
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
269 lines
9.2 KiB
Go
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
|
|
}
|