Files
felhom-controller/controller/internal/upwatch/explain_test.go
T

156 lines
5.3 KiB
Go

package upwatch
import (
"context"
"encoding/base64"
"encoding/json"
"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)
// R-1292: the 50 % rule reads and writes the NORMAL slot through app/preferences (normal mode here: the same value).
case "/api/v2/app/preferences":
fmt.Fprintf(w, `{"up_limit":%d,"alt_up_limit":0}`, *limit)
case "/api/v2/app/setPreferences":
r.ParseForm()
var m map[string]int64
if err := json.Unmarshal([]byte(r.Form.Get("json")), &m); err != nil {
w.WriteHeader(400)
return
}
if v, ok := m["up_limit"]; ok {
*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)
}
}
}
}