package service import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "affiliate_dash/internal/model" ) func TestRefreshTokenBinding_BoundMatchesExpectedUID(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") if r.URL.Path != "/api/redeem/CODE-BOUND/refresh" { w.WriteHeader(http.StatusNotFound) return } _ = json.NewEncoder(w).Encode(map[string]interface{}{ "token": map[string]interface{}{ "status": "bound", "bound_account": "痛写心扉", "bound_account_id": "3132710334", "region": "安卓-微信", }, "binding": map[string]interface{}{ "bound": true, "role": "痛写心扉", "roleId": "3132710334", "region": "安卓-微信", }, "message": "已刷新绑定信息", }) })) defer server.Close() svc := NewRockService(db, NewRockClient(server.URL)) setRockTestBearer(t, svc, "bearer-test") now := time.Now() token := model.RockToken{ UpstreamTokenID: "9001", Token: "jwt-9001", RedeemCode: "CODE-BOUND", Color: model.RockColorBlack, Status: model.RockStatusUsing, AssignedOrderNo: "FO-REFRESH-1", ExpectedUID: "3132710334", ClaimedAt: &now, } if err := db.Create(&token).Error; err != nil { t.Fatalf("创建 Token 失败: %v", err) } res, err := svc.RefreshTokenBinding(context.Background(), token.ID) if err != nil { t.Fatalf("刷新绑定状态失败: %v", err) } if res.Status != model.RockStatusBound || !res.Bound { t.Fatalf("期望 bound,实际 status=%s bound=%v", res.Status, res.Bound) } if res.RoleID != "3132710334" || res.RoleName != "痛写心扉" { t.Fatalf("角色信息未同步: %+v", res) } 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.ActualRoleID != "3132710334" || saved.ActualRoleName != "痛写心扉" { t.Fatalf("数据库未更新: %+v", saved) } if saved.BoundAt == nil { t.Fatalf("bound_at 未写入") } } func TestRefreshTokenBinding_MarksMismatch(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", "region": "安卓-微信", }, }) })) defer server.Close() svc := NewRockService(db, NewRockClient(server.URL)) setRockTestBearer(t, svc, "bearer-test") now := time.Now() token := model.RockToken{ UpstreamTokenID: "9002", RedeemCode: "CODE-MISMATCH", Color: model.RockColorBlack, Status: model.RockStatusUsing, AssignedOrderNo: "FO-REFRESH-2", ExpectedUID: "1111111111", ClaimedAt: &now, } if err := db.Create(&token).Error; err != nil { t.Fatalf("创建 Token 失败: %v", err) } res, err := svc.RefreshTokenBinding(context.Background(), token.ID) if err != nil { t.Fatalf("刷新绑定状态失败: %v", err) } if res.Status != model.RockStatusMismatch || !strings.Contains(res.Message, "不一致") { t.Fatalf("期望 mismatch 提示,实际 %+v", res) } var saved model.RockToken if err := db.First(&saved, token.ID).Error; err != nil { t.Fatalf("读取 Token 失败: %v", err) } if saved.Status != model.RockStatusMismatch || saved.MismatchAt == nil || !strings.Contains(saved.MismatchReason, "3132710334") { t.Fatalf("数据库 mismatch 字段不符: %+v", saved) } } func TestRefreshTokenBinding_EmptyExpectedUIDKeepsStatus(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{}{ "binding": map[string]interface{}{ "bound": true, "role": "痛写心扉", "roleId": "3132710334", "region": "安卓-微信", }, }) })) defer server.Close() svc := NewRockService(db, NewRockClient(server.URL)) setRockTestBearer(t, svc, "bearer-test") token := model.RockToken{ UpstreamTokenID: "9003", RedeemCode: "CODE-NOUID", Color: model.RockColorBlack, Status: model.RockStatusUsing, } if err := db.Create(&token).Error; err != nil { t.Fatalf("创建 Token 失败: %v", err) } res, err := svc.RefreshTokenBinding(context.Background(), token.ID) if err != nil { t.Fatalf("刷新绑定状态失败: %v", err) } if res.Status != model.RockStatusUsing { t.Fatalf("缺少预期 UID 时状态应保持 using,实际 %s", res.Status) } if !strings.Contains(res.Message, "人工核对") { t.Fatalf("应提示人工核对,实际 %q", res.Message) } if !res.Warning { t.Fatalf("缺少预期 UID 时应返回 warning=true") } var saved model.RockToken if err := db.First(&saved, token.ID).Error; err != nil { t.Fatalf("读取 Token 失败: %v", err) } if saved.Status != model.RockStatusUsing || saved.ActualRoleID != "3132710334" { t.Fatalf("只应同步角色信息,实际 %+v", saved) } } func TestRefreshTokenBinding_SyncsRedeemed(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": "redeemed", "bound_account": "痛写心扉", "bound_account_id": "3132710334", "redeemed_at": "2026-10-29 12:00:00", }, "deliveryList": []map[string]interface{}{ {"fulfillId": 4519960, "time": "2026-10-29T12:00:00.1", "role": "安卓-微信-痛写心扉"}, }, }) })) defer server.Close() svc := NewRockService(db, NewRockClient(server.URL)) setRockTestBearer(t, svc, "bearer-test") token := model.RockToken{ UpstreamTokenID: "9004", RedeemCode: "CODE-REDEEMED", Color: model.RockColorBlack, Status: model.RockStatusUsing, AssignedOrderNo: "FO-REFRESH-4", ExpectedUID: "3132710334", } if err := db.Create(&token).Error; err != nil { t.Fatalf("创建 Token 失败: %v", err) } res, err := svc.RefreshTokenBinding(context.Background(), token.ID) if err != nil { t.Fatalf("刷新绑定状态失败: %v", err) } if res.Status != model.RockStatusRedeemed { t.Fatalf("期望 redeemed,实际 %s", res.Status) } var saved model.RockToken if err := db.First(&saved, token.ID).Error; err != nil { t.Fatalf("读取 Token 失败: %v", err) } if saved.Status != model.RockStatusRedeemed || saved.RedeemedAt == nil || saved.UpstreamFulfillID != 4519960 { t.Fatalf("已发货字段未同步: %+v", saved) } } func TestRefreshTokenBinding_UpstreamErrorKeepsState(t *testing.T) { db := newRockTestDB(t) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(`{"error":"refresh down"}`)) })) defer server.Close() svc := NewRockService(db, NewRockClient(server.URL)) setRockTestBearer(t, svc, "bearer-test") token := model.RockToken{ UpstreamTokenID: "9005", RedeemCode: "CODE-ERROR", Color: model.RockColorBlack, Status: model.RockStatusUsing, } if err := db.Create(&token).Error; err != nil { t.Fatalf("创建 Token 失败: %v", err) } if _, err := svc.RefreshTokenBinding(context.Background(), token.ID); err == nil { t.Fatalf("上游 500 时应返回错误") } var saved model.RockToken if err := db.First(&saved, token.ID).Error; err != nil { t.Fatalf("读取 Token 失败: %v", err) } if saved.Status != model.RockStatusUsing || saved.ActualRoleID != "" { t.Fatalf("失败时不应改动本地状态: %+v", saved) } } func TestRefreshTokenBinding_FallsBackToAdminRefresh(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") switch r.URL.Path { case "/api/redeem/CODE-FALLBACK/refresh": w.WriteHeader(http.StatusNotFound) case "/api/user/tokens/9006/refresh": _ = json.NewEncoder(w).Encode(map[string]interface{}{ "token": map[string]interface{}{ "status": "bound", "bound_account": "痛写心扉", "bound_account_id": "3132710334", }, }) default: w.WriteHeader(http.StatusNotFound) } })) defer server.Close() svc := NewRockService(db, NewRockClient(server.URL)) setRockTestBearer(t, svc, "bearer-test") now := time.Now() token := model.RockToken{ UpstreamTokenID: "9006", RedeemCode: "CODE-FALLBACK", Color: model.RockColorBlack, Status: model.RockStatusUsing, AssignedOrderNo: "FO-REFRESH-6", ExpectedUID: "3132710334", ClaimedAt: &now, } if err := db.Create(&token).Error; err != nil { t.Fatalf("创建 Token 失败: %v", err) } res, err := svc.RefreshTokenBinding(context.Background(), token.ID) if err != nil { t.Fatalf("降级刷新失败: %v", err) } if res.Status != model.RockStatusBound { t.Fatalf("降级后期望 bound,实际 %s", res.Status) } } func TestRefreshTokenBinding_NotBoundKeepsRoleFields(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": "using", "region": "Q/V可用", }, "binding": map[string]interface{}{"bound": false}, }) })) defer server.Close() svc := NewRockService(db, NewRockClient(server.URL)) setRockTestBearer(t, svc, "bearer-test") token := model.RockToken{ UpstreamTokenID: "9008", RedeemCode: "CODE-UNBOUND", Color: model.RockColorBlack, Status: model.RockStatusUsing, ActualRoleName: "旧角色", ActualRoleID: "123456", ActualChannel: "安卓-微信", } if err := db.Create(&token).Error; err != nil { t.Fatalf("创建 Token 失败: %v", err) } res, err := svc.RefreshTokenBinding(context.Background(), token.ID) if err != nil { t.Fatalf("刷新绑定状态失败: %v", err) } if res.Status != model.RockStatusUsing || res.Bound { t.Fatalf("上游未绑定时应保持 using 且 bound=false,实际 %+v", res) } var saved model.RockToken if err := db.First(&saved, token.ID).Error; err != nil { t.Fatalf("读取 Token 失败: %v", err) } if saved.ActualRoleName != "旧角色" || saved.ActualRoleID != "123456" || saved.ActualChannel != "安卓-微信" { t.Fatalf("上游未绑定时不应覆盖本地角色字段: %+v", saved) } } func TestRefreshTokenBinding_RejectsAvailableToken(t *testing.T) { db := newRockTestDB(t) svc := NewRockService(db, NewRockClient("https://example.com")) token := model.RockToken{ UpstreamTokenID: "9007", RedeemCode: "CODE-AVAILABLE", Color: model.RockColorBlack, Status: model.RockStatusAvailable, } if err := db.Create(&token).Error; err != nil { t.Fatalf("创建 Token 失败: %v", err) } if _, err := svc.RefreshTokenBinding(context.Background(), token.ID); err == nil || !strings.Contains(err.Error(), "仅扫码使用中") { t.Fatalf("available 状态应被拒绝,实际 err=%v", err) } }