- 日志落盘:新增 internal/gui/logfile.go,写到 <用户配置目录>/huya-go/logs/huya-go.log,单文件 5MB 后滚动保留 5 个备份;「日志」页签顶部显示路径并支持打开目录/复制路径;写盘失败自动降级为仅界面输出 - 修复重复兑换:界面选品会同时提交 pid、名称与 targetText 里的同一 pid,normalizeTargets 现按 pid/名称去重;runState 再按「账号+奖品」记账,多个 target 命中同一奖品时只兑换一次(新增 skip_duplicate 结果) - 修复 use of closed network connection:成功后的异步刷新改为 runState.refresh 等待,RunTargets 返回前不再提前关闭连接 - queryHttpDns 增加 single-flight 与失败冷却,多账号并发建连只解析一次 - 新增 internal/gui/logfile_test.go 与 sniper/huya 回归测试;README 补日志说明
413 lines
13 KiB
Go
413 lines
13 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)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeTargetsDropsDuplicatePrizeTargets(t *testing.T) {
|
|
targets := normalizeTargets([]Target{
|
|
{PID: 13409, Name: "幸运币礼包(小)"},
|
|
{PID: 13409},
|
|
{Name: "幸运币礼包(小)"},
|
|
{PID: 12889, Name: "套装-潮萌小黄鸭"},
|
|
})
|
|
if len(targets) != 2 {
|
|
t.Fatalf("targets=%d, want 2: %+v", len(targets), targets)
|
|
}
|
|
if targets[0].PID != 13409 || targets[1].PID != 12889 {
|
|
t.Fatalf("unexpected targets: %+v", targets)
|
|
}
|
|
}
|
|
|
|
func TestRunTargetsExchangesPrizeOnceWhenTargetsDuplicated(t *testing.T) {
|
|
client := &fakeActivityClient{}
|
|
runner := Runner{Client: client}
|
|
|
|
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{
|
|
{PID: 13409, Name: "幸运币礼包(小)"},
|
|
{PID: 13409},
|
|
}, Config{
|
|
SID: 2277,
|
|
Loops: 3,
|
|
Delay: time.Millisecond,
|
|
StopAccountAfterSuccess: true,
|
|
}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, exchangeCount, _ := client.counts()
|
|
if exchangeCount != 1 {
|
|
t.Fatalf("exchangeCount=%d, want 1(同一奖品不应重复兑换)", exchangeCount)
|
|
}
|
|
}
|
|
|
|
func TestRunTargetsWaitsForRefreshBeforeReturn(t *testing.T) {
|
|
client := &fakeActivityClient{}
|
|
runner := Runner{Client: client}
|
|
|
|
var mu sync.Mutex
|
|
seen := map[string]bool{}
|
|
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{PID: 13409}}, Config{
|
|
SID: 2277,
|
|
Loops: 1,
|
|
StopAccountAfterSuccess: true,
|
|
RefreshAfterSuccess: true,
|
|
}, func(result Result) {
|
|
mu.Lock()
|
|
seen[result.Action] = true
|
|
mu.Unlock()
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if !seen["refresh_score"] || !seen["refresh_records"] {
|
|
t.Fatalf("RunTargets 返回时异步刷新还没结束: %v", seen)
|
|
}
|
|
}
|
|
|
|
func TestRunTargetsDoesNotExchangeSamePrizeTwiceViaNameMatch(t *testing.T) {
|
|
client := &fakeActivityClient{
|
|
listResp: &huya.GetActPrizeListResp{
|
|
Status: 200,
|
|
Prizes: []huya.PrizeInfo{{ID: 13409, Name: "幸运币礼包(小)", NewScore: 20}},
|
|
},
|
|
}
|
|
runner := Runner{Client: client}
|
|
|
|
// 两个不同名称的 target 都会匹配到同一个奖品。
|
|
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{
|
|
{Name: "幸运币"},
|
|
{Name: "礼包"},
|
|
}, Config{
|
|
SID: 2277,
|
|
Loops: 2,
|
|
Delay: time.Millisecond,
|
|
DirectExchange: false,
|
|
StopAccountAfterSuccess: true,
|
|
}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, exchangeCount, _ := client.counts()
|
|
if exchangeCount != 1 {
|
|
t.Fatalf("exchangeCount=%d, want 1(同一奖品不应重复兑换)", exchangeCount)
|
|
}
|
|
}
|