317 lines
11 KiB
Go
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"
|
|
}
|