Files
djk/internal/app/provider_hongguo_catalog_test.go
T

211 lines
8.6 KiB
Go

package app
import (
"context"
"encoding/json"
"errors"
"net/http"
"reflect"
"strings"
"testing"
"time"
)
type hongguoCatalogRequestFixture struct {
Offset int `json:"offset"`
Session string `json:"session_id"`
Scene string `json:"req_scene"`
}
func hongguoCatalogRequestForTest(t *testing.T, request *http.Request) hongguoCatalogRequestFixture {
t.Helper()
var payload hongguoCatalogRequestFixture
if request.URL.Path != "/reading/distribution/category/landpage/v/" || json.NewDecoder(request.Body).Decode(&payload) != nil {
t.Errorf("unexpected catalog request: %s", request.URL.Path)
}
return payload
}
func hongguoCatalogResponseForTest(request *http.Request, next any, more any, session string, ids ...string) *http.Response {
rows := make([]any, 0, len(ids))
for _, id := range ids {
rows = append(rows, map[string]any{
"series_id": id, "series_title": "文字目录样本 " + id,
"first_visible_time": "1773662280", "hot_score": "20", "series_play_cnt": "0",
})
}
body, _ := json.Marshal(map[string]any{"data": map[string]any{
"next_offset": next, "has_more": more, "session_id": session, "video_data": rows,
}})
return rankingHTTPResponse(request, http.StatusOK, string(body))
}
func TestHongguoCatalogAllowsOverlappingPages(t *testing.T) {
for _, legacy := range []bool{false, true} {
t.Run(map[bool]string{false: "new cursor", true: "legacy cursor"}[legacy], func(t *testing.T) {
var offsets []int
d := rankingTestDownloader(t, func(request *http.Request) (*http.Response, error) {
payload := hongguoCatalogRequestForTest(t, request)
offsets = append(offsets, payload.Offset)
switch payload.Offset {
case 0:
return hongguoCatalogResponseForTest(request, 18, true, "fixture", "700001", "700002"), nil
case 18:
return hongguoCatalogResponseForTest(request, 36, true, "fixture", "700003", "700002"), nil
case 36:
return hongguoCatalogResponseForTest(request, 54, false, "fixture", "700004"), nil
default:
t.Errorf("skipped a page: %d", payload.Offset)
return rankingHTTPResponse(request, 400, ""), nil
}
})
d.cfg.MaxPagesPerSort = 3
for _, genre := range hongguoAppGenres[:2] {
d.hongguoClient().state.Feeds[genre.key] = hongguoCatalogCursor{Initialized: true, Exhausted: true}
}
wantOffsets, wantCount := []int{0, 18, 36}, 4
if legacy {
d.hongguoClient().state.Feeds["ai_series"] = hongguoCatalogCursor{Offset: 18, LastID: "hongguo:700002", Initialized: true}
wantOffsets, wantCount = []int{18, 36}, 3
}
items, err := d.fetchHongguoAppCatalog(context.WithValue(context.Background(), libraryMoreKey{}, true))
if err != nil || len(items) != wantCount || !reflect.DeepEqual(offsets, wantOffsets) {
t.Fatalf("overlap stopped valid pagination: items=%d offsets=%v error=%v", len(items), offsets, err)
}
cursor := d.hongguoCatalogSnapshot().Feeds["ai_series"]
if cursor.Offset != 54 || !cursor.Exhausted {
t.Fatalf("wrong final checkpoint: %+v", cursor)
}
})
}
}
func TestHongguoCatalogRecoversStalledPageAfterRestart(t *testing.T) {
for _, test := range []struct {
name string
next any
more any
ids []string
}{
{name: "unchanged offset", next: 18, more: true, ids: []string{"700003"}},
{name: "regressed offset", next: 0, more: true, ids: []string{"700003"}},
{name: "missing offset", more: true, ids: []string{"700003"}},
{name: "missing continuation flag", next: 36, ids: []string{"700003"}},
{name: "empty page", next: 36, more: true},
{name: "same page", next: 36, more: true, ids: []string{"700001", "700002"}},
{name: "same page reordered", next: 36, more: true, ids: []string{"700002", "700001"}},
} {
t.Run(test.name, func(t *testing.T) {
var calls []hongguoCatalogRequestFixture
transport := func(request *http.Request) (*http.Response, error) {
payload := hongguoCatalogRequestForTest(t, request)
if payload.Scene != "ai_series" {
return hongguoCatalogResponseForTest(request, 0, false, ""), nil
}
calls = append(calls, payload)
switch len(calls) {
case 1:
return hongguoCatalogResponseForTest(request, 18, true, "expired-session", "700001", "700002"), nil
case 2:
return hongguoCatalogResponseForTest(request, test.next, test.more, "expired-session", test.ids...), nil
case 3:
return hongguoCatalogResponseForTest(request, 36, true, "new-session", "700004"), nil
default:
t.Error("retried more than once")
return rankingHTTPResponse(request, 400, ""), nil
}
}
first := rankingTestDownloader(t, transport)
first.cfg.MaxPagesPerSort = 1
initial, err := first.fetchHongguoAppCatalog(context.Background())
if err != nil || len(initial) != 2 {
t.Fatalf("initial batch failed: %v", err)
}
if err := writeLibraryCache(first.cfg.dataDirectory(), libraryCache{Dramas: initial, HongguoApp: first.hongguoCatalogSnapshot()}); err != nil {
t.Fatal(err)
}
cache, err := readLibraryCache(first.cfg.dataDirectory())
if err != nil {
t.Fatal(err)
}
second := rankingTestDownloader(t, transport)
second.cfg.MaxPagesPerSort = 1
second.restoreHongguoCatalog(cache.HongguoApp)
items, err := second.fetchHongguoAppCatalog(context.WithValue(context.Background(), libraryMoreKey{}, true))
if err != nil || len(calls) != 3 {
t.Fatalf("did not recover at the saved position: calls=%+v error=%v", calls, err)
}
if calls[1].Offset != 18 || calls[1].Session != "expired-session" || calls[2].Offset != 18 || calls[2].Session != "" {
t.Fatalf("recovery skipped a page or reused the bad session: %+v", calls)
}
cursor := second.hongguoCatalogSnapshot().Feeds["ai_series"]
if cursor.Offset != 36 || cursor.SessionID != "new-session" || cursor.Exhausted {
t.Fatalf("recovery did not save the valid checkpoint: %+v", cursor)
}
ids := map[string]bool{}
for _, drama := range mergeSourceDramas(cache.Dramas, items, nil, sourceHongguo) {
ids[drama.ID] = true
if drama.OnlineDate != "2026-03-16" || drama.Heat != "20" || drama.Views != "0" {
t.Error("recovery lost primary sorting fields")
}
}
for _, id := range append([]string{"700001", "700002", "700004"}, test.ids...) {
if !ids["hongguo:"+id] {
t.Errorf("recovery discarded a valid item from an earlier attempt: %s", id)
}
}
})
}
}
func TestHongguoCatalogStopsPersistentStallWithoutLosingCheckpoint(t *testing.T) {
for _, session := range []string{"expired-session", ""} {
t.Run(map[bool]string{false: "with session", true: "without session"}[session == ""], func(t *testing.T) {
calls := map[string]int{}
d := rankingTestDownloader(t, func(request *http.Request) (*http.Response, error) {
payload := hongguoCatalogRequestForTest(t, request)
calls[payload.Scene]++
if payload.Scene == "ai_series" {
if payload.Offset != 18 || calls[payload.Scene] > 2 {
t.Errorf("stall skipped pages or retried without a bound: %+v", payload)
}
return hongguoCatalogResponseForTest(request, 18, true, "bad-session", "700003"), nil
}
return hongguoCatalogResponseForTest(request, 18, false, "", "700004"), nil
})
d.cfg.MaxPagesPerSort = 5
saved := hongguoCatalogCursor{Offset: 18, SessionID: session, LastID: "hongguo:700002", Initialized: true, UpdatedAt: time.Now()}
d.hongguoClient().state.Feeds["ai_series"] = saved
items, err := d.fetchHongguoAppCatalog(context.WithValue(context.Background(), libraryMoreKey{}, true))
if err == nil || !strings.Contains(err.Error(), "AI剧: App 分页未前进") || calls["ai_series"] != 2 || len(items) != 2 {
t.Fatalf("persistent failure did not preserve partial results: calls=%v items=%d error=%v", calls, len(items), err)
}
state := d.hongguoCatalogSnapshot()
if state.Feeds["ai_series"] != saved || !hongguoCatalogHasMore(state) {
t.Fatalf("persistent failure changed the saved position: %+v", state.Feeds["ai_series"])
}
for _, genre := range hongguoAppGenres[:2] {
if !state.Feeds[genre.key].Exhausted {
t.Errorf("one stalled feed prevented %s from finishing", genre.name)
}
}
})
}
}
func TestHongguoCatalogDoesNotRetryAfterCancellation(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
calls := 0
d := rankingTestDownloader(t, func(request *http.Request) (*http.Response, error) {
calls++
cancel()
return nil, context.Canceled
})
d.hongguoClient().state.Feeds[hongguoAppGenres[0].key] = hongguoCatalogCursor{SessionID: "fixture", UpdatedAt: time.Now()}
_, err := d.fetchHongguoAppCatalog(ctx)
if !errors.Is(err, context.Canceled) || calls != 1 {
t.Fatalf("cancelled catalog was retried: calls=%d error=%v", calls, err)
}
}