Files
ggjuku/internal/app/account_downloads_test.go
T
2026-09-30 10:18:32 +08:00

149 lines
7.5 KiB
Go

package app
import (
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
)
func TestOnlineOnlyAccountPersistsAndBlocksDownloadAPIs(t *testing.T) {
app, admin := administratorFixture(t)
created := admin.request(t, http.MethodPost, "/api/ui/admin/accounts", map[string]any{"username": "online-user", "password": accountFixturePassword, "sources": []string{sourceHongguo}, "onlineOnly": true})
if created.Code != http.StatusCreated {
t.Fatal(created.Code, created.Body.String())
}
member := newAccountTestBrowser(t, app)
member.login(t, "online-user", accountFixturePassword)
state := member.refresh(t)
if state["onlineOnly"] != true || state["account"].(map[string]any)["onlineOnly"] != true {
t.Fatal("online-only permission missing from viewer state")
}
for _, test := range []struct {
method, path string
body any
}{
{http.MethodGet, "/api/ui/tasks", nil},
{http.MethodPost, "/api/ui/download", map[string]any{"ids": []string{historyFixtureDramaID}}},
{http.MethodPost, "/api/ui/update", map[string]any{"ids": []string{historyFixtureDramaID}}},
{http.MethodPost, "/api/ui/merge", map[string]any{"dramaIds": []string{historyFixtureDramaID}}},
{http.MethodPost, "/api/ui/tasks/retry", map[string]any{"ids": []string{"existing-task"}}},
{http.MethodPost, "/api/ui/tasks/resume", map[string]any{"ids": []string{"existing-task"}}},
{http.MethodPost, "/api/ui/tasks/pause", map[string]any{"ids": []string{"existing-task"}}},
{http.MethodPost, "/api/ui/tasks/cancel", map[string]any{"ids": []string{"existing-task"}}},
{http.MethodPost, "/api/ui/tasks/clear", map[string]any{"all": true}},
{http.MethodPost, "/api/emby/export", map[string]any{"dramaId": historyFixtureDramaID, "baseUrl": "http://local.test"}},
{http.MethodPost, "/api/ui/playback/open", map[string]any{"taskId": "existing-task"}},
} {
response := member.request(t, test.method, test.path, test.body)
if response.Code != http.StatusForbidden || !strings.Contains(response.Body.String(), "download_forbidden") {
t.Fatal("online-only request reached a download handler", test.path, response.Code, response.Body.String())
}
}
session := member.watch(t, app, 1, 12)
app.closePlayback(session)
viewerResultOK(t, member.request(t, http.MethodGet, "/api/ui/following", nil))
viewerResultOK(t, member.request(t, http.MethodGet, "/api/ui/dramas", nil))
if response := member.request(t, http.MethodPost, "/api/ui/admin/accounts/permissions", map[string]any{"username": "online-user", "onlineOnly": false}); response.Code != http.StatusForbidden {
t.Fatal("ordinary user granted themselves downloads", response.Code)
}
if response := admin.request(t, http.MethodPost, "/api/ui/admin/accounts/permissions", map[string]any{"username": "admin", "onlineOnly": true}); response.Code != http.StatusBadRequest {
t.Fatal("administrator permissions were reduced", response.Code)
}
restored := &UIApp{cfg: app.cfg}
saved := restored.browserViewers().accountStore().state.Accounts["online-user"]
if !saved.onlineOnly() || len(saved.Sources) != 1 || saved.Sources[0] != sourceHongguo {
t.Fatal("permissions lost on restart")
}
viewerResultOK(t, admin.request(t, http.MethodPost, "/api/ui/admin/accounts/permissions", map[string]any{"username": "online-user", "onlineOnly": false}))
if state := member.refresh(t); state["onlineOnly"] != false || len(state["sources"].([]any)) != 1 {
t.Fatal("download grant changed source permissions")
}
viewerResultOK(t, member.request(t, http.MethodGet, "/api/ui/tasks", nil))
if (accountRecord{}).onlineOnly() || (accountRecord{Admin: true, OnlineOnly: true}).onlineOnly() {
t.Fatal("legacy or administrator download defaults changed")
}
}
func TestOnlineOnlyRevocationRejectsStalePagesAndAccountExports(t *testing.T) {
app, admin := administratorFixture(t)
member := newAccountTestBrowser(t, app)
member.register(t, "download-revoked")
online := member.watch(t, app, 1, 3)
collection := member.watch(t, app, 1, 4)
app.playbackMu.Lock()
app.playbacks[collection].downloadIDs = []string{"download-one"}
app.playbackMu.Unlock()
account := app.browserViewers().accountStore().state.Accounts["download-revoked"]
key, err := app.embySigningKey(true)
if err != nil {
t.Fatal(err)
}
chapter := "hongguo:7000000000000000001:1"
query := url.Values{"id": {historyFixtureDramaID}, "chapter": {chapter}, "account": {account.ID}, "key": {embyToken(key, historyFixtureDramaID, chapter, account.ID)}}
if result := member.request(t, http.MethodHead, "/api/emby/stream.m3u8?"+query.Encode(), nil); result.Code != http.StatusOK {
t.Fatal("permitted account export rejected", result.Code)
}
viewerResultOK(t, admin.request(t, http.MethodPost, "/api/ui/admin/accounts/permissions", map[string]any{"username": "download-revoked", "onlineOnly": true}))
app.playbackMu.Lock()
keptOnline, keptCollection := app.playbacks[online] != nil, app.playbacks[collection] != nil
app.playbackMu.Unlock()
if !keptOnline || keptCollection {
t.Fatal("revocation did not distinguish online and download playback")
}
request := httptest.NewRequest(http.MethodGet, "http://shared.example.test/api/ui/dramas", nil)
request.Header.Set("X-Juku-Viewer", member.id)
request.Header.Set("X-Juku-Online-Only", "false")
for _, cookie := range member.cookies {
request.AddCookie(cookie)
}
recorder := httptest.NewRecorder()
member.handler.ServeHTTP(recorder, request)
if recorder.Code != http.StatusConflict || !strings.Contains(recorder.Body.String(), "permissions_changed") {
t.Fatal("stale or forged download permission accepted", recorder.Code)
}
for _, endpoint := range []string{"stream.m3u8", "segment.ts"} {
if result := member.request(t, http.MethodGet, "/api/emby/"+endpoint+"?"+query.Encode(), nil); result.Code != http.StatusForbidden {
t.Fatal("revoked account export remains usable", endpoint, result.Code)
}
}
restored := &UIApp{cfg: app.cfg}
if !restored.browserViewers().accountStore().state.Accounts["download-revoked"].onlineOnly() {
t.Fatal("revocation lost after restart")
}
app.closePlayback(online)
}
func TestOnlineOnlyCollectionHistoryResumesOnline(t *testing.T) {
app, admin := administratorFixture(t)
member := newAccountTestBrowser(t, app)
member.register(t, "collection-history")
sessionID := member.watch(t, app, 1, 8)
app.playbackMu.Lock()
session := app.playbacks[sessionID]
session.downloadIDs = []string{"download-one", "download-two", "download-three"}
app.playbackMu.Unlock()
progress := playbackHistoryProgress{Run: 1, Sequence: 2, Episode: 1, Position: 9, Duration: 90}
viewerResultOK(t, member.request(t, http.MethodPost, "/api/ui/playback/progress", map[string]any{"session": sessionID, "progress": progress}))
if history := member.history(t); len(history) != 1 || history[0].Mode != "collection" {
t.Fatal("missing collection history")
}
viewerResultOK(t, admin.request(t, http.MethodPost, "/api/ui/admin/accounts/permissions", map[string]any{"username": "collection-history", "onlineOnly": true}))
response := member.request(t, http.MethodPost, "/api/ui/playback/open", map[string]any{"dramaId": historyFixtureDramaID, "resume": true, "fromHistory": true})
viewerResultOK(t, response)
var opened struct {
Session string `json:"session"`
Mode string `json:"mode"`
InitialPosition float64 `json:"initialPosition"`
}
if err := json.Unmarshal(response.Body.Bytes(), &opened); err != nil {
t.Fatal(err)
}
if opened.Mode != "online" || opened.InitialPosition != 9 {
t.Fatal("collection history did not resume online", response.Body.String())
}
app.closePlayback(opened.Session)
}