Radarr and Sonarr can return HTTP 201 with no body when a movie or series is added. The previous code returned an "accepted_without_response" result with an empty ExternalID, which trapped the reconciler: every subsequent CheckStatus call short-circuited on the empty ID and the request never advanced past queued. When the add POST decodes empty, look the freshly-added record up by TMDB or TVDB ID via the standard list endpoints and use the resulting Arr ID. Fall back to the previous accepted-without-response result only when the lookup also returns no match, preserving the original behavior as a safety net. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
229 lines
7.3 KiB
Go
229 lines
7.3 KiB
Go
package sonarr
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
mediarequests "github.com/Silo-Server/silo-server/internal/requests"
|
|
)
|
|
|
|
func TestSubmitSeriesAddsLookupResult(t *testing.T) {
|
|
qualityProfileID := 3
|
|
tvdbID := 121361
|
|
var posted seriesResource
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if got := r.Header.Get("X-Api-Key"); got != "sonarr-key" {
|
|
t.Fatalf("X-Api-Key = %q, want sonarr-key", got)
|
|
}
|
|
switch r.URL.Path {
|
|
case "/api/v3/series":
|
|
if r.Method == http.MethodGet {
|
|
if got := r.URL.Query().Get("tvdbId"); got != "121361" {
|
|
t.Fatalf("tvdbId = %q, want 121361", got)
|
|
}
|
|
w.Write([]byte(`[]`))
|
|
return
|
|
}
|
|
if r.Method == http.MethodPost {
|
|
if err := json.NewDecoder(r.Body).Decode(&posted); err != nil {
|
|
t.Fatalf("decode posted series: %v", err)
|
|
}
|
|
w.Write([]byte(`{"id":24,"tvdbId":121361}`))
|
|
return
|
|
}
|
|
case "/api/v3/series/lookup":
|
|
if got := r.URL.Query().Get("term"); got != "tvdb:121361" {
|
|
t.Fatalf("lookup term = %q, want tvdb:121361", got)
|
|
}
|
|
w.Write([]byte(`[{"title":"Game of Thrones","tvdbId":121361,"titleSlug":"game-of-thrones"}]`))
|
|
return
|
|
}
|
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewClient(server.Client())
|
|
result, err := client.SubmitSeries(context.Background(), mediarequests.Request{
|
|
MediaType: mediarequests.MediaTypeSeries,
|
|
TMDBID: 1399,
|
|
TVDBID: &tvdbID,
|
|
Title: "Game of Thrones",
|
|
}, mediarequests.Integration{
|
|
Kind: "sonarr",
|
|
BaseURL: server.URL,
|
|
APIKeyRef: "sonarr-key",
|
|
RootFolder: "/series",
|
|
QualityProfileID: &qualityProfileID,
|
|
Options: map[string]any{
|
|
"monitor": "future",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("SubmitSeries returned error: %v", err)
|
|
}
|
|
if result.ExternalID != "24" || result.IntegrationKind != "sonarr" {
|
|
t.Fatalf("result = %+v, want sonarr external id 24", result)
|
|
}
|
|
if posted.RootFolderPath != "/series" || posted.QualityProfileID != qualityProfileID {
|
|
t.Fatalf("posted series = %+v, missing root folder/quality profile", posted)
|
|
}
|
|
if posted.AddOptions.Monitor != "future" {
|
|
t.Fatalf("monitor = %q, want future", posted.AddOptions.Monitor)
|
|
}
|
|
}
|
|
|
|
func TestSubmitSeriesRecoversFromEmptyAddResponse(t *testing.T) {
|
|
qualityProfileID := 3
|
|
tvdbID := 121361
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/api/v3/series/lookup":
|
|
w.Write([]byte(`[{"title":"Game of Thrones","tvdbId":121361,"titleSlug":"game-of-thrones"}]`))
|
|
case "/api/v3/series":
|
|
if r.Method == http.MethodPost {
|
|
w.WriteHeader(http.StatusCreated)
|
|
return
|
|
}
|
|
if r.Method == http.MethodGet {
|
|
if got := r.URL.Query().Get("tvdbId"); got != "121361" {
|
|
t.Fatalf("tvdbId = %q, want 121361", got)
|
|
}
|
|
w.Write([]byte(`[{"id":77,"tvdbId":121361,"title":"Game of Thrones"}]`))
|
|
return
|
|
}
|
|
default:
|
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewClient(server.Client())
|
|
result, err := client.SubmitSeries(context.Background(), mediarequests.Request{
|
|
MediaType: mediarequests.MediaTypeSeries,
|
|
TMDBID: 1399,
|
|
TVDBID: &tvdbID,
|
|
Title: "Game of Thrones",
|
|
}, mediarequests.Integration{
|
|
Kind: "sonarr",
|
|
BaseURL: server.URL,
|
|
APIKeyRef: "sonarr-key",
|
|
RootFolder: "/series",
|
|
QualityProfileID: &qualityProfileID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("SubmitSeries returned error: %v", err)
|
|
}
|
|
if result.ExternalID != "77" {
|
|
t.Fatalf("ExternalID = %q, want 77 (recovered after empty 201)", result.ExternalID)
|
|
}
|
|
if result.ExternalStatus != "queued" {
|
|
t.Fatalf("ExternalStatus = %q, want queued", result.ExternalStatus)
|
|
}
|
|
}
|
|
|
|
func TestSubmitSeriesRejectsNonExactTVDBLookupMatch(t *testing.T) {
|
|
qualityProfileID := 3
|
|
tvdbID := 121361
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/v3/series/lookup" {
|
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
|
}
|
|
w.Write([]byte(`[{"title":"Wrong Show","tvdbId":999,"titleSlug":"wrong-show"}]`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewClient(server.Client())
|
|
_, err := client.SubmitSeries(context.Background(), mediarequests.Request{
|
|
MediaType: mediarequests.MediaTypeSeries,
|
|
TMDBID: 1399,
|
|
TVDBID: &tvdbID,
|
|
Title: "Game of Thrones",
|
|
}, mediarequests.Integration{
|
|
Kind: "sonarr",
|
|
BaseURL: server.URL,
|
|
APIKeyRef: "sonarr-key",
|
|
RootFolder: "/series",
|
|
QualityProfileID: &qualityProfileID,
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for non-exact TVDB lookup match")
|
|
}
|
|
}
|
|
|
|
func TestCheckSeriesStatusReadsQueueDetails(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if got := r.Header.Get("X-Api-Key"); got != "sonarr-key" {
|
|
t.Fatalf("X-Api-Key = %q, want sonarr-key", got)
|
|
}
|
|
if r.URL.Path != "/api/v3/queue/details" {
|
|
t.Fatalf("unexpected path %s", r.URL.Path)
|
|
}
|
|
if got := r.URL.Query().Get("seriesId"); got != "24" {
|
|
t.Fatalf("seriesId = %q, want 24", got)
|
|
}
|
|
w.Write([]byte(`[{"seriesId":24,"status":"downloading","trackedDownloadState":"importing"}]`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewClient(server.Client())
|
|
status, err := client.CheckSeriesStatus(context.Background(), mediarequests.Request{
|
|
MediaType: mediarequests.MediaTypeSeries,
|
|
TMDBID: 1399,
|
|
ExternalID: "24",
|
|
}, mediarequests.Integration{
|
|
Kind: "sonarr",
|
|
BaseURL: server.URL,
|
|
APIKeyRef: "sonarr-key",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CheckSeriesStatus returned error: %v", err)
|
|
}
|
|
if status.Status != mediarequests.StatusDownloading || status.ExternalStatus != "downloading/importing" {
|
|
t.Fatalf("status = %+v, want downloading", status)
|
|
}
|
|
}
|
|
|
|
func TestListSeriesIntegrationOptionsLoadsChoices(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if got := r.Header.Get("X-Api-Key"); got != "sonarr-key" {
|
|
t.Fatalf("X-Api-Key = %q, want sonarr-key", got)
|
|
}
|
|
switch r.URL.Path {
|
|
case "/api/v3/rootfolder":
|
|
w.Write([]byte(`[{"path":"/series","freeSpace":123,"totalSpace":456,"accessible":true}]`))
|
|
case "/api/v3/qualityprofile":
|
|
w.Write([]byte(`[{"id":3,"name":"HD-1080p"}]`))
|
|
case "/api/v3/tag":
|
|
w.Write([]byte(`[{"id":4,"label":"requests"}]`))
|
|
default:
|
|
t.Fatalf("unexpected path %s", r.URL.Path)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewClient(server.Client())
|
|
options, err := client.ListSeriesIntegrationOptions(context.Background(), mediarequests.Integration{
|
|
Kind: "sonarr",
|
|
BaseURL: server.URL,
|
|
APIKeyRef: "sonarr-key",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ListSeriesIntegrationOptions returned error: %v", err)
|
|
}
|
|
if options.Kind != "sonarr" {
|
|
t.Fatalf("kind = %q, want sonarr", options.Kind)
|
|
}
|
|
if len(options.RootFolders) != 1 || options.RootFolders[0].Path != "/series" {
|
|
t.Fatalf("root folders = %+v, want /series", options.RootFolders)
|
|
}
|
|
if len(options.QualityProfiles) != 1 || options.QualityProfiles[0].ID != 3 {
|
|
t.Fatalf("quality profiles = %+v, want id 3", options.QualityProfiles)
|
|
}
|
|
if len(options.Tags) != 1 || options.Tags[0].ID != 4 {
|
|
t.Fatalf("tags = %+v, want id 4", options.Tags)
|
|
}
|
|
}
|