package huya import ( "testing" "time" "huya-go/internal/taf" ) // testPrize 只写入断言用到的字段,字段顺序必须与真实响应一致(tag 递增)。 type testPrize struct { id int64 name string score int64 num int64 percent string tags string startTime int64 endTime int64 } func (p testPrize) WriteTo(w *taf.Writer) error { if err := w.WriteInt64(0, p.id); err != nil { return err } if err := w.WriteString(2, p.name); err != nil { return err } if err := w.WriteInt64(3, p.score); err != nil { return err } if err := w.WriteInt64(10, p.num); err != nil { return err } if err := w.WriteInt32(11, 1); err != nil { return err } if err := w.WriteString(12, p.percent); err != nil { return err } if err := w.WriteInt64(13, p.score); err != nil { return err } if err := w.WriteString(14, p.tags); err != nil { return err } if err := w.WriteInt64(25, p.startTime); err != nil { return err } if err := w.WriteInt64(26, p.endTime); err != nil { return err } if err := w.WriteString(27, "00:00:00"); err != nil { return err } return w.WriteString(28, "23:59:59") } type testTag struct { id int64 name string weight int32 } func (t testTag) WriteTo(w *taf.Writer) error { if err := w.WriteInt64(0, t.id); err != nil { return err } if err := w.WriteString(1, t.name); err != nil { return err } return w.WriteInt32(2, int64(t.weight)) } // decodePrizeList 拼一个和线上同构的 getActPrizeList 响应再解码回来。 func decodePrizeList(t *testing.T, tags, prizes []taf.Marshaler) GetActPrizeListResp { t.Helper() w := taf.NewWriter() if err := w.WriteStructBegin(0); err != nil { t.Fatalf("写入结构体失败: %v", err) } if err := w.WriteInt32(0, 200); err != nil { t.Fatalf("写入 status 失败: %v", err) } if err := w.WriteString(1, "请求成功"); err != nil { t.Fatalf("写入 msg 失败: %v", err) } if err := w.WriteListStruct(2, prizes); err != nil { t.Fatalf("写入奖品列表失败: %v", err) } if err := w.WriteInt32(3, 20575); err != nil { t.Fatalf("写入 moduleId 失败: %v", err) } if err := w.WriteListStruct(4, tags); err != nil { t.Fatalf("写入类目字典失败: %v", err) } if err := w.WriteStructEnd(); err != nil { t.Fatalf("写入结构体结尾失败: %v", err) } reader := taf.NewReader(w.Bytes()) if _, typ, err := reader.ReadHead(); err != nil || typ != taf.TypeStructBegin { t.Fatalf("响应头解析失败: typ=%v err=%v", typ, err) } var resp GetActPrizeListResp if err := resp.ReadFrom(reader); err != nil { t.Fatalf("解析响应失败: %v", err) } return resp } func samplePrizeList(t *testing.T) GetActPrizeListResp { t.Helper() // 时间取 2026-10-16 17:00:00(限量返场实际开抢时间)与 2026-10-23 23:59:59。 return decodePrizeList(t, []taf.Marshaler{ testTag{id: 101, name: "精英专享", weight: 2}, testTag{id: 99, name: "限量返场", weight: 1}, }, []taf.Marshaler{ testPrize{id: 13415, name: "套装-雪山精英", score: 600, num: 50000, percent: "93", tags: "101"}, testPrize{id: 13433, name: "怪诞乔克服装礼包", score: 8000, num: 6001, percent: "100", tags: "99", startTime: 1792141200, endTime: 1792771200}, }) } func TestPrizeListCategoryHelpers(t *testing.T) { resp := samplePrizeList(t) if len(resp.Prizes) != 2 { t.Fatalf("奖品数 = %d, 期望 2", len(resp.Prizes)) } limited := resp.Prizes[1] if got := limited.CategoryIDs(); len(got) != 1 || got[0] != 99 { t.Fatalf("CategoryIDs = %v, 期望 [99]", got) } if !limited.InCategory(99) || limited.InCategory(101) { t.Fatalf("InCategory 判定异常: tags=%s", limited.Tags) } start := limited.StartsAt() if start.IsZero() || start.Unix() != 1792141200 { t.Fatalf("StartsAt = %v, 期望 epoch 1792141200", start) } if end := limited.EndsAt(); end.Unix() != 1792771200 { t.Fatalf("EndsAt = %d, 期望 epoch 1792771200", end.Unix()) } if !limited.NotYetOnSale(start.Add(-time.Minute)) { t.Fatal("上架前应判定为未开售") } if limited.NotYetOnSale(start.Add(time.Minute)) { t.Fatal("上架后不应判定为未开售") } if resp.Prizes[0].NotYetOnSale(time.Now().Add(-100 * time.Hour)) { t.Fatal("未下发上架时间的商品不应判定为未开售") } if name := resp.CategoryName(99); name != "限量返场" { t.Fatalf("CategoryName(99) = %q", name) } cats := resp.CategoriesOf(limited) if len(cats) != 1 || cats[0].Name != "限量返场" { t.Fatalf("CategoriesOf = %v", cats) } } func TestPrizeListCategoryStatsAndSummary(t *testing.T) { resp := samplePrizeList(t) stats := resp.CategoryStats() if len(stats) != 2 { t.Fatalf("类目数 = %d, 期望 2", len(stats)) } if stats[0].Name != "精英专享" || stats[0].Count != 1 || stats[0].ID != 101 { t.Fatalf("精英专享统计异常: %+v", stats[0]) } if stats[1].Name != "限量返场" || stats[1].Count != 1 || stats[1].ID != 99 { t.Fatalf("限量返场统计异常: %+v", stats[1]) } // 剩余库存按 percent*num 估算:50000*93% = 46500、6001*100% = 6001。 if stats[0].Left != 46500 { t.Fatalf("精英专享剩余 = %d, 期望 46500", stats[0].Left) } if stats[1].Left != 6001 { t.Fatalf("限量返场剩余 = %d, 期望 6001", stats[1].Left) } want := "精英专享(101)=1项/限量返场(99)=1项" if got := resp.CategorySummary(); got != want { t.Fatalf("CategorySummary = %q, 期望 %q", got, want) } } func TestFilterByCategory(t *testing.T) { resp := samplePrizeList(t) cases := []struct { name string category string wantIDs []int64 }{ {name: "不限类目", category: "", wantIDs: []int64{13415, 13433}}, {name: "按类目名精确匹配", category: "限量返场", wantIDs: []int64{13433}}, {name: "按类目名带空格", category: " 精英专享 ", wantIDs: []int64{13415}}, {name: "按类目 id", category: "99", wantIDs: []int64{13433}}, {name: "按类目名模糊匹配", category: "返场", wantIDs: []int64{13433}}, {name: "字典里没有的类目", category: "不存在的类目", wantIDs: nil}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { filtered := resp.FilterByCategory(tc.category) if len(filtered) != len(tc.wantIDs) { t.Fatalf("命中 %d 项, 期望 %d 项", len(filtered), len(tc.wantIDs)) } for i, want := range tc.wantIDs { if filtered[i].ID != want { t.Fatalf("第 %d 项 pid = %d, 期望 %d", i, filtered[i].ID, want) } } }) } } func TestFilterByCategoryFallsBackToRawTags(t *testing.T) { // 服务端新增类目但没下发字典时,用原始 tag14 文本兜底,避免把商品全过滤掉。 resp := decodePrizeList(t, nil, []taf.Marshaler{testPrize{id: 1, name: "新类目商品", tags: "777"}}, ) if got := resp.FilterByCategory("777"); len(got) != 1 { t.Fatalf("兜底匹配命中 %d 项, 期望 1 项", len(got)) } if stats := resp.CategoryStats(); len(stats) != 0 { t.Fatalf("无字典时不应产出类目统计, 实际 %v", stats) } if summary := resp.CategorySummary(); summary != "未下发类目(共1项)" { t.Fatalf("CategorySummary = %q", summary) } } func TestCategoryIDsIgnoresInvalidValues(t *testing.T) { prize := PrizeInfo{Tags: "99, 101,abc,"} ids := prize.CategoryIDs() if len(ids) != 2 || ids[0] != 99 || ids[1] != 101 { t.Fatalf("CategoryIDs = %v", ids) } if empty := (PrizeInfo{}).CategoryIDs(); empty != nil { t.Fatalf("空 tags 应返回 nil, 实际 %v", empty) } }