package upwatch import ( "context" "encoding/base64" "fmt" "net/http" "net/http/httptest" "strings" "testing" ) type envBox map[string]string func (b envBox) Env(c, n string) (string, error) { if v, ok := b[c+"/"+n]; ok { return v, nil } return "", fmt.Errorf("no %s in %s", n, c) } func (b envBox) Running(string) bool { return true } // qBittorrent as measured on 9202: a login is 204 + a SID cookie, and only when Referer equals the base; transfer/info // carries up_info_data; a stale session is 403. func fakeQBServer(t *testing.T, pw string, logins *int, expireNext *bool, limit *int64) *httptest.Server { var srv *httptest.Server srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/api/v2/auth/login": r.ParseForm() if r.Header.Get("Referer") != srv.URL || r.Form.Get("username") != "admin" || r.Form.Get("password") != pw { w.WriteHeader(401) return } *logins++ http.SetCookie(w, &http.Cookie{Name: "QBT_SID_8080", Value: fmt.Sprint("s", *logins)}) w.WriteHeader(204) case "/api/v2/transfer/info": if *expireNext { *expireNext = false w.WriteHeader(403) return } if !strings.HasPrefix(r.Header.Get("Cookie"), "QBT_SID_8080=") { w.WriteHeader(403) return } fmt.Fprint(w, `{"up_info_data":123456789,"up_info_speed":16384000,"up_rate_limit":0}`) case "/api/v2/transfer/uploadLimit": fmt.Fprint(w, *limit) case "/api/v2/transfer/setUploadLimit": r.ParseForm() var v int64 fmt.Sscan(r.Form.Get("limit"), &v) *limit = v / 1024 * 1024 w.WriteHeader(200) default: w.WriteHeader(404) } })) return srv } // RED-PROOF: send no Referer → 401 → fails; log in every minute instead of keeping the session → logins == 3 → fails. func TestQBittorrent_AskKeepsTheSessionAndReLogsInOn403(t *testing.T) { logins, expire, limit := 0, false, int64(0) srv := fakeQBServer(t, "pw-canary", &logins, &expire, &limit) defer srv.Close() q := &QBittorrent{Box: envBox{"qbittorrent/QBT_ADMIN_PASSWORD": "pw-canary"}, URL: srv.URL} for i := 0; i < 3; i++ { r, err := q.Ask(context.Background()) if err != nil || !r.HasUploadCounter || r.UploadCounter != 123456789 || !r.HasLimit { t.Fatalf("ask %d: %+v %v", i, r, err) } } if logins != 1 { t.Fatalf("logins = %d for three minutes — the session must be kept", logins) } expire = true if _, err := q.Ask(context.Background()); err != nil || logins != 2 { t.Fatalf("after a 403: err=%v logins=%d — want one fresh login", err, logins) } got, err := q.SetUploadLimit(context.Background(), 2500000) if err != nil || got != 2499584 { t.Fatalf("SetUploadLimit = %d %v, want the stored 2499584", got, err) } } // Transmission's RPC: a 409 hands out the session id; Basic auth with the catalog's PASS. func TestTransmission_AskFollowsTheSessionID(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Authorization") != "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:tr-pw")) { w.WriteHeader(401) return } if r.Header.Get("X-Transmission-Session-Id") != "sid-1" { w.Header().Set("X-Transmission-Session-Id", "sid-1") w.WriteHeader(409) return } fmt.Fprint(w, `{"arguments":{"current-stats":{"uploadedBytes":987654321},"uploadSpeed":0},"result":"success"}`) })) defer srv.Close() tr := &Transmission{Box: envBox{"transmission/PASS": "tr-pw"}, URL: srv.URL} r, err := tr.Ask(context.Background()) if err != nil || r.UploadCounter != 987654321 || !r.HasUploadCounter { t.Fatalf("Ask = %+v %v", r, err) } } // Plex: the token goes along only when the server is claimed; both session lists are read. // RED-PROOF: always send the header → the unclaimed case sends "X-Plex-Token: " → fails. func TestPlex_AskReadsSessionsWithTheTokenOnlyWhenClaimed(t *testing.T) { var seen []string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, has := r.Header["X-Plex-Token"] seen = append(seen, fmt.Sprintf("%s token=%v", r.URL.Path, has)) switch r.URL.Path { case "/status/sessions": fmt.Fprint(w, `{"MediaContainer":{"size":2}}`) case "/transcode/sessions": fmt.Fprint(w, `{"MediaContainer":{"size":1}}`) } })) defer srv.Close() for _, tok := range []string{"", "plex-token-canary"} { seen = nil p := &Plex{Token: func() string { return tok }, URL: func(int) (string, error) { return srv.URL, nil }} r, err := p.Ask(context.Background()) if err != nil || r.Viewers != 2 || r.Transcodes != 1 || !r.HasViewerCount || !r.HasTranscodeCount { t.Fatalf("Ask = %+v %v", r, err) } want := fmt.Sprintf("token=%v", tok != "") for _, s := range seen { if !strings.HasSuffix(s, want) { t.Fatalf("claimed=%v: request %q", tok != "", s) } } } }