Files
djk/internal/app/provider_legacy_test.go
T

219 lines
8.6 KiB
Go

package app
import (
"context"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"encoding/json"
"errors"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
)
var fixtureLegacyProtocol = legacyProtocol{InterfaceKey: "fixture-interface", ParamKey: "0123456789abcdef", ParamIV: "fedcba9876543210"}
func legacyFixtureDownloader(t *testing.T, transport rankingTransport) *Downloader {
t.Helper()
d := rankingTestDownloader(t, transport)
d.cfg.APIBase = "https://legacy.example.test"
d.cfg.MaxPagesPerSort, d.cfg.PageSize = 1, 3
return d
}
func legacyFixtureResponse(request *http.Request, payload any) *http.Response {
body, _ := json.Marshal(map[string]any{"code": 200, "hash": false, "data": payload})
return rankingHTTPResponse(request, 200, string(body))
}
func legacyFixtureLogin(t *testing.T, request *http.Request) {
t.Helper()
var body struct {
Data string `json:"data"`
}
if request.Method != http.MethodPost || request.Header.Get("Authorization") != "" || json.NewDecoder(request.Body).Decode(&body) != nil {
t.Error("anonymous login must use an encrypted POST without another user's token")
return
}
encrypted, err := base64.StdEncoding.DecodeString(body.Data)
if err != nil || len(encrypted) == 0 || len(encrypted)%16 != 0 {
t.Error("invalid encrypted login")
return
}
block, _ := aes.NewCipher([]byte(fixtureLegacyProtocol.ParamKey))
cipher.NewCBCDecrypter(block, []byte(fixtureLegacyProtocol.ParamIV)).CryptBlocks(encrypted, encrypted)
plain, err := pkcs7Unpad(encrypted, 16)
var fields struct {
Device string `json:"devID"`
System string `json:"sysType"`
Store bool `json:"isAppStore"`
}
if err != nil || json.Unmarshal(plain, &fields) != nil || len(fields.Device) != 29 || fields.System != "ios" || fields.Store ||
!strings.Contains(request.Header.Get("X-User-Agent"), "DevID="+fields.Device+";") {
t.Error("login must carry a fresh consistent H5 device identity")
}
}
func TestLegacyAnonymousCatalogAndSessionReuse(t *testing.T) {
var loginCalls, scriptCalls atomic.Int32
transport := rankingTransport(func(request *http.Request) (*http.Response, error) {
switch request.URL.Path {
case "/":
return rankingHTTPResponse(request, 200, `<script src="https://unrelated.example/main-bad.js"></script><script src="/js/main-fixture.js"></script>`), nil
case "/js/main-fixture.js":
scriptCalls.Add(1)
return rankingHTTPResponse(request, 200, `t(F,"interfaceKey","fixture-interface");t(F,"parameterKey","0123456789abcdef");t(F,"parameterIv","fedcba9876543210")`), nil
case "/api/app/mine/login/h5":
loginCalls.Add(1)
legacyFixtureLogin(t, request)
return legacyFixtureResponse(request, map[string]string{"token": "own-anonymous-session"}), nil
case "/api/app/playlet-tab/all":
if request.Header.Get("Authorization") != "own-anonymous-session" {
t.Error("catalog lost its session")
}
return legacyFixtureResponse(request, map[string]any{"list": []Tab{{ID: "current-tab", Name: "当前分类"}}}), nil
case "/api/app/playlet/home/tab/current-tab":
return legacyFixtureResponse(request, map[string]any{"list": []Drama{{ID: "current-drama", Title: "无图剧库样本", TotalEpisode: 2}}}), nil
case "/api/app/playlet/detail/current-drama":
return legacyFixtureResponse(request, map[string]any{"title": "无图剧库样本", "chapters": []Chapter{{ID: "episode-1", VideoURL: "opaque-video-id"}}}), nil
default:
t.Errorf("unexpected request blocked: %s", request.URL.Path)
return nil, errors.New("unexpected endpoint")
}
})
d := legacyFixtureDownloader(t, transport)
items, err := d.fetchCloudFrontDramas(context.Background())
if err != nil || len(items) != 1 || items[0].Source != "cloudfront" || items[0].ChannelName != "当前分类" {
t.Fatalf("catalog: count=%d error=%v", len(items), err)
}
_, chapters, err := d.GetDramaChapters(context.Background(), "current-drama")
if err != nil || len(chapters) != 1 {
t.Fatalf("chapters: count=%d error=%v", len(chapters), err)
}
if d.cfg.Token != "" || d.cfg.AESKeyHex != "" || d.cfg.InterfaceKey != "" {
t.Fatal("automatic session changed user configuration")
}
info, err := os.Stat(filepath.Join(d.cfg.dataDirectory(), "huangguo-session.json"))
if err != nil || info.Mode().Perm() != 0600 {
t.Fatal("anonymous session must be private", err)
}
restarted := NewDownloader(d.cfg)
restarted.client = &http.Client{Transport: transport}
restarted.limiter = newRequestLimiter(8, time.Nanosecond)
if _, err := restarted.legacyCredentials(context.Background()); err != nil {
t.Fatal(err)
}
if loginCalls.Load() != 1 || scriptCalls.Load() != 1 {
t.Fatal("restart did not reuse the anonymous session and public protocol")
}
}
func TestLegacyLoginWaiterCancellationAndSingleFlight(t *testing.T) {
started, release := make(chan struct{}), make(chan struct{})
var logins atomic.Int32
d := legacyFixtureDownloader(t, func(request *http.Request) (*http.Response, error) {
logins.Add(1)
close(started)
select {
case <-release:
case <-request.Context().Done():
return nil, request.Context().Err()
}
return legacyFixtureResponse(request, map[string]string{"token": "shared-own-session"}), nil
})
d.cfg.InterfaceKey, d.cfg.ParamKey, d.cfg.ParamIV = fixtureLegacyProtocol.InterfaceKey, fixtureLegacyProtocol.ParamKey, fixtureLegacyProtocol.ParamIV
var group sync.WaitGroup
group.Add(1)
go func() {
defer group.Done()
if _, err := d.legacyCredentials(context.Background()); err != nil {
t.Error(err)
}
}()
<-started
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
defer cancel()
if _, err := d.legacyCredentials(ctx); !errors.Is(err, context.DeadlineExceeded) {
t.Fatal("a waiting caller cannot be canceled", err)
}
for i := 0; i < 8; i++ {
group.Add(1)
go func() {
defer group.Done()
if _, err := d.legacyCredentials(context.Background()); err != nil {
t.Error(err)
}
}()
}
close(release)
group.Wait()
if logins.Load() != 1 {
t.Fatal("concurrent callers created multiple anonymous logins")
}
}
func TestLegacyExpiredAnonymousSessionRefreshesOnce(t *testing.T) {
var logins atomic.Int32
d := legacyFixtureDownloader(t, func(request *http.Request) (*http.Response, error) {
if request.URL.Path == "/api/app/mine/login/h5" {
logins.Add(1)
return legacyFixtureResponse(request, map[string]string{"token": "renewed-session"}), nil
}
if request.Header.Get("Authorization") == "expired-session" {
return rankingHTTPResponse(request, 200, `{"code":5005,"msg":"no token"}`), nil
}
return legacyFixtureResponse(request, []Tab{{ID: "valid", Name: "当前分类"}}), nil
})
state := legacySessionState{Version: 1, Scope: d.legacySessionScope(), DeviceID: "0123456789ABCDEF1789290000000", Token: "expired-session", Protocol: fixtureLegacyProtocol, UpdatedAt: time.Now()}
if err := d.saveLegacySession(state); err != nil {
t.Fatal(err)
}
var tabs legacyTabList
if err := d.fetchAPI(context.Background(), "/api/app/playlet-tab/all", nil, &tabs); err != nil || len(tabs) != 1 {
t.Fatal("anonymous recovery failed", err)
}
if logins.Load() != 1 {
t.Fatal("session refresh did not stay bounded")
}
}
func TestLegacyExplicitTokenIsPreservedWithoutMediaKey(t *testing.T) {
var calls atomic.Int32
d := legacyFixtureDownloader(t, func(request *http.Request) (*http.Response, error) {
calls.Add(1)
if request.URL.Path != "/api/app/playlet-tab/all" || request.Header.Get("Authorization") != "configured-session" {
t.Error("explicit configuration was replaced or sent to another endpoint")
}
return rankingHTTPResponse(request, 200, `{"code":5005,"msg":"expired configured session"}`), nil
})
d.cfg.Token = "configured-session"
d.cfg.InterfaceKey, d.cfg.ParamKey, d.cfg.ParamIV = fixtureLegacyProtocol.InterfaceKey, fixtureLegacyProtocol.ParamKey, fixtureLegacyProtocol.ParamIV
var tabs legacyTabList
if err := d.fetchAPI(context.Background(), "/api/app/playlet-tab/all", nil, &tabs); err == nil {
t.Fatal("expired explicit token was ignored")
}
if calls.Load() != 1 || d.cfg.Token != "configured-session" {
t.Fatal("explicit token was silently renewed")
}
}
func TestLegacyPublicProtocolValidation(t *testing.T) {
for _, script := range []string{"", `t(F,"parameterKey","short")`, `t(F,"interfaceKey","fixture");t(F,"parameterKey","short");t(F,"parameterIv","fedcba9876543210")`} {
if _, err := parseLegacyProtocol(script); err == nil {
t.Fatal("accepted incomplete or invalid public protocol")
}
}
var tabs legacyTabList
for _, raw := range []string{`[{"id":"a","name":"甲"}]`, `{"list":[{"id":"a","name":"甲"}],"count":1}`} {
if json.Unmarshal([]byte(raw), &tabs) != nil || len(tabs) != 1 || tabs[0].ID != "a" {
t.Fatal("tab response format not supported")
}
}
}