Files
dygou_go/internal/client/client_test.go
T
2026-08-08 17:40:16 +08:00

226 lines
7.3 KiB
Go

package client
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"sync/atomic"
"testing"
"douyu_go/internal/activity"
"douyu_go/internal/trace"
"douyu_go/internal/types"
)
func TestGenerateCSRFUseCTNReusesCookie(t *testing.T) {
var called atomic.Bool
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
called.Store(true)
http.Error(w, "should not call ctn endpoint", http.StatusTeapot)
}))
defer server.Close()
cli := testCTNClient(t, server.URL)
token, err := cli.GenerateCSRF("other=1; acf_ccn=ctn-from-cookie")
if err != nil {
t.Fatalf("GenerateCSRF() error = %v", err)
}
if token != "\x00ctn-from-cookie" {
t.Fatalf("GenerateCSRF() token = %q, want ctn token", token)
}
if called.Load() {
t.Fatal("GenerateCSRF() called CTN endpoint despite acf_ccn in Cookie")
}
}
func TestPostMultipartUseCTNPreservesCSRFCookie(t *testing.T) {
var gotCookie string
var gotContentType string
gotFields := map[string]string{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotCookie = r.Header.Get("Cookie")
gotContentType = r.Header.Get("Content-Type")
if err := r.ParseMultipartForm(1 << 20); err != nil {
t.Errorf("ParseMultipartForm() error = %v", err)
http.Error(w, "bad multipart", http.StatusBadRequest)
return
}
for key, values := range r.MultipartForm.Value {
if len(values) > 0 {
gotFields[key] = values[0]
}
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"error":0,"msg":"ok"}`))
}))
defer server.Close()
cli := testCTNClient(t, server.URL)
token := "\x00ctn-new"
var result types.APIResponse
err := cli.postMultipart(server.URL, "cvl_csrf_token=old; other=1", &token, "正式兑换", map[string]string{
"manualID": "manual-1",
"commodityID": "goods-1",
"num": "1",
"rid": "9263298",
"ctn": "ctn-new",
}, &result)
if err != nil {
t.Fatalf("postMultipart() error = %v", err)
}
if !strings.HasPrefix(gotContentType, "multipart/form-data;") {
t.Fatalf("Content-Type = %q, want multipart/form-data", gotContentType)
}
if !strings.Contains(gotCookie, "cvl_csrf_token=old") {
t.Fatalf("Cookie = %q, want preserve old cvl_csrf_token", gotCookie)
}
if !strings.Contains(gotCookie, "acf_ccn=ctn-new") {
t.Fatalf("Cookie = %q, want merged acf_ccn", gotCookie)
}
for key, want := range map[string]string{
"manualID": "manual-1",
"commodityID": "goods-1",
"num": "1",
"rid": "9263298",
"ctn": "ctn-new",
} {
if gotFields[key] != want {
t.Fatalf("field %s = %q, want %q", key, gotFields[key], want)
}
}
}
func TestNetworkDiagnosticIncludesActivityAndFieldNamesOnly(t *testing.T) {
cli := testCTNClient(t, "https://www.douyu.com/wgapi/livenc/liveweb/csrfApi/getCsrfCookie")
req, err := http.NewRequest("POST", "https://www.douyu.com/wgapi/ordnc/activity/peacematch/exchange", nil)
if err != nil {
t.Fatalf("NewRequest() error = %v", err)
}
req = withFormFieldDiagnostics(req, map[string]string{
"manualID": "manual-secret",
"commodityID": "goods-secret",
"ctn": "ctn-secret",
})
got := cli.networkDiagnostic("正式兑换", req)
for _, want := range []string{
"活动=电竞手册(peacematch)",
"UseCTN=true",
"label=正式兑换",
"method=POST",
"path=/wgapi/ordnc/activity/peacematch/exchange",
"manualID=1W0yGkOdDbAXb",
"rid=9263298",
"actAlias=20260226ZMGDG",
"fields=commodityID,ctn,manualID",
} {
if !strings.Contains(got, want) {
t.Fatalf("networkDiagnostic() = %q, want contains %q", got, want)
}
}
for _, leaked := range []string{"manual-secret", "goods-secret", "ctn-secret"} {
if strings.Contains(got, leaked) {
t.Fatalf("networkDiagnostic() leaked field value %q in %q", leaked, got)
}
}
}
func TestProductsAutomaticallyResolvesXiaodianRoleAndPaginates(t *testing.T) {
var goodsOffsets []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/embed":
if !strings.Contains(r.Header.Get("Cookie"), "acf_uid=uid") {
t.Error("授权请求未携带斗鱼 Cookie")
}
if got := r.URL.Query().Get("token"); got != "uid_1_stk_0_ltkid" {
t.Errorf("token = %q", got)
}
_, _ = w.Write([]byte(`{"error":0,"data":{"txurlH5":"https://app.daoju.qq.com/?gameId=cjm&actId=18882&appId=bp_cf&livePlatId=douyu&code=code&timestamp=1&v=2.0&sig=sig"}}`))
case "/role":
if got := r.URL.Query().Get("code"); got != "code" {
t.Errorf("code = %q", got)
}
if got := r.URL.Query().Get("_act_id"); got != "46195" {
t.Errorf("_act_id = %q", got)
}
_, _ = w.Write([]byte(`var info = {"gameOpenId":"openid","roleId":1192083858,"type":"qq","area":1};`))
case "/goods":
offset := r.URL.Query().Get("page_begin")
goodsOffsets = append(goodsOffsets, offset)
if got := r.URL.Query().Get("areaid"); got != "2" {
t.Errorf("areaid = %q", got)
}
if got := r.URL.Query().Get("page_size"); got != "2" {
t.Errorf("page_size = %q", got)
}
items := []map[string]string{}
switch offset {
case "0":
items = []map[string]string{{"iGoodsId": "g1", "sGoodsName": "商品一", "iPrice": "10", "iGoodsLeft": "-1"}, {"iGoodsId": "g2", "sGoodsName": "商品二", "iPrice": "20", "iGoodsLeft": "3"}}
case "2":
items = []map[string]string{{"iGoodsId": "g2", "sGoodsName": "重复商品", "iPrice": "20", "iGoodsLeft": "3"}, {"iGoodsId": "g3", "sGoodsName": "商品三", "iPrice": "30", "iGoodsLeft": "0"}}
}
body, _ := json.Marshal(map[string]any{"data": map[string]any{"client_data": map[string]any{"itemsdetail": items}}})
_, _ = w.Write(append([]byte("var recommend="), append(body, ';')...))
default:
http.NotFound(w, r)
}
}))
defer server.Close()
act := &activity.Activity{
ID: "xiaodian",
DisplayName: "和平小店",
RoomID: "9263298",
ActAlias: "20260623KDQFH",
ActID: "46195",
BaseURL: server.URL,
ShopReferer: server.URL,
RoleParamURL: server.URL + "/embed",
RoleURL: server.URL + "/role",
StoreHomeURL: server.URL + "/goods",
IsXiaodian: true,
}
cli := New(act, "", &trace.Recorder{})
cli.HTTPClient = server.Client()
products, err := cli.Products("acf_uid=uid; acf_stk=stk; acf_ltkid=ltkid", types.XiaodianProductListOptions{PageSize: 2})
if err != nil {
t.Fatalf("Products() error = %v", err)
}
if len(products) != 3 {
t.Fatalf("商品数量 = %d,期望 3", len(products))
}
if products[0].CommodityID != "g1" || products[2].CommodityID != "g3" {
t.Fatalf("商品顺序 = %#v", products)
}
if products[1].Count != 3 || products[2].Score != 30 {
t.Fatalf("商品字段 = %#v", products)
}
if got := strings.Join(goodsOffsets, ","); got != strings.Join([]string{"0", strconv.Itoa(2), strconv.Itoa(4)}, ",") {
t.Fatalf("分页偏移 = %s", got)
}
}
func testCTNClient(t *testing.T, ctnURL string) *DouyuClient {
t.Helper()
act := &activity.Activity{
ID: "peacematch",
DisplayName: "电竞手册",
ManualID: "1W0yGkOdDbAXb",
RoomID: "9263298",
ActAlias: "20260226ZMGDG",
BaseURL: "https://www.douyu.com",
ShopReferer: "https://www.douyu.com/pages/live-peace-pel/web/shop?roomId=9263298&isAnchorSide=0",
UseCTN: true,
CTNURL: ctnURL,
}
cli := New(act, "", &trace.Recorder{})
cli.HTTPClient = http.DefaultClient
return cli
}