feat(lidarrquarantine): admin actions Resolve/DeleteFile/DeleteViaLidarr
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
// Package lidarrquarantine owns the per-user track quarantine workflow.
|
||||
// Users flag a track as broken (Flag/Unflag), the SPA hides the track
|
||||
// from their views, and admins resolve the resulting reports via the
|
||||
// Service's admin actions (Resolve / DeleteFile / DeleteViaLidarr —
|
||||
// the admin action methods are added in a follow-up task).
|
||||
// Service's admin actions (Resolve / DeleteFile / DeleteViaLidarr).
|
||||
package lidarrquarantine
|
||||
|
||||
import (
|
||||
@@ -17,6 +16,7 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/library"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/lidarr"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/lidarrconfig"
|
||||
)
|
||||
@@ -179,3 +179,172 @@ func validReason(r string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Resolve clears all per-user quarantine rows for a track and writes an
|
||||
// audit log row. Idempotent — a track with no rows still writes an audit
|
||||
// entry with affected_users=0 (so admin can see "I clicked resolve on a
|
||||
// track that already had no reports").
|
||||
func (s *Service) Resolve(ctx context.Context, trackID, adminID pgtype.UUID) (dbq.LidarrQuarantineAction, error) {
|
||||
q := dbq.New(s.pool)
|
||||
track, err := q.GetTrackByID(ctx, trackID)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return dbq.LidarrQuarantineAction{}, ErrTrackNotFound
|
||||
}
|
||||
return dbq.LidarrQuarantineAction{}, fmt.Errorf("get track: %w", err)
|
||||
}
|
||||
snap, err := s.snapshot(ctx, q, track)
|
||||
if err != nil {
|
||||
return dbq.LidarrQuarantineAction{}, err
|
||||
}
|
||||
|
||||
affected, err := q.CountQuarantineForTrack(ctx, trackID)
|
||||
if err != nil {
|
||||
return dbq.LidarrQuarantineAction{}, fmt.Errorf("count: %w", err)
|
||||
}
|
||||
if err := q.DeleteQuarantineForTrack(ctx, trackID); err != nil {
|
||||
return dbq.LidarrQuarantineAction{}, fmt.Errorf("delete rows: %w", err)
|
||||
}
|
||||
return q.WriteQuarantineAction(ctx, dbq.WriteQuarantineActionParams{
|
||||
TrackID: trackID, TrackTitle: snap.TrackTitle, ArtistName: snap.ArtistName,
|
||||
AlbumTitle: snap.AlbumTitle, Action: dbq.LidarrQuarantineActionKindResolved,
|
||||
AdminID: adminID, LidarrAlbumMbid: snap.LidarrAlbumMBID, AffectedUsers: affected,
|
||||
})
|
||||
}
|
||||
|
||||
// quarantineSnapshot is the audit-row-ready snapshot of track + album +
|
||||
// artist text columns. Used by the three admin actions.
|
||||
type quarantineSnapshot struct {
|
||||
TrackTitle string
|
||||
ArtistName string
|
||||
AlbumTitle *string
|
||||
LidarrAlbumMBID *string
|
||||
}
|
||||
|
||||
// snapshot pulls the audit-row text columns from track / album / artist.
|
||||
// AlbumTitle is materialized as *string because WriteQuarantineActionParams
|
||||
// expects nullable; we coerce the schema's NOT NULL value into a pointer.
|
||||
func (s *Service) snapshot(ctx context.Context, q *dbq.Queries, track dbq.Track) (quarantineSnapshot, error) {
|
||||
album, err := q.GetAlbumByID(ctx, track.AlbumID)
|
||||
if err != nil {
|
||||
return quarantineSnapshot{}, fmt.Errorf("get album: %w", err)
|
||||
}
|
||||
artist, err := q.GetArtistByID(ctx, track.ArtistID)
|
||||
if err != nil {
|
||||
return quarantineSnapshot{}, fmt.Errorf("get artist: %w", err)
|
||||
}
|
||||
titlePtr := album.Title
|
||||
return quarantineSnapshot{
|
||||
TrackTitle: track.Title,
|
||||
ArtistName: artist.Name,
|
||||
AlbumTitle: &titlePtr,
|
||||
LidarrAlbumMBID: album.Mbid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteFile removes the track file from disk and the tracks row, then
|
||||
// (via FK ON DELETE CASCADE) clears all per-user quarantine rows for that
|
||||
// track and writes an audit row. If the file deletion fails, the per-user
|
||||
// rows stay so admin can retry. No partial state.
|
||||
func (s *Service) DeleteFile(ctx context.Context, trackID, adminID pgtype.UUID) (dbq.LidarrQuarantineAction, error) {
|
||||
q := dbq.New(s.pool)
|
||||
track, err := q.GetTrackByID(ctx, trackID)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return dbq.LidarrQuarantineAction{}, ErrTrackNotFound
|
||||
}
|
||||
return dbq.LidarrQuarantineAction{}, fmt.Errorf("get track: %w", err)
|
||||
}
|
||||
snap, err := s.snapshot(ctx, q, track)
|
||||
if err != nil {
|
||||
return dbq.LidarrQuarantineAction{}, err
|
||||
}
|
||||
|
||||
affected, err := q.CountQuarantineForTrack(ctx, trackID)
|
||||
if err != nil {
|
||||
return dbq.LidarrQuarantineAction{}, fmt.Errorf("count: %w", err)
|
||||
}
|
||||
if err := library.DeleteTrackFile(ctx, s.pool, trackID); err != nil {
|
||||
return dbq.LidarrQuarantineAction{}, fmt.Errorf("delete file: %w", err)
|
||||
}
|
||||
// tracks row is gone; ON DELETE CASCADE on lidarr_quarantine.track_id
|
||||
// already cleared the per-user rows. We don't call
|
||||
// DeleteQuarantineForTrack separately.
|
||||
return q.WriteQuarantineAction(ctx, dbq.WriteQuarantineActionParams{
|
||||
TrackID: trackID, TrackTitle: snap.TrackTitle, ArtistName: snap.ArtistName,
|
||||
AlbumTitle: snap.AlbumTitle, Action: dbq.LidarrQuarantineActionKindDeletedFile,
|
||||
AdminID: adminID, LidarrAlbumMbid: snap.LidarrAlbumMBID, AffectedUsers: affected,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteViaLidarr is the destructive admin path: tells Lidarr to remove
|
||||
// the parent album with deleteFiles=true + addImportListExclusion=true,
|
||||
// then removes Minstrel rows for all tracks of that album. The cascade
|
||||
// on lidarr_quarantine.track_id clears per-user rows automatically.
|
||||
//
|
||||
// On Lidarr failure (unreachable, auth-failed, lookup-empty), nothing
|
||||
// changes locally. Admin retries.
|
||||
//
|
||||
// Returns the audit row + the count of tracks deleted from Minstrel.
|
||||
func (s *Service) DeleteViaLidarr(ctx context.Context, trackID, adminID pgtype.UUID) (dbq.LidarrQuarantineAction, int, error) {
|
||||
cfg, err := s.lidarrCfg.Get(ctx)
|
||||
if err != nil {
|
||||
return dbq.LidarrQuarantineAction{}, 0, fmt.Errorf("load config: %w", err)
|
||||
}
|
||||
client := s.clientFn()
|
||||
if !cfg.Enabled || client == nil {
|
||||
return dbq.LidarrQuarantineAction{}, 0, ErrLidarrDisabled
|
||||
}
|
||||
|
||||
q := dbq.New(s.pool)
|
||||
track, err := q.GetTrackByID(ctx, trackID)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return dbq.LidarrQuarantineAction{}, 0, ErrTrackNotFound
|
||||
}
|
||||
return dbq.LidarrQuarantineAction{}, 0, fmt.Errorf("get track: %w", err)
|
||||
}
|
||||
snap, err := s.snapshot(ctx, q, track)
|
||||
if err != nil {
|
||||
return dbq.LidarrQuarantineAction{}, 0, err
|
||||
}
|
||||
if snap.LidarrAlbumMBID == nil || *snap.LidarrAlbumMBID == "" {
|
||||
return dbq.LidarrQuarantineAction{}, 0, ErrAlbumMBIDMissing
|
||||
}
|
||||
|
||||
affected, err := q.CountQuarantineForTrack(ctx, trackID)
|
||||
if err != nil {
|
||||
return dbq.LidarrQuarantineAction{}, 0, fmt.Errorf("count: %w", err)
|
||||
}
|
||||
|
||||
// Lidarr lookup: MBID -> internal album ID.
|
||||
album, err := client.LookupAlbumByMBID(ctx, *snap.LidarrAlbumMBID)
|
||||
if err != nil {
|
||||
if errors.Is(err, lidarr.ErrNotFound) {
|
||||
return dbq.LidarrQuarantineAction{}, 0, ErrLidarrAlbumNotFound
|
||||
}
|
||||
return dbq.LidarrQuarantineAction{}, 0, fmt.Errorf("lidarr lookup: %w", err)
|
||||
}
|
||||
|
||||
// Lidarr DELETE — both flags true.
|
||||
if err := client.DeleteAlbum(ctx, album.ID, true, true); err != nil {
|
||||
return dbq.LidarrQuarantineAction{}, 0, fmt.Errorf("lidarr delete: %w", err)
|
||||
}
|
||||
|
||||
// Now remove the local rows. CASCADE on lidarr_quarantine.track_id
|
||||
// handles per-user rows.
|
||||
res, err := s.pool.Exec(ctx, "DELETE FROM tracks WHERE album_id = $1", track.AlbumID)
|
||||
if err != nil {
|
||||
// Lidarr is already done; we couldn't update locally. Operator-
|
||||
// recoverable: re-running picks up where we left off.
|
||||
return dbq.LidarrQuarantineAction{}, 0, fmt.Errorf("delete tracks: %w", err)
|
||||
}
|
||||
deletedCount := int(res.RowsAffected())
|
||||
|
||||
action, err := q.WriteQuarantineAction(ctx, dbq.WriteQuarantineActionParams{
|
||||
TrackID: trackID, TrackTitle: snap.TrackTitle, ArtistName: snap.ArtistName,
|
||||
AlbumTitle: snap.AlbumTitle, Action: dbq.LidarrQuarantineActionKindDeletedViaLidarr,
|
||||
AdminID: adminID, LidarrAlbumMbid: snap.LidarrAlbumMBID, AffectedUsers: affected,
|
||||
})
|
||||
return action, deletedCount, err
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -15,6 +17,7 @@ import (
|
||||
"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/lidarr"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/lidarrconfig"
|
||||
)
|
||||
|
||||
@@ -225,3 +228,238 @@ func TestListAdminQueue_AggregatesByTrackWithReasonCounts(t *testing.T) {
|
||||
t.Errorf("reports len = %d, want 3", len(r.Reports))
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolve_ClearsRowsAndWritesAudit(t *testing.T) {
|
||||
pool := newPool(t)
|
||||
alice := seedUser(t, pool, "alice")
|
||||
bob := seedUser(t, pool, "bob")
|
||||
track, _, _ := seedTrack(t, pool, "T", "x")
|
||||
|
||||
svc := NewService(pool, lidarrconfig.New(pool), nil)
|
||||
if _, err := svc.Flag(context.Background(), alice.ID, track.ID, "bad_rip", ""); err != nil {
|
||||
t.Fatalf("alice flag: %v", err)
|
||||
}
|
||||
if _, err := svc.Flag(context.Background(), bob.ID, track.ID, "wrong_tags", ""); err != nil {
|
||||
t.Fatalf("bob flag: %v", err)
|
||||
}
|
||||
|
||||
audit, err := svc.Resolve(context.Background(), track.ID, alice.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve: %v", err)
|
||||
}
|
||||
if audit.AffectedUsers != 2 {
|
||||
t.Errorf("affected_users = %d, want 2", audit.AffectedUsers)
|
||||
}
|
||||
if audit.Action != dbq.LidarrQuarantineActionKindResolved {
|
||||
t.Errorf("action = %v, want resolved", audit.Action)
|
||||
}
|
||||
n, _ := dbq.New(pool).CountQuarantineForTrack(context.Background(), track.ID)
|
||||
if n != 0 {
|
||||
t.Errorf("rows after resolve = %d, want 0", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolve_TrackNotFound(t *testing.T) {
|
||||
pool := newPool(t)
|
||||
user := seedUser(t, pool, "alice")
|
||||
var bogus pgtype.UUID
|
||||
bogus.Bytes = [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
|
||||
bogus.Valid = true
|
||||
|
||||
svc := NewService(pool, lidarrconfig.New(pool), nil)
|
||||
_, err := svc.Resolve(context.Background(), bogus, user.ID)
|
||||
if !errors.Is(err, ErrTrackNotFound) {
|
||||
t.Errorf("err = %v, want ErrTrackNotFound", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteFile_RemovesFileAndAuditsAffected(t *testing.T) {
|
||||
pool := newPool(t)
|
||||
user := seedUser(t, pool, "alice")
|
||||
q := dbq.New(pool)
|
||||
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "track.mp3")
|
||||
if err := os.WriteFile(path, []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
artist, _ := q.UpsertArtist(context.Background(), dbq.UpsertArtistParams{Name: "A", SortName: "A"})
|
||||
album, _ := q.UpsertAlbum(context.Background(), dbq.UpsertAlbumParams{Title: "Al", SortTitle: "Al", ArtistID: artist.ID})
|
||||
track, _ := q.UpsertTrack(context.Background(), dbq.UpsertTrackParams{
|
||||
Title: "T", AlbumID: album.ID, ArtistID: artist.ID,
|
||||
DurationMs: 1000, FilePath: path, FileSize: 1, FileFormat: "mp3",
|
||||
})
|
||||
|
||||
svc := NewService(pool, lidarrconfig.New(pool), nil)
|
||||
if _, err := svc.Flag(context.Background(), user.ID, track.ID, "bad_rip", ""); err != nil {
|
||||
t.Fatalf("Flag: %v", err)
|
||||
}
|
||||
|
||||
audit, err := svc.DeleteFile(context.Background(), track.ID, user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteFile: %v", err)
|
||||
}
|
||||
if audit.AffectedUsers != 1 {
|
||||
t.Errorf("affected_users = %d, want 1", audit.AffectedUsers)
|
||||
}
|
||||
if audit.Action != dbq.LidarrQuarantineActionKindDeletedFile {
|
||||
t.Errorf("action = %v, want deleted_file", audit.Action)
|
||||
}
|
||||
if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Errorf("file still exists: %v", err)
|
||||
}
|
||||
// Track row gone; CASCADE cleared the quarantine row.
|
||||
if _, err := q.GetTrackByID(context.Background(), track.ID); err == nil {
|
||||
t.Errorf("track row still exists")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteViaLidarr_FullCascade(t *testing.T) {
|
||||
pool := newPool(t)
|
||||
user := seedUser(t, pool, "alice")
|
||||
|
||||
var captured []string
|
||||
stub := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
captured = append(captured, r.Method+" "+r.URL.Path+"?"+r.URL.RawQuery)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/api/v1/album" && r.Method == http.MethodGet {
|
||||
_, _ = w.Write([]byte(`[{"id":42,"foreignAlbumId":"al-mbid","title":"Al","artistId":7}]`))
|
||||
return
|
||||
}
|
||||
// DELETE /api/v1/album/42 -> 200.
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
t.Cleanup(stub.Close)
|
||||
|
||||
cfg := lidarrconfig.New(pool)
|
||||
if err := cfg.Save(context.Background(), lidarrconfig.Config{
|
||||
Enabled: true, BaseURL: stub.URL, APIKey: "k",
|
||||
}); err != nil {
|
||||
t.Fatalf("save config: %v", err)
|
||||
}
|
||||
clientFn := func() *lidarr.Client { return lidarr.NewClient(stub.URL, "k") }
|
||||
svc := NewService(pool, cfg, clientFn)
|
||||
|
||||
q := dbq.New(pool)
|
||||
artist, _ := q.UpsertArtist(context.Background(), dbq.UpsertArtistParams{Name: "A", SortName: "A"})
|
||||
albumMBID := "al-mbid"
|
||||
album, _ := q.UpsertAlbum(context.Background(), dbq.UpsertAlbumParams{
|
||||
Title: "Al", SortTitle: "Al", ArtistID: artist.ID, Mbid: &albumMBID,
|
||||
})
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "T.mp3")
|
||||
if err := os.WriteFile(path, []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
track, _ := q.UpsertTrack(context.Background(), dbq.UpsertTrackParams{
|
||||
Title: "T", AlbumID: album.ID, ArtistID: artist.ID,
|
||||
DurationMs: 1000, FilePath: path, FileSize: 1, FileFormat: "mp3",
|
||||
})
|
||||
if _, err := svc.Flag(context.Background(), user.ID, track.ID, "bad_rip", ""); err != nil {
|
||||
t.Fatalf("Flag: %v", err)
|
||||
}
|
||||
|
||||
audit, deleted, err := svc.DeleteViaLidarr(context.Background(), track.ID, user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteViaLidarr: %v", err)
|
||||
}
|
||||
if deleted != 1 {
|
||||
t.Errorf("deleted = %d, want 1 track removed", deleted)
|
||||
}
|
||||
if audit.Action != dbq.LidarrQuarantineActionKindDeletedViaLidarr {
|
||||
t.Errorf("action = %v", audit.Action)
|
||||
}
|
||||
if audit.AffectedUsers != 1 {
|
||||
t.Errorf("affected_users = %d, want 1", audit.AffectedUsers)
|
||||
}
|
||||
if audit.LidarrAlbumMbid == nil || *audit.LidarrAlbumMbid != "al-mbid" {
|
||||
t.Errorf("lidarr_album_mbid = %v", audit.LidarrAlbumMbid)
|
||||
}
|
||||
if _, err := q.GetTrackByID(context.Background(), track.ID); err == nil {
|
||||
t.Errorf("track row still exists")
|
||||
}
|
||||
// Verify Lidarr received DELETE with both flags true (query order may vary).
|
||||
foundDelete := false
|
||||
for _, c := range captured {
|
||||
if c == "DELETE /api/v1/album/42?addImportListExclusion=true&deleteFiles=true" ||
|
||||
c == "DELETE /api/v1/album/42?deleteFiles=true&addImportListExclusion=true" {
|
||||
foundDelete = true
|
||||
}
|
||||
}
|
||||
if !foundDelete {
|
||||
t.Errorf("Lidarr DELETE not called with both flags true; captured = %v", captured)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteViaLidarr_LidarrDisabled(t *testing.T) {
|
||||
pool := newPool(t)
|
||||
user := seedUser(t, pool, "alice")
|
||||
track, _, _ := seedTrack(t, pool, "T", "x")
|
||||
|
||||
svc := NewService(pool, lidarrconfig.New(pool), nil)
|
||||
_, _, err := svc.DeleteViaLidarr(context.Background(), track.ID, user.ID)
|
||||
if !errors.Is(err, ErrLidarrDisabled) {
|
||||
t.Errorf("err = %v, want ErrLidarrDisabled", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteViaLidarr_AlbumMBIDMissing(t *testing.T) {
|
||||
pool := newPool(t)
|
||||
user := seedUser(t, pool, "alice")
|
||||
|
||||
q := dbq.New(pool)
|
||||
artist, _ := q.UpsertArtist(context.Background(), dbq.UpsertArtistParams{Name: "A", SortName: "A"})
|
||||
// Album with NO mbid set.
|
||||
album, _ := q.UpsertAlbum(context.Background(), dbq.UpsertAlbumParams{
|
||||
Title: "Al", SortTitle: "Al", ArtistID: artist.ID, // Mbid: nil
|
||||
})
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "T.mp3")
|
||||
_ = os.WriteFile(path, []byte("x"), 0o644)
|
||||
track, _ := q.UpsertTrack(context.Background(), dbq.UpsertTrackParams{
|
||||
Title: "T", AlbumID: album.ID, ArtistID: artist.ID,
|
||||
DurationMs: 1000, FilePath: path, FileSize: 1, FileFormat: "mp3",
|
||||
})
|
||||
|
||||
stub := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
t.Cleanup(stub.Close)
|
||||
cfg := lidarrconfig.New(pool)
|
||||
_ = cfg.Save(context.Background(), lidarrconfig.Config{Enabled: true, BaseURL: stub.URL, APIKey: "k"})
|
||||
clientFn := func() *lidarr.Client { return lidarr.NewClient(stub.URL, "k") }
|
||||
svc := NewService(pool, cfg, clientFn)
|
||||
_, _ = svc.Flag(context.Background(), user.ID, track.ID, "bad_rip", "")
|
||||
|
||||
_, _, err := svc.DeleteViaLidarr(context.Background(), track.ID, user.ID)
|
||||
if !errors.Is(err, ErrAlbumMBIDMissing) {
|
||||
t.Errorf("err = %v, want ErrAlbumMBIDMissing", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteViaLidarr_LidarrAlbumNotFound(t *testing.T) {
|
||||
pool := newPool(t)
|
||||
user := seedUser(t, pool, "alice")
|
||||
|
||||
stub := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/api/v1/album" && r.Method == http.MethodGet {
|
||||
_, _ = w.Write([]byte("[]")) // Lidarr returns empty -> ErrNotFound
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
t.Cleanup(stub.Close)
|
||||
cfg := lidarrconfig.New(pool)
|
||||
_ = cfg.Save(context.Background(), lidarrconfig.Config{Enabled: true, BaseURL: stub.URL, APIKey: "k"})
|
||||
clientFn := func() *lidarr.Client { return lidarr.NewClient(stub.URL, "k") }
|
||||
svc := NewService(pool, cfg, clientFn)
|
||||
|
||||
track, _, _ := seedTrack(t, pool, "T", "x")
|
||||
_, _ = svc.Flag(context.Background(), user.ID, track.ID, "bad_rip", "")
|
||||
|
||||
_, _, err := svc.DeleteViaLidarr(context.Background(), track.ID, user.ID)
|
||||
if !errors.Is(err, ErrLidarrAlbumNotFound) {
|
||||
t.Errorf("err = %v, want ErrLidarrAlbumNotFound", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user