package download import ( "context" "testing" ) // An artist subscription becomes a monitored Lidarr artist, with the // scope translated into Lidarr's own monitor option — so the policy // keeps applying to albums released after the push, which is the whole // reason to mirror a subscription rather than a list of albums. func TestLidarrPushArtistRequestMapsScope(t *testing.T) { t.Parallel() tests := []struct { name string scope RequestScope wantMonitor string }{ {name: "future", scope: ScopeFuture, wantMonitor: "future"}, {name: "all", scope: ScopeAll, wantMonitor: "missing"}, } for _, tt := range tests { stub := newLidarrStub(t) l := newStubLidarr(t, stub) id, err := l.PushRequest(context.Background(), Request{ MBID: "artist-mbid", Entity: EntityArtist, Artist: "Radiohead", Scope: tt.scope, }) if err != nil { t.Fatalf("%s: PushRequest: %v", tt.name, err) } if id != "42" { t.Errorf("%s: external id = %q, want 42", tt.name, id) } stub.mu.Lock() added := append([]map[string]any(nil), stub.addedArtists...) stub.mu.Unlock() if len(added) != 1 { t.Fatalf("%s: added %d artists, want 1", tt.name, len(added)) } opts, ok := added[0]["addOptions"].(map[string]any) if !ok { t.Fatalf("%s: no addOptions in the artist body", tt.name) } if opts["monitor"] != tt.wantMonitor { t.Errorf( "%s: monitor = %v, want %q", tt.name, opts["monitor"], tt.wantMonitor, ) } // Adding an artist must never kick off a discography-wide // search on a system the user shares with their own queue. if opts["searchForMissingAlbums"] != false { t.Errorf("%s: push triggered a search", tt.name) } } } // Pushing a want Lidarr already has returns the existing ID instead of // adding a second copy — the reconciler pushes on every pass, so this // is load-bearing rather than tidy. func TestLidarrPushArtistRequestIsIdempotent(t *testing.T) { t.Parallel() stub := newLidarrStub(t) stub.artists = []map[string]any{{ "id": 7, "artistName": "Radiohead", "foreignArtistId": "artist-mbid", "monitored": true, }} l := newStubLidarr(t, stub) id, err := l.PushRequest(context.Background(), Request{ MBID: "artist-mbid", Entity: EntityArtist, Artist: "Radiohead", }) if err != nil { t.Fatalf("PushRequest: %v", err) } if id != "7" { t.Errorf("external id = %q, want the existing artist's 7", id) } stub.mu.Lock() added := len(stub.addedArtists) stub.mu.Unlock() if added != 0 { t.Errorf("added %d artists, want 0 — it already existed", added) } } // Lidarr cannot express "I want one track", and monitoring the whole // album to get it would download far more than was asked for. func TestLidarrPushRecordingRequestIsSkipped(t *testing.T) { t.Parallel() stub := newLidarrStub(t) l := newStubLidarr(t, stub) id, err := l.PushRequest(context.Background(), Request{ MBID: "recording-mbid", Entity: EntityRecording, Title: "Paranoid Android", }) if err != nil { t.Fatalf("PushRequest: %v", err) } if id != "" { t.Errorf("external id = %q, want empty (not pushed)", id) } stub.mu.Lock() added := len(stub.addedArtists) monitors := len(stub.monitorCalls) stub.mu.Unlock() if added != 0 || monitors != 0 { t.Errorf( "track want touched Lidarr: %d artists, %d monitors", added, monitors, ) } } // Importing adopts monitored artists conservatively: a subscription // pulled in from elsewhere must not queue a back catalogue. func TestLidarrListRequestsImportsMonitoredArtistsOnly(t *testing.T) { t.Parallel() stub := newLidarrStub(t) stub.artists = []map[string]any{ { "id": 1, "artistName": "Radiohead", "foreignArtistId": "artist-1", "monitored": true, }, { "id": 2, "artistName": "Unmonitored Band", "foreignArtistId": "artist-2", "monitored": false, }, { "id": 3, "artistName": "No MBID", "foreignArtistId": "", "monitored": true, }, } l := newStubLidarr(t, stub) requests, err := l.ListRequests(context.Background()) if err != nil { t.Fatalf("ListRequests: %v", err) } if len(requests) != 1 { t.Fatalf("imported %d requests, want 1", len(requests)) } req := requests[0] if req.MBID != "artist-1" { t.Errorf("mbid = %q, want artist-1", req.MBID) } if req.Entity != EntityArtist { t.Errorf("entity = %q, want artist", req.Entity) } if req.Scope != ScopeFuture { t.Errorf("scope = %q, want the conservative future", req.Scope) } } // Removing a want must not tear down a Lidarr setup that may predate // this app: it unmonitors, it does not delete. func TestLidarrRemoveRequestUnmonitorsOnly(t *testing.T) { t.Parallel() stub := newLidarrStub(t) l := newStubLidarr(t, stub) if err := l.RemoveRequest(context.Background(), "55"); err != nil { t.Fatalf("RemoveRequest: %v", err) } stub.mu.Lock() monitors := append([]map[string]any(nil), stub.monitorCalls...) stub.mu.Unlock() if len(monitors) != 1 { t.Fatalf("got %d monitor calls, want 1", len(monitors)) } if monitors[0]["monitored"] != false { t.Errorf("monitored = %v, want false", monitors[0]["monitored"]) } } // The Lister role has to be declared, not just implemented, or the // reconciler never finds it. func TestLidarrDeclaresListerCapability(t *testing.T) { t.Parallel() stub := newLidarrStub(t) l := newStubLidarr(t, stub) if _, ok := asLister(l); !ok { t.Error("lidarr does not present as a Lister") } desc, ok := DescriptorFor(KindLidarr) if !ok { t.Fatal("no descriptor registered for lidarr") } if !desc.Caps.CanList { t.Error("lidarr's descriptor does not declare CanList") } }