hyapp-server/tests/smoke/cp_intimacy_leaderboard_real_flow_test.go
2026-06-11 13:33:44 +08:00

317 lines
11 KiB
Go

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"
}