package client import ( "net/http" "net/http/httptest" "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 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 }