Files
huya-go/internal/sniper/sniper_test.go
T
yml2213 e8bb4eb571 补全过滤入口与直接兑换,优化兑换分类和 WSS 建连
- GUI 接线 Exchange 绑定:新增「直接兑换」按钮,结果带 category/message 并进入结果面板
- 补 -max-price/-min-left(CLI 与 GUI 设置),Target 的价格与库存过滤不再悬空
- 删除死代码 sendUpdateUserInfo 及其请求结构体(浏览器流程不含该帧)
- ClassifyExchange 改为多路判定:合并 msg/actPreCondition/exchangeInfo 文案,status=200 且有 orderId 视为成功,新增 condition 分类(precondition 门禁)
- WSS 建连改为锁外 dial、锁内注册,同 uid 并发调用共享一次握手
- 默认传输层统一为 http(CLI/GUI/README),.gitignore 忽略 .DS_Store
- 同步 Wails 依赖升级(2.12.0 -> 2.15.0)并重新生成前端绑定
2026-10-13 19:57:08 +08:00

320 lines
9.9 KiB
Go

package sniper
import (
"context"
"errors"
"sync"
"testing"
"time"
"huya-go/internal/huya"
)
type fakeActivityClient struct {
mu sync.Mutex
listCount int
exchangeCount int
prewarmCount int
scoreCount int
recordCount int
listResp *huya.GetActPrizeListResp
exchangeFn func(uid int64, sid, pid int32) (*huya.ScoreExchangePrizeResp, error)
}
func (f *fakeActivityClient) GetActPrizeList(ctx context.Context, uid int64, cookie string, sid int32) (*huya.GetActPrizeListResp, error) {
f.mu.Lock()
f.listCount++
f.mu.Unlock()
if f.listResp != nil {
return f.listResp, nil
}
return &huya.GetActPrizeListResp{}, nil
}
func (f *fakeActivityClient) QueryUserScore(ctx context.Context, uid int64, cookie string, sid int32) (*huya.GetUserScoreResp, error) {
f.mu.Lock()
f.scoreCount++
f.mu.Unlock()
return &huya.GetUserScoreResp{Status: 200, Msg: "请求成功"}, nil
}
func (f *fakeActivityClient) GetUserPrizeRecords(ctx context.Context, uid int64, cookie string, sid int32) (*huya.GetUserPrizeRecordsResp, error) {
f.mu.Lock()
f.recordCount++
f.mu.Unlock()
return &huya.GetUserPrizeRecordsResp{Status: 200, Msg: "请求成功"}, nil
}
func (f *fakeActivityClient) ScoreExchangePrize(ctx context.Context, uid int64, cookie string, sid, pid int32) (*huya.ScoreExchangePrizeResp, error) {
f.mu.Lock()
f.exchangeCount++
f.mu.Unlock()
if f.exchangeFn != nil {
return f.exchangeFn(uid, sid, pid)
}
return &huya.ScoreExchangePrizeResp{Status: 200, Msg: "请求成功"}, nil
}
func (f *fakeActivityClient) PrewarmActivity(ctx context.Context, uid int64, cookie string, sid int32) error {
f.mu.Lock()
f.prewarmCount++
f.mu.Unlock()
return nil
}
func (f *fakeActivityClient) counts() (int, int, int) {
f.mu.Lock()
defer f.mu.Unlock()
return f.listCount, f.exchangeCount, f.prewarmCount
}
func TestClassifyExchange(t *testing.T) {
tests := []struct {
name string
resp *huya.ScoreExchangePrizeResp
err error
want string
}{
{name: "success", resp: &huya.ScoreExchangePrizeResp{Status: 200, Msg: "请求成功"}, want: CategorySuccess},
{name: "frequent", resp: &huya.ScoreExchangePrizeResp{Status: 500, Msg: "操作频繁,请稍后再试"}, want: CategoryTooFrequent},
{name: "not open", resp: &huya.ScoreExchangePrizeResp{Status: 500, Msg: "奖品暂未开放兑换"}, want: CategoryNotOpen},
{name: "not enough", resp: &huya.ScoreExchangePrizeResp{Status: 500, Msg: "兑换货币不足"}, want: CategoryNotEnough},
{name: "already", resp: &huya.ScoreExchangePrizeResp{Status: 500, Msg: "已兑换该奖品"}, want: CategoryAlreadyDone},
{name: "off shelf", resp: &huya.ScoreExchangePrizeResp{Status: 500, Msg: "奖品已下架"}, want: CategoryOffShelf},
{name: "sold out", resp: &huya.ScoreExchangePrizeResp{Status: 500, Msg: "库存不足"}, want: CategorySoldOut},
{name: "error", err: errors.New("timeout"), want: CategoryError},
{
name: "success with order id",
resp: &huya.ScoreExchangePrizeResp{Status: 200, OrderID: "sOrderId_2203_12889_1_1783328404_957"},
want: CategorySuccess,
},
{
name: "not enough in precondition tip",
resp: &huya.ScoreExchangePrizeResp{
Status: 500,
Msg: "兑换失败",
ActPreCondition: huya.ActPreCondition{Tip: "积分不足,无法兑换"},
},
want: CategoryNotEnough,
},
{
name: "not enough in condition params",
resp: &huya.ScoreExchangePrizeResp{
Status: 500,
Msg: "兑换失败",
ActPreCondition: huya.ActPreCondition{ConditionParams: map[string]string{"tip": "余额不足"}},
},
want: CategoryNotEnough,
},
{
name: "condition gate without text",
resp: &huya.ScoreExchangePrizeResp{
Status: 500,
ActPreCondition: huya.ActPreCondition{ConditionType: 2},
},
want: CategoryCondition,
},
{name: "empty response", resp: &huya.ScoreExchangePrizeResp{Status: 500}, want: CategoryFailed},
{name: "nil response", resp: nil, want: CategoryFailed},
{
name: "text wins over order id",
resp: &huya.ScoreExchangePrizeResp{Status: 200, OrderID: "order", Msg: "库存不足"},
want: CategorySoldOut,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ClassifyExchange(tt.resp, tt.err); got != tt.want {
t.Fatalf("ClassifyExchange()=%s, want %s", got, tt.want)
}
})
}
}
func TestRunTargetsDirectExchangeSkipsListAndStopsPerAccount(t *testing.T) {
client := &fakeActivityClient{}
runner := Runner{Client: client}
accounts := []Account{{UID: 1, Cookie: "yyuid=1"}, {UID: 2, Cookie: "yyuid=2"}}
err := runner.RunTargets(context.Background(), accounts, []Target{{PID: 12889}}, Config{
SID: 2203,
Loops: 5,
Delay: time.Millisecond,
Concurrency: 2,
StopAccountAfterSuccess: true,
}, nil)
if err != nil {
t.Fatal(err)
}
listCount, exchangeCount, _ := client.counts()
if listCount != 0 {
t.Fatalf("listCount=%d, want 0", listCount)
}
if exchangeCount != len(accounts) {
t.Fatalf("exchangeCount=%d, want %d", exchangeCount, len(accounts))
}
}
func TestRunTargetsTerminalNotEnoughStopsAccountTarget(t *testing.T) {
client := &fakeActivityClient{
exchangeFn: func(uid int64, sid, pid int32) (*huya.ScoreExchangePrizeResp, error) {
return &huya.ScoreExchangePrizeResp{Status: 500, Msg: "兑换货币不足"}, nil
},
}
runner := Runner{Client: client}
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{PID: 12889}}, Config{
SID: 2203,
Loops: 4,
Delay: time.Millisecond,
}, nil)
if err != nil {
t.Fatal(err)
}
_, exchangeCount, _ := client.counts()
if exchangeCount != 1 {
t.Fatalf("exchangeCount=%d, want 1", exchangeCount)
}
}
func TestRunTargetsTooFrequentBackoffSkipsNextLoop(t *testing.T) {
client := &fakeActivityClient{
exchangeFn: func(uid int64, sid, pid int32) (*huya.ScoreExchangePrizeResp, error) {
return &huya.ScoreExchangePrizeResp{Status: 500, Msg: "操作频繁,请稍后再试"}, nil
},
}
runner := Runner{Client: client}
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{PID: 12889}}, Config{
SID: 2203,
Loops: 2,
Delay: time.Millisecond,
TooFrequentBackoff: 50 * time.Millisecond,
}, nil)
if err != nil {
t.Fatal(err)
}
_, exchangeCount, _ := client.counts()
if exchangeCount != 1 {
t.Fatalf("exchangeCount=%d, want 1", exchangeCount)
}
}
func TestRunTargetsSharedListForNameTargets(t *testing.T) {
client := &fakeActivityClient{
listResp: &huya.GetActPrizeListResp{
Status: 200,
Msg: "请求成功",
Prizes: []huya.PrizeInfo{{ID: 12889, SID: 2203, Name: "套装-潮萌小黄鸭", NewScore: 988, LeftNum: 10}},
},
}
runner := Runner{Client: client}
accounts := []Account{{UID: 1, Cookie: "yyuid=1"}, {UID: 2, Cookie: "yyuid=2"}}
err := runner.RunTargets(context.Background(), accounts, []Target{{Name: "小黄鸭"}}, Config{
SID: 2203,
Loops: 1,
Concurrency: 2,
StopAccountAfterSuccess: true,
ListBeforeExchange: true,
SharePrizeList: true,
}, nil)
if err != nil {
t.Fatal(err)
}
listCount, exchangeCount, _ := client.counts()
if listCount != 1 {
t.Fatalf("listCount=%d, want 1", listCount)
}
if exchangeCount != len(accounts) {
t.Fatalf("exchangeCount=%d, want %d", exchangeCount, len(accounts))
}
}
func TestRunTargetsAutoSelectPrefersPrizeWithLeft(t *testing.T) {
var gotPID int32
client := &fakeActivityClient{
listResp: &huya.GetActPrizeListResp{
Status: 200,
Msg: "请求成功",
Prizes: []huya.PrizeInfo{
{ID: 1, SID: 2203, Name: "无剩余", NewScore: 10, LeftNum: 0},
{ID: 2, SID: 2203, Name: "有剩余", NewScore: 20, LeftNum: 5},
},
},
exchangeFn: func(uid int64, sid, pid int32) (*huya.ScoreExchangePrizeResp, error) {
gotPID = pid
return &huya.ScoreExchangePrizeResp{Status: 200, Msg: "请求成功"}, nil
},
}
runner := Runner{Client: client}
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{Auto: true}}, Config{
SID: 2203,
Loops: 1,
StopAccountAfterSuccess: true,
}, nil)
if err != nil {
t.Fatal(err)
}
if gotPID != 2 {
t.Fatalf("gotPID=%d, want 2", gotPID)
}
}
func TestRunTargetsRefreshAfterSuccessEmitsRefreshResults(t *testing.T) {
client := &fakeActivityClient{}
runner := Runner{Client: client}
results := make(chan Result, 8)
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{PID: 12889}}, Config{
SID: 2203,
Loops: 1,
StopAccountAfterSuccess: true,
RefreshAfterSuccess: true,
}, func(result Result) {
results <- result
})
if err != nil {
t.Fatal(err)
}
seenScore := false
seenRecords := false
deadline := time.After(time.Second)
for !seenScore || !seenRecords {
select {
case result := <-results:
seenScore = seenScore || result.Action == "refresh_score"
seenRecords = seenRecords || result.Action == "refresh_records"
case <-deadline:
t.Fatalf("未收到刷新结果: score=%v records=%v", seenScore, seenRecords)
}
}
}
func TestRunTargetsConditionGateKeepsRetrying(t *testing.T) {
client := &fakeActivityClient{
exchangeFn: func(uid int64, sid, pid int32) (*huya.ScoreExchangePrizeResp, error) {
return &huya.ScoreExchangePrizeResp{
Status: 500,
ActPreCondition: huya.ActPreCondition{ConditionType: 3},
}, nil
},
}
runner := Runner{Client: client}
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{PID: 12889}}, Config{
SID: 2203,
Loops: 3,
Delay: 10 * time.Millisecond,
ErrorBackoff: time.Millisecond,
}, nil)
if err != nil {
t.Fatal(err)
}
_, exchangeCount, _ := client.counts()
if exchangeCount != 3 {
t.Fatalf("exchangeCount=%d, want 3", exchangeCount)
}
}