package smoke_test import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strconv" "strings" "testing" "time" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" userv1 "hyapp.local/api/proto/user/v1" ) func TestCPIntimacyLeaderboardLocalRealFlow(t *testing.T) { if os.Getenv("CP_INTIMACY_REAL_FLOW_TEST") != "1" { t.Skip("set CP_INTIMACY_REAL_FLOW_TEST=1 to run the local CP intimacy leaderboard flow with real users and real data") } ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second) defer cancel() baseURL := strings.TrimRight(envOr("CP_INTIMACY_BASE_URL", "http://127.0.0.1:13000/api/v1"), "/") userServiceAddr := envOr("CP_INTIMACY_USER_SERVICE_ADDR", "127.0.0.1:13005") appCode := envOr("CP_INTIMACY_APP_CODE", "lalu") token := requiredCPIntimacyEnv(t, "CP_INTIMACY_ACCESS_TOKEN") relationType := strings.TrimSpace(os.Getenv("CP_INTIMACY_RELATION_TYPE")) pageSize := int32EnvOr(t, "CP_INTIMACY_PAGE_SIZE", 100) batchSize := int32EnvOr(t, "CP_INTIMACY_BATCH_SIZE", 10000) expectedUserA, expectedUserB := optionalCPIntimacyExpectedPair(t) processed := refreshCPIntimacyLeaderboard(t, ctx, userServiceAddr, appCode, batchSize) if processed <= 0 { t.Fatalf("CP intimacy refresh processed no active relationships; local real data must contain at least one active CP/brother/sister relationship") } data := requestCPIntimacyLeaderboard(t, ctx, &http.Client{Timeout: 15 * time.Second}, baseURL, appCode, token, relationType, pageSize) if data.Total <= 0 || len(data.Items) == 0 { t.Fatalf("gateway returned empty CP intimacy leaderboard after refresh: total=%d items=%d", data.Total, len(data.Items)) } if data.Page != 1 || data.PageSize != pageSize || data.ServerTimeMS <= 0 { t.Fatalf("gateway leaderboard pagination/server time mismatch: %+v", data) } item := data.Items[0] if expectedUserA != "" && expectedUserB != "" { found, ok := findCPIntimacyPair(data.Items, expectedUserA, expectedUserB) if !ok { t.Fatalf("expected real CP pair (%s,%s) not found in first page: total=%d page_size=%d items=%+v", expectedUserA, expectedUserB, data.Total, data.PageSize, data.Items) } item = found } assertCPIntimacyLeaderboardItem(t, item) if shouldRequireCPIntimacyAvatarFrame() { assertCPIntimacyAvatarFrame(t, item.UserA, "user_a") assertCPIntimacyAvatarFrame(t, item.UserB, "user_b") } t.Logf( "cp intimacy real flow passed: base_url=%s user_service=%s app_code=%s relation_type=%q processed=%d total=%d rank=%d relationship_id=%s user_a=%s/%s user_b=%s/%s", baseURL, userServiceAddr, appCode, relationType, processed, data.Total, item.Rank, item.RelationshipID, item.UserA.UserID, item.UserA.DisplayUserID, item.UserB.UserID, item.UserB.DisplayUserID, ) } type cpIntimacyEnvelope struct { Code string `json:"code"` Message string `json:"message"` RequestID string `json:"request_id"` Data json.RawMessage `json:"data"` } type cpIntimacyLeaderboardData struct { Items []cpIntimacyLeaderboardItem `json:"items"` Total int64 `json:"total"` Page int32 `json:"page"` PageSize int32 `json:"page_size"` ServerTimeMS int64 `json:"server_time_ms"` } type cpIntimacyLeaderboardItem struct { Rank int64 `json:"rank"` RelationshipID string `json:"relationship_id"` RelationType string `json:"relation_type"` IntimacyValue int64 `json:"intimacy_value"` Level int32 `json:"level"` UserA cpIntimacyUser `json:"user_a"` UserB cpIntimacyUser `json:"user_b"` FormedAtMS int64 `json:"formed_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` } type cpIntimacyUser struct { UserID string `json:"user_id"` DisplayUserID string `json:"display_user_id"` Username string `json:"username"` Avatar string `json:"avatar"` AvatarFrame *cpIntimacyAvatarFrame `json:"avatar_frame"` } type cpIntimacyAvatarFrame struct { ResourceID int64 `json:"resource_id"` ResourceCode string `json:"resource_code"` Name string `json:"name"` AssetURL string `json:"asset_url"` PreviewURL string `json:"preview_url"` AnimationURL string `json:"animation_url"` MetadataJSON string `json:"metadata_json"` } func refreshCPIntimacyLeaderboard(t *testing.T, ctx context.Context, addr string, appCode string, batchSize int32) int32 { t.Helper() conn, err := grpc.DialContext(ctx, addr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { t.Fatalf("dial local user-service %s failed: %v", addr, err) } defer conn.Close() runID := fmt.Sprintf("cp_intimacy_real_flow_%d", time.Now().UTC().UnixMilli()) resp, err := userv1.NewUserCronServiceClient(conn).RefreshCPIntimacyLeaderboard(ctx, &userv1.CronBatchRequest{ Meta: &userv1.RequestMeta{ RequestId: runID, Caller: "cp-intimacy-real-flow-test", GatewayNodeId: "local-smoke-test", SentAtMs: time.Now().UTC().UnixMilli(), AppCode: appCode, }, RunId: runID, WorkerId: "local-smoke-test", BatchSize: batchSize, LockTtlMs: int64((5 * time.Minute).Milliseconds()), }) if err != nil { t.Fatalf("refresh CP intimacy leaderboard through local user-service failed: %v", err) } if resp.GetFailureCount() != 0 { t.Fatalf("refresh CP intimacy leaderboard reported failures: %+v", resp) } return resp.GetProcessedCount() } func requestCPIntimacyLeaderboard(t *testing.T, ctx context.Context, client *http.Client, baseURL string, appCode string, token string, relationType string, pageSize int32) cpIntimacyLeaderboardData { t.Helper() query := url.Values{} query.Set("page", "1") query.Set("page_size", strconv.FormatInt(int64(pageSize), 10)) if relationType != "" { query.Set("relation_type", relationType) } var data cpIntimacyLeaderboardData doCPIntimacyJSON(t, ctx, client, http.MethodGet, baseURL+"/cp/intimacy-leaderboard?"+query.Encode(), appCode, token, nil, &data) return data } func doCPIntimacyJSON(t *testing.T, ctx context.Context, client *http.Client, method string, url string, appCode string, token string, body any, out any) { t.Helper() var payload io.Reader if body != nil { raw, err := json.Marshal(body) if err != nil { t.Fatalf("marshal %s %s body failed: %v", method, url, err) } payload = bytes.NewReader(raw) } req, err := http.NewRequestWithContext(ctx, method, url, payload) if err != nil { t.Fatalf("build %s %s request failed: %v", method, url, err) } req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("X-App-Code", appCode) req.Header.Set("X-App-Language", "en") if body != nil { req.Header.Set("Content-Type", "application/json") } resp, err := client.Do(req) if err != nil { t.Fatalf("%s %s failed: %v", method, url, err) } defer resp.Body.Close() raw, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("read %s %s response failed: %v", method, url, err) } var envelope cpIntimacyEnvelope if err := json.Unmarshal(raw, &envelope); err != nil { t.Fatalf("decode %s %s envelope failed: status=%d body=%s err=%v", method, url, resp.StatusCode, string(raw), err) } if resp.StatusCode < 200 || resp.StatusCode >= 300 || envelope.Code != "OK" { t.Fatalf("%s %s returned non-OK: status=%d code=%s message=%s request_id=%s body=%s", method, url, resp.StatusCode, envelope.Code, envelope.Message, envelope.RequestID, string(raw)) } if out != nil { if len(envelope.Data) == 0 { t.Fatalf("%s %s returned empty data: request_id=%s", method, url, envelope.RequestID) } if err := json.Unmarshal(envelope.Data, out); err != nil { t.Fatalf("decode %s %s data failed: request_id=%s data=%s err=%v", method, url, envelope.RequestID, string(envelope.Data), err) } } } func assertCPIntimacyLeaderboardItem(t *testing.T, item cpIntimacyLeaderboardItem) { t.Helper() if item.Rank <= 0 || strings.TrimSpace(item.RelationshipID) == "" || strings.TrimSpace(item.RelationType) == "" || item.IntimacyValue < 0 || item.Level <= 0 { t.Fatalf("leaderboard item base fields invalid: %+v", item) } assertCPIntimacyUser(t, item.UserA, "user_a") assertCPIntimacyUser(t, item.UserB, "user_b") if item.UserA.UserID == item.UserB.UserID { t.Fatalf("leaderboard pair must contain two different users: %+v", item) } } func assertCPIntimacyUser(t *testing.T, user cpIntimacyUser, side string) { t.Helper() if strings.TrimSpace(user.UserID) == "" || strings.TrimSpace(user.DisplayUserID) == "" || strings.TrimSpace(user.Username) == "" || strings.TrimSpace(user.Avatar) == "" { t.Fatalf("%s must include real long id, short id, username and avatar: %+v", side, user) } if user.AvatarFrame != nil { assertCPIntimacyAvatarFrame(t, user, side) } } func assertCPIntimacyAvatarFrame(t *testing.T, user cpIntimacyUser, side string) { t.Helper() if user.AvatarFrame == nil || user.AvatarFrame.ResourceID <= 0 || strings.TrimSpace(user.AvatarFrame.ResourceCode) == "" { t.Fatalf("%s must include equipped avatar_frame when CP_INTIMACY_REQUIRE_AVATAR_FRAME=1: %+v", side, user) } } func findCPIntimacyPair(items []cpIntimacyLeaderboardItem, userA string, userB string) (cpIntimacyLeaderboardItem, bool) { for _, item := range items { if sameCPIntimacyPair(item, userA, userB) { return item, true } } return cpIntimacyLeaderboardItem{}, false } func sameCPIntimacyPair(item cpIntimacyLeaderboardItem, userA string, userB string) bool { left := strings.TrimSpace(item.UserA.UserID) right := strings.TrimSpace(item.UserB.UserID) return (left == userA && right == userB) || (left == userB && right == userA) } func optionalCPIntimacyExpectedPair(t *testing.T) (string, string) { t.Helper() userA := strings.TrimSpace(os.Getenv("CP_INTIMACY_EXPECT_USER_A_ID")) userB := strings.TrimSpace(os.Getenv("CP_INTIMACY_EXPECT_USER_B_ID")) if userA == "" && userB == "" { return "", "" } if userA == "" || userB == "" { t.Fatalf("CP_INTIMACY_EXPECT_USER_A_ID and CP_INTIMACY_EXPECT_USER_B_ID must be set together") } if _, err := strconv.ParseInt(userA, 10, 64); err != nil { t.Fatalf("CP_INTIMACY_EXPECT_USER_A_ID must be an int64: %v", err) } if _, err := strconv.ParseInt(userB, 10, 64); err != nil { t.Fatalf("CP_INTIMACY_EXPECT_USER_B_ID must be an int64: %v", err) } return userA, userB } func int32EnvOr(t *testing.T, key string, fallback int32) int32 { t.Helper() raw := strings.TrimSpace(os.Getenv(key)) if raw == "" { return fallback } value, err := strconv.ParseInt(raw, 10, 32) if err != nil || value <= 0 { t.Fatalf("%s must be a positive int32", key) } return int32(value) } func requiredCPIntimacyEnv(t *testing.T, key string) string { t.Helper() value := strings.TrimSpace(os.Getenv(key)) if value == "" { t.Fatalf("%s is required for CP intimacy real flow test", key) } return value } func shouldRequireCPIntimacyAvatarFrame() bool { return strings.TrimSpace(os.Getenv("CP_INTIMACY_REQUIRE_AVATAR_FRAME")) == "1" }