package service import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "affiliate_dash/internal/model" ) func newImportAllMock(t *testing.T, packages []map[string]interface{}, details map[string]map[string]interface{}) *httptest.Server { t.Helper() return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch { case r.URL.Path == "/api/user/packages": _ = json.NewEncoder(w).Encode(map[string]interface{}{ "packages": packages, "pagination": map[string]interface{}{ "page": 1, "pageSize": 100, "total": len(packages), "totalPages": 1, }, }) case strings.HasPrefix(r.URL.Path, "/api/package/"): code := strings.TrimPrefix(r.URL.Path, "/api/package/") detail, ok := details[code] if !ok { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"error":"package not found"}`)) return } _ = json.NewEncoder(w).Encode(detail) default: w.WriteHeader(http.StatusNotFound) } })) } func TestIsUnboundRockToken(t *testing.T) { cases := []struct { name string token RockPackageToken expect bool }{ {"两个绑定字段都为空", RockPackageToken{BindChangedAt: "", BindChangeType: ""}, true}, {"有 bind_changed_at", RockPackageToken{BindChangedAt: "2026-10-30 16:02:19", BindChangeType: ""}, false}, {"bind 类型", RockPackageToken{BindChangedAt: "", BindChangeType: "bind"}, false}, {"rebind 类型", RockPackageToken{BindChangedAt: "", BindChangeType: "rebind"}, false}, {"两者都有", RockPackageToken{BindChangedAt: "2026-10-30 16:02:19", BindChangeType: "rebind"}, false}, } for _, tc := range cases { if got := isUnboundRockToken(tc.token); got != tc.expect { t.Fatalf("%s: isUnboundRockToken=%v, want %v", tc.name, got, tc.expect) } } } func TestImportAllUpstreamPackages_SkipsImported(t *testing.T) { db := newRockTestDB(t) packages := []map[string]interface{}{ {"id": 100, "token_count": 1, "share_code": "share-100", "status": "active", "created_at": "2026-10-20 10:00:00", "note": "a"}, {"id": 101, "token_count": 1, "share_code": "share-101", "status": "active", "created_at": "2026-10-21 10:00:00", "note": "b"}, } details := map[string]map[string]interface{}{ "share-101": { "package": map[string]interface{}{"id": 101, "share_code": "share-101", "token_count": 1, "status": "active"}, "tokens": []map[string]interface{}{ {"id": 95001, "token": "tk-95001", "color": "black", "region": "Q/V可用", "status": "using", "redeem_code": "rc-95001"}, }, }, } server := newImportAllMock(t, packages, details) defer server.Close() pid := int64(100) if err := db.Create(&model.RockToken{ UpstreamTokenID: "90001", Token: "tk-90001", RedeemCode: "rc-90001", Status: model.RockStatusAvailable, PackageID: &pid, }).Error; err != nil { t.Fatalf("写入本地 Token 失败: %v", err) } svc := NewRockService(db, NewRockClient(server.URL)) setRockTestBearer(t, svc, "bearer-test") result, err := svc.ImportAllUpstreamPackages(context.Background(), 50, 1) if err != nil { t.Fatalf("全量导入失败: %v", err) } if result.Scanned != 2 || result.Skipped != 1 || result.Imported != 1 { t.Fatalf("汇总不符: %+v", result) } if result.TokensImported != 1 { t.Fatalf("应导入 1 个 Token,实际 %d", result.TokensImported) } var token model.RockToken if err := db.Where("upstream_token_id = ?", "95001").First(&token).Error; err != nil { t.Fatalf("查询导入 Token 失败: %v", err) } if token.Status != model.RockStatusAvailable { t.Fatalf("导入后应为 available,实际 %s", token.Status) } } func TestImportAllUpstreamPackages_FiltersBoundTokens(t *testing.T) { db := newRockTestDB(t) packages := []map[string]interface{}{ {"id": 300, "token_count": 3, "share_code": "share-300", "status": "active", "created_at": "2026-10-22 10:00:00", "note": "c"}, } details := map[string]map[string]interface{}{ "share-300": { "package": map[string]interface{}{"id": 300, "share_code": "share-300", "token_count": 3, "status": "active"}, "tokens": []map[string]interface{}{ {"id": 96001, "token": "tk-96001", "color": "black", "region": "Q/V可用", "status": "using", "redeem_code": "rc-96001"}, {"id": 96002, "token": "tk-96002", "color": "black", "region": "Q/V可用", "status": "bound", "redeem_code": "rc-96002", "bind_changed_at": "2026-10-30 16:02:19", "bind_change_type": "bind", "bound_account": "角色A", "bound_account_id": "111"}, {"id": 96003, "token": "tk-96003", "color": "black", "region": "Q/V可用", "status": "redeemed", "redeem_code": "rc-96003", "bind_changed_at": "2026-10-31 12:45:06", "bind_change_type": "rebind", "bound_account": "角色B", "bound_account_id": "222"}, }, }, } server := newImportAllMock(t, packages, details) defer server.Close() svc := NewRockService(db, NewRockClient(server.URL)) setRockTestBearer(t, svc, "bearer-test") result, err := svc.ImportAllUpstreamPackages(context.Background(), 50, 1) if err != nil { t.Fatalf("全量导入失败: %v", err) } if result.Imported != 1 || result.TokensImported != 1 { t.Fatalf("应只导入 1 个未绑定 Token,实际 %+v", result) } if result.TokensSkippedBound != 2 { t.Fatalf("应跳过 2 个已绑定 Token,实际 %d", result.TokensSkippedBound) } var count int64 db.Model(&model.RockToken{}).Where("upstream_token_id IN ?", []string{"96002", "96003"}).Count(&count) if count != 0 { t.Fatalf("已绑定 Token 不应入库,实际 %d 条", count) } var token model.RockToken if err := db.Where("upstream_token_id = ?", "96001").First(&token).Error; err != nil { t.Fatalf("查询导入 Token 失败: %v", err) } if token.Status != model.RockStatusAvailable || token.UsingAt != nil || token.BoundAt != nil { t.Fatalf("导入结果不符: status=%s usingAt=%v boundAt=%v", token.Status, token.UsingAt, token.BoundAt) } } func TestImportAllUpstreamPackages_EmptyWhenAllBound(t *testing.T) { db := newRockTestDB(t) packages := []map[string]interface{}{ {"id": 310, "token_count": 1, "share_code": "share-310", "status": "active", "created_at": "2026-10-22 10:00:00", "note": "d"}, } details := map[string]map[string]interface{}{ "share-310": { "package": map[string]interface{}{"id": 310, "share_code": "share-310", "token_count": 1, "status": "active"}, "tokens": []map[string]interface{}{ {"id": 97001, "token": "tk-97001", "color": "black", "region": "Q/V可用", "status": "bound", "redeem_code": "rc-97001", "bind_changed_at": "2026-10-30 16:02:19", "bind_change_type": "bind"}, }, }, } server := newImportAllMock(t, packages, details) defer server.Close() svc := NewRockService(db, NewRockClient(server.URL)) setRockTestBearer(t, svc, "bearer-test") result, err := svc.ImportAllUpstreamPackages(context.Background(), 50, 1) if err != nil { t.Fatalf("全量导入失败: %v", err) } if result.Empty != 1 || result.Imported != 0 { t.Fatalf("全部已绑定时应记为 empty,实际 %+v", result) } } func TestImportAllUpstreamPackages_PartialFailure(t *testing.T) { db := newRockTestDB(t) packages := []map[string]interface{}{ {"id": 400, "token_count": 1, "share_code": "share-400", "status": "active", "created_at": "2026-10-23 10:00:00", "note": "e"}, {"id": 401, "token_count": 1, "share_code": "share-401", "status": "active", "created_at": "2026-10-24 10:00:00", "note": "f"}, } details := map[string]map[string]interface{}{ "share-401": { "package": map[string]interface{}{"id": 401, "share_code": "share-401", "token_count": 1, "status": "active"}, "tokens": []map[string]interface{}{ {"id": 98001, "token": "tk-98001", "color": "black", "region": "Q/V可用", "status": "using", "redeem_code": "rc-98001"}, }, }, } server := newImportAllMock(t, packages, details) defer server.Close() svc := NewRockService(db, NewRockClient(server.URL)) setRockTestBearer(t, svc, "bearer-test") result, err := svc.ImportAllUpstreamPackages(context.Background(), 50, 1) if err != nil { t.Fatalf("全量导入调用失败: %v", err) } if result.Failed != 1 || result.Imported != 1 { t.Fatalf("应 1 失败 1 成功,实际 %+v", result) } if len(result.Results) != 2 || result.Results[0].Error == "" { t.Fatalf("失败包应带错误信息: %+v", result.Results) } } func TestImportAllUpstreamPackages_MaxPackages(t *testing.T) { db := newRockTestDB(t) packages := []map[string]interface{}{ {"id": 500, "token_count": 1, "share_code": "share-500", "status": "active", "created_at": "2026-10-25 10:00:00", "note": "g"}, {"id": 501, "token_count": 1, "share_code": "share-501", "status": "active", "created_at": "2026-10-26 10:00:00", "note": "h"}, {"id": 502, "token_count": 1, "share_code": "share-502", "status": "active", "created_at": "2026-10-27 10:00:00", "note": "i"}, } details := map[string]map[string]interface{}{ "share-500": { "package": map[string]interface{}{"id": 500, "share_code": "share-500", "token_count": 1, "status": "active"}, "tokens": []map[string]interface{}{ {"id": 99001, "token": "tk-99001", "color": "black", "region": "Q/V可用", "status": "using", "redeem_code": "rc-99001"}, }, }, } server := newImportAllMock(t, packages, details) defer server.Close() svc := NewRockService(db, NewRockClient(server.URL)) setRockTestBearer(t, svc, "bearer-test") result, err := svc.ImportAllUpstreamPackages(context.Background(), 1, 1) if err != nil { t.Fatalf("全量导入失败: %v", err) } if result.Remaining != 2 { t.Fatalf("上限 1 时应剩余 2 个包,实际 %d", result.Remaining) } if len(result.Results) != 1 { t.Fatalf("应只处理 1 个包,实际 %d", len(result.Results)) } }