package service import ( "context" "encoding/json" "net/http" "net/http/httptest" "sync/atomic" "testing" "time" "affiliate_dash/internal/model" ) func TestUpdateConfig_BoundWarnDaysAndAutoRefresh(t *testing.T) { db := newRockTestDB(t) svc := NewRockService(db, NewRockClient("https://example.com")) ctx := context.Background() cfg, err := svc.GetConfig(ctx) if err != nil { t.Fatalf("获取配置失败: %v", err) } if cfg.BoundWarnDays != 2 { t.Fatalf("绑定过期预警默认值应为 2,实际 %d", cfg.BoundWarnDays) } if cfg.AutoRefreshMismatchEnabled { t.Fatalf("自动刷新开关默认必须关闭") } zero := 0 if _, err := svc.UpdateConfig(ctx, UpdateRockConfigReq{BoundWarnDays: &zero}); err == nil { t.Fatalf("绑定过期预警天数小于 1 时必须报错") } tooMany := 366 if _, err := svc.UpdateConfig(ctx, UpdateRockConfigReq{BoundWarnDays: &tooMany}); err == nil { t.Fatalf("绑定过期预警天数大于 365 时必须报错") } seven := 7 enabled := true updated, err := svc.UpdateConfig(ctx, UpdateRockConfigReq{ BoundWarnDays: &seven, AutoRefreshMismatchEnabled: &enabled, }) if err != nil { t.Fatalf("保存绑定过期预警失败: %v", err) } if updated.BoundWarnDays != 7 || !updated.AutoRefreshMismatchEnabled { t.Fatalf("绑定过期预警/自动刷新未保存: %+v", updated) } } func TestListTokens_OverdueFilters(t *testing.T) { db := newRockTestDB(t) svc := NewRockService(db, NewRockClient("https://example.com")) ctx := context.Background() now := time.Now() old := now.AddDate(0, 0, -10) fresh := now.Add(-time.Hour) usingOld := model.RockToken{UpstreamTokenID: "overdue-using", RedeemCode: "C1", Status: model.RockStatusUsing, UsingAt: &old} usingFresh := model.RockToken{UpstreamTokenID: "fresh-using", RedeemCode: "C2", Status: model.RockStatusUsing, UsingAt: &fresh} boundOld := model.RockToken{UpstreamTokenID: "overdue-bound", RedeemCode: "C3", Status: model.RockStatusBound, BoundAt: &old} boundFresh := model.RockToken{UpstreamTokenID: "fresh-bound", RedeemCode: "C4", Status: model.RockStatusBound, BoundAt: &fresh} redeemedOld := model.RockToken{UpstreamTokenID: "old-redeemed", RedeemCode: "C5", Status: model.RockStatusRedeemed, BoundAt: &old} for _, token := range []*model.RockToken{&usingOld, &usingFresh, &boundOld, &boundFresh, &redeemedOld} { if err := db.Create(token).Error; err != nil { t.Fatalf("创建 Token 失败: %v", err) } } cases := []struct { status string wantID uint }{ {rockTokenStatusUsingOverdue, usingOld.ID}, {rockTokenStatusBoundOverdue, boundOld.ID}, } for _, tc := range cases { list, total, err := svc.ListTokens(ctx, 1, 20, tc.status, "") if err != nil { t.Fatalf("查询 %s 失败: %v", tc.status, err) } if total != 1 || len(list) != 1 || list[0].ID != tc.wantID { t.Fatalf("%s 应只返回 ID=%d,实际 total=%d list=%+v", tc.status, tc.wantID, total, list) } } stats, err := svc.GetStats(ctx) if err != nil { t.Fatalf("获取统计失败: %v", err) } if stats.UsingOverdueStock != 1 || stats.UsingDangerStock != 1 || stats.BoundOverdueStock != 1 { t.Fatalf("过期预警统计不符: using_overdue=%d using_danger=%d bound_overdue=%d", stats.UsingOverdueStock, stats.UsingDangerStock, stats.BoundOverdueStock) } } func TestRefreshMismatchTokens_SkipsWhenAutoRefreshDisabled(t *testing.T) { db := newRockTestDB(t) var calls int32 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { atomic.AddInt32(&calls, 1) w.WriteHeader(http.StatusInternalServerError) })) defer server.Close() svc := NewRockService(db, NewRockClient(server.URL)) setRockTestBearer(t, svc, "bearer-test") token := model.RockToken{ UpstreamTokenID: "scan-off", RedeemCode: "CODE-SCAN-OFF", Status: model.RockStatusMismatch, } if err := db.Create(&token).Error; err != nil { t.Fatalf("创建 Token 失败: %v", err) } if err := db.Model(&model.RockToken{}).Where("id = ?", token.ID). UpdateColumn("updated_at", time.Now().Add(-time.Hour)).Error; err != nil { t.Fatalf("调整 updated_at 失败: %v", err) } svc.RefreshMismatchTokens(context.Background()) if atomic.LoadInt32(&calls) != 0 { t.Fatalf("自动刷新关闭时不应调用上游") } var saved model.RockToken if err := db.First(&saved, token.ID).Error; err != nil { t.Fatalf("读取 Token 失败: %v", err) } if saved.Status != model.RockStatusMismatch { t.Fatalf("自动刷新关闭时应保持 mismatch,实际 %s", saved.Status) } } func TestRefreshAllMismatchTokens_FixesMismatch(t *testing.T) { db := newRockTestDB(t) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]interface{}{ "token": map[string]interface{}{ "status": "bound", "bound_account": "痛写心扉", "bound_account_id": "3132710334", }, }) })) defer server.Close() svc := NewRockService(db, NewRockClient(server.URL)) setRockTestBearer(t, svc, "bearer-test") token := model.RockToken{ UpstreamTokenID: "scan-on", RedeemCode: "CODE-SCAN-ON", Status: model.RockStatusMismatch, ExpectedUID: "1111111111", } if err := db.Create(&token).Error; err != nil { t.Fatalf("创建 Token 失败: %v", err) } if err := db.Model(&model.RockToken{}).Where("id = ?", token.ID). UpdateColumn("updated_at", time.Now().Add(-time.Hour)).Error; err != nil { t.Fatalf("调整 updated_at 失败: %v", err) } result, err := svc.RefreshAllMismatchTokens(context.Background()) if err != nil { t.Fatalf("一键刷新失败: %v", err) } if result.Scanned != 1 || result.Fixed != 1 || result.Remaining != 0 || result.Failed != 0 { t.Fatalf("一键刷新结果不符: %+v", result) } var saved model.RockToken if err := db.First(&saved, token.ID).Error; err != nil { t.Fatalf("读取 Token 失败: %v", err) } if saved.Status != model.RockStatusBound || saved.MismatchAt != nil { t.Fatalf("应修复为 bound 并清空 mismatch 标记,实际 %+v", saved) } }