SABnzbd client (queue, history, pause/resume, delete, retry) with lenient number decoding and API-key-safe errors. Downloads view polls while visible, shows queue and history, and supports pause, delete (with confirmation) and retry. Add dev/ with pinned Radarr/Sonarr/SABnzbd containers wired together for manual testing. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
125 lines
4.0 KiB
Go
125 lines
4.0 KiB
Go
package sabnzbd
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const queueJSON = `{"queue":{"status":"Downloading","paused":false,"kbpersec":"1536.50","speedlimit_abs":"0",
|
|
"size":"4.0 GB","sizeleft":"2.0 GB","timeleft":"0:21:10","noofslots_total":2,
|
|
"slots":[
|
|
{"nzo_id":"SABnzbd_nzo_a","filename":"Movie.2020.1080p","cat":"radarr","status":"Downloading","priority":"Normal",
|
|
"size":"3 GB","sizeleft":"1 GB","mb":"3072.00","mbleft":"1024.00","percentage":"67","timeleft":"0:11:10"},
|
|
{"nzo_id":"SABnzbd_nzo_b","filename":"Show.S01E01","cat":"sonarr","status":"Queued","mb":1000,"mbleft":1000,"percentage":0}
|
|
]}}`
|
|
|
|
func newTestClient(t *testing.T, h http.HandlerFunc) *Client {
|
|
t.Helper()
|
|
srv := httptest.NewServer(h)
|
|
t.Cleanup(srv.Close)
|
|
return NewClient(srv.URL, "secret")
|
|
}
|
|
|
|
func TestQueue(t *testing.T) {
|
|
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
q := r.URL.Query()
|
|
if q.Get("mode") != "queue" || q.Get("apikey") != "secret" || q.Get("output") != "json" {
|
|
t.Errorf("unexpected request %s", r.URL)
|
|
}
|
|
w.Write([]byte(queueJSON))
|
|
})
|
|
q, err := c.Queue(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if q.Paused || q.KBPerSec != 1536.5 || q.TotalSlots != 2 || len(q.Slots) != 2 {
|
|
t.Fatalf("queue = %+v", q)
|
|
}
|
|
s := q.Slots[0]
|
|
if s.ID != "SABnzbd_nzo_a" || s.Percentage != 67 || s.MB != 3072 || s.Category != "radarr" {
|
|
t.Errorf("slot = %+v", s)
|
|
}
|
|
if q.Slots[1].Percentage != 0 || q.Slots[1].MB != 1000 {
|
|
t.Errorf("numeric (non-string) values not decoded: %+v", q.Slots[1])
|
|
}
|
|
}
|
|
|
|
func TestHistory(t *testing.T) {
|
|
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Query().Get("mode") != "history" || r.URL.Query().Get("limit") != "10" {
|
|
t.Errorf("unexpected request %s", r.URL)
|
|
}
|
|
w.Write([]byte(`{"history":{"slots":[
|
|
{"nzo_id":"a","name":"Good","status":"Completed","size":"1 GB","completed":1700000000},
|
|
{"nzo_id":"b","name":"Bad","status":"Failed","fail_message":"Aborted, cannot be completed"}]}}`))
|
|
})
|
|
h, err := c.History(context.Background(), 10)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(h) != 2 || h[0].Failed() || !h[1].Failed() || h[1].FailMessage == "" {
|
|
t.Fatalf("history = %+v", h)
|
|
}
|
|
}
|
|
|
|
func TestActions(t *testing.T) {
|
|
var got []string
|
|
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
q := r.URL.Query()
|
|
got = append(got, strings.Join([]string{q.Get("mode"), q.Get("name"), q.Get("value"), q.Get("del_files")}, "|"))
|
|
w.Write([]byte(`{"status":true}`))
|
|
})
|
|
ctx := context.Background()
|
|
for _, err := range []error{
|
|
c.Pause(ctx), c.Resume(ctx),
|
|
c.DeleteJob(ctx, "id1", true), c.DeleteHistory(ctx, "id2", false), c.Retry(ctx, "id3"),
|
|
} {
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
want := []string{"pause|||", "resume|||", "queue|delete|id1|1", "history|delete|id2|0", "retry||id3|"}
|
|
if strings.Join(got, ",") != strings.Join(want, ",") {
|
|
t.Errorf("requests = %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestErrors(t *testing.T) {
|
|
tests := map[string]struct {
|
|
status int
|
|
body string
|
|
want string
|
|
}{
|
|
"bad key": {200, `{"status":false,"error":"API Key Incorrect"}`, "API Key Incorrect"},
|
|
"host check": {200, `Access denied - Hostname verification failed`, "Hostname verification failed"},
|
|
"http error": {500, `boom`, "HTTP 500: boom"},
|
|
"status false": {200, `{"status":false}`, "request failed"},
|
|
}
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(tc.status)
|
|
w.Write([]byte(tc.body))
|
|
})
|
|
_, err := c.Version(context.Background())
|
|
if err == nil || !strings.Contains(err.Error(), tc.want) {
|
|
t.Fatalf("err = %v, want containing %q", err, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTransportErrorHidesAPIKey(t *testing.T) {
|
|
c := NewClient("http://127.0.0.1:1", "topsecretkey")
|
|
_, err := c.Version(context.Background())
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
if strings.Contains(err.Error(), "topsecretkey") {
|
|
t.Errorf("error leaks API key: %v", err)
|
|
}
|
|
}
|