import { afterEach, expect, test, vi } from "vitest"; import { setAccessToken } from "@/shared/api/request"; import { adjustAppUserLevels, banAppUser, createAppUserExport, getAppUser, issueAppUserAccessToken, listAppUsers, listPrettyDisplayIDs, recyclePrettyDisplayID, unbanAppUser, } from "./api"; afterEach(() => { setAccessToken(""); vi.unstubAllGlobals(); }); test("listPrettyDisplayIDs uses admin endpoint and normalizes snake case fields", async () => { vi.stubGlobal( "fetch", vi.fn( async () => new Response( JSON.stringify({ code: 0, data: { items: [ { assigned_user_id: "10001", display_user_id: "VIP2026", pool: { level_track: "wealth", max_level: 10, min_level: 1, name: "财富池", pool_id: "pool-1", rule_type: "aabbcc", status: "active", }, pool_id: "pool-1", pretty_id: "pretty-1", source: "pool", status: "assigned", }, ], page: 1, page_size: 20, total: 1, }, }), ), ), ); const result = await listPrettyDisplayIDs({ keyword: "VIP", page: 1, page_size: 20, status: "assigned" }); const [url, init] = vi.mocked(fetch).mock.calls[0]; expect(String(url)).toContain("/api/v1/admin/users/pretty-ids?"); expect(String(url)).toContain("keyword=VIP"); expect(String(url)).toContain("status=assigned"); expect(init?.method).toBe("GET"); expect(result.pageSize).toBe(20); expect(result.items[0]).toMatchObject({ assignedUserId: "10001", displayUserId: "VIP2026", pool: { levelTrack: "wealth", poolId: "pool-1" }, prettyId: "pretty-1", }); }); test("listPrettyDisplayIDs normalizes assigned user and operator snapshots", async () => { vi.stubGlobal( "fetch", vi.fn( async () => new Response( JSON.stringify({ code: 0, data: { items: [ { assigned_user: { avatar: "https://cdn.example/u.png", default_display_user_id: "10001", display_user_id: "777777", user_id: "9001", username: "pretty owner", }, assigned_user_id: "9001", created_by_admin_id: "7", display_user_id: "777777", operator: { account: "ops", admin_id: "8", name: "运营", type: "admin", }, pretty_id: "pretty-2", source: "admin_grant", status: "assigned", updated_by_admin_id: "8", }, ], page: 1, page_size: 20, total: 1, }, }), ), ), ); const result = await listPrettyDisplayIDs({ page: 1, page_size: 20 }); expect(result.items[0]).toMatchObject({ assignedUser: { avatar: "https://cdn.example/u.png", defaultDisplayUserId: "10001", displayUserId: "777777", userId: "9001", username: "pretty owner", }, operator: { account: "ops", adminId: "8", name: "运营", type: "admin", }, updatedByAdminId: "8", }); }); test("recyclePrettyDisplayID uses generated recycle endpoint", async () => { vi.stubGlobal( "fetch", vi.fn( async () => new Response( JSON.stringify({ code: 0, data: { assigned_user_id: "", display_user_id: "777777", pretty_id: "pretty-2", released_at_ms: 1782493200000, source: "admin_grant", status: "released", }, }), ), ), ); const result = await recyclePrettyDisplayID("pretty-2"); const [url, init] = vi.mocked(fetch).mock.calls[0]; expect(String(url)).toContain("/api/v1/admin/users/pretty-ids/pretty-2/recycle"); expect(init?.method).toBe("POST"); expect(result).toMatchObject({ assignedUserId: "", displayUserId: "777777", prettyId: "pretty-2", status: "released", }); }); test("listPrettyDisplayIDs normalizes zero assigned user as empty", async () => { vi.stubGlobal( "fetch", vi.fn( async () => new Response( JSON.stringify({ code: 0, data: { items: [ { assigned_user_id: "0", display_user_id: "00002", pretty_id: "pretty-0", source: "pool", status: "available", }, ], page: 1, page_size: 20, total: 1, }, }), ), ), ); const result = await listPrettyDisplayIDs({ page: 1, page_size: 20 }); expect(result.items[0]).toMatchObject({ assignedUser: undefined, assignedUserId: "", displayUserId: "00002", }); }); test("listAppUsers sends country region and time filters", async () => { vi.stubGlobal( "fetch", vi.fn( async () => new Response(JSON.stringify({ code: 0, data: { items: [], page: 1, pageSize: 50, total: 0 } })), ), ); const result = await listAppUsers({ country: "PH", end_ms: 2000, page: 1, page_size: 50, region_id: 3, start_ms: 1000, }); const [url, init] = vi.mocked(fetch).mock.calls[0]; expect(result.items).toEqual([]); expect(String(url)).toContain("/api/v1/app/users?"); expect(String(url)).toContain("country=PH"); expect(String(url)).toContain("region_id=3"); expect(String(url)).toContain("start_ms=1000"); expect(String(url)).toContain("end_ms=2000"); expect(init?.method).toBe("GET"); }); test("listAppUsers normalizes pretty display id fields from snake case", async () => { vi.stubGlobal( "fetch", vi.fn( async () => new Response( JSON.stringify({ code: 0, data: { items: [ { avatar: "https://cdn.example/avatar.png", coin: 99, default_display_user_id: "123456", display_user_id: "123456", pretty_display_user_id: "VIP2026", pretty_id: "pretty-2026", user_id: "10001", username: "tester", vip: { active: true, expires_at_ms: 1790000000000, level: 3, name: "黄金会员", }, }, ], page: 1, page_size: 50, total: 1, }, }), ), ), ); const result = await listAppUsers({ page: 1, page_size: 50 }); expect(result.pageSize).toBe(50); expect(result.items[0]).toMatchObject({ defaultDisplayUserId: "123456", displayUserId: "123456", prettyDisplayUserId: "VIP2026", prettyId: "pretty-2026", userId: "10001", username: "tester", vip: expect.objectContaining({ active: true, level: 3, name: "黄金会员", }), }); }); test("getAppUser uses detail endpoint and normalizes assets", async () => { vi.stubGlobal( "fetch", vi.fn( async () => new Response( JSON.stringify({ code: 0, data: { display_user_id: "123456", default_display_user_id: "123456", pretty_display_user_id: "VIP888", pretty_id: "pretty-888", user_id: "10001", balances: [ { asset_type: "COIN", available_amount: 1200, frozen_amount: 50, total_amount: 1250, version: 7, }, ], equipped_resources: [ { entitlement_id: "ent-1", equipped: true, name: "金色头像框", quantity: 1, remaining_quantity: 1, resource_code: "frame_gold", resource_id: 9, resource_type: "avatar_frame", }, ], resources: [ { entitlement_id: "ent-1", equipped: true, name: "金色头像框", quantity: 1, remaining_quantity: 1, resource_code: "frame_gold", resource_id: 9, resource_type: "avatar_frame", }, ], }, }), ), ), ); const result = await getAppUser("10001"); const [url, init] = vi.mocked(fetch).mock.calls[0]; expect(String(url)).toContain("/api/v1/app/users/10001"); expect(init?.method).toBe("GET"); expect(result.defaultDisplayUserId).toBe("123456"); expect(result.prettyDisplayUserId).toBe("VIP888"); expect(result.prettyId).toBe("pretty-888"); expect(result.balances?.[0]).toMatchObject({ assetType: "COIN", availableAmount: 1200, frozenAmount: 50, totalAmount: 1250, version: 7, }); expect(result.equippedResources?.[0]).toMatchObject({ equipped: true, name: "金色头像框", resourceType: "avatar_frame", }); expect(result.resources?.[0]).toMatchObject({ entitlementId: "ent-1", resourceCode: "frame_gold", }); }); test("app user projection normalizes levels roles ban and successful login fields", async () => { vi.stubGlobal( "fetch", vi.fn( async () => new Response( JSON.stringify({ code: 0, data: { age: 26, app_version: "3.2.1", app_version_at_ms: 1779990000000, ban: { active: true, expires_at_ms: 1790000000000, id: "ban-1", permanent: false, reason: "risk", source: "admin", }, birth: "2000-02-29", build_number: "345", cumulative_recharge_usd_minor: 12345, devices: [ { app_version: "3.2.1", build_number: "345", device_id: "android_abcdef", device_model: "Redmi Note 8 Pro", first_seen_at_ms: 1770000000000, imei: "", last_seen_at_ms: 1781500000000, manufacturer: "Xiaomi", os_version: "13", platform: "android", }, ], last_login_at_ms: 1780000000000, last_login_ip: "203.0.113.7", last_login_ip_at_ms: 1781600000000, last_login_ip_country_code: "AE", last_operated_at_ms: 1781000000000, last_operator: { action: "ban", admin_id: "7", name: "运营" }, levels: { wealth: { display_level: 20, expires_at_ms: 1790000000000, real_level: 8, temporary_level_id: "temporary-1", temporary_target_level: 20, track: "wealth", }, }, register_device: "iPhone17,2", register_device_id: "ios_1F2E3D", register_os_version: "17.5", roles: ["主播", "BD"], third_party_identities: [ { created_at_ms: 1760000000000, openid: "g-openid-1", provider: "google", union_id: "g-union-1", }, ], user_id: "10001", }, }), ), ), ); const result = await getAppUser("10001"); expect(result).toMatchObject({ age: 26, appVersion: "3.2.1", appVersionAtMs: 1779990000000, ban: { active: true, expiresAtMs: 1790000000000, id: "ban-1", permanent: false }, birth: "2000-02-29", buildNumber: "345", cumulativeRechargeUsdMinor: 12345, devices: [ { appVersion: "3.2.1", buildNumber: "345", deviceId: "android_abcdef", deviceModel: "Redmi Note 8 Pro", firstSeenAtMs: 1770000000000, imei: "", lastSeenAtMs: 1781500000000, manufacturer: "Xiaomi", osVersion: "13", platform: "android", }, ], lastLoginAtMs: 1780000000000, lastLoginIp: "203.0.113.7", lastLoginIpAtMs: 1781600000000, lastLoginIpCountryCode: "AE", lastOperatedAtMs: 1781000000000, lastOperator: { action: "ban", adminId: "7", name: "运营" }, levels: { wealth: expect.objectContaining({ displayLevel: 20, realLevel: 8, temporaryLevelId: "temporary-1", temporaryTargetLevel: 20, }), }, registerDevice: "iPhone17,2", registerDeviceId: "ios_1F2E3D", registerOsVersion: "17.5", roles: ["主播", "BD"], thirdPartyIdentities: [ { createdAtMs: 1760000000000, openid: "g-openid-1", provider: "google", unionId: "g-union-1" }, ], }); }); test("issueAppUserAccessToken posts to access-token endpoint and normalizes the token payload", async () => { vi.stubGlobal( "fetch", vi.fn( async () => new Response( JSON.stringify({ code: 0, data: { access_token: "eyJhbGciOiJIUzI1NiJ9.payload.signature", device_id: "android_abcdef", expires_in_sec: 7200, session_id: "session-1", session_last_heartbeat_at_ms: 1781700000000, token_type: "Bearer", }, }), ), ), ); const result = await issueAppUserAccessToken("10001"); const [url, init] = vi.mocked(fetch).mock.calls[0]; expect(String(url)).toContain("/api/v1/app/users/10001/access-token"); expect(init?.method).toBe("POST"); expect(result).toEqual({ accessToken: "eyJhbGciOiJIUzI1NiJ9.payload.signature", deviceId: "android_abcdef", expiresInSec: 7200, sessionId: "session-1", sessionLastHeartbeatAtMs: 1781700000000, tokenType: "Bearer", }); }); test("temporary level adjustment sends wealth and charm in one request", async () => { vi.stubGlobal( "fetch", vi.fn(async () => new Response(JSON.stringify({ code: 0, data: { user_id: "10001" } }))), ); const payload = { adjustments: [ { duration_days: 7, level: 20, track: "wealth" as const }, { duration_days: 3, level: 18, track: "charm" as const }, ], reason: "campaign", }; const result = await adjustAppUserLevels("10001", payload); const [url, init] = vi.mocked(fetch).mock.calls[0]; expect(String(url)).toContain("/api/v1/app/users/10001/levels"); expect(init?.method).toBe("PUT"); expect(JSON.parse(String(init?.body))).toEqual(payload); expect(result.userId).toBe("10001"); }); test("ban and unban normalize the status mutation user wrapper", async () => { const fetchMock = vi .fn() .mockResolvedValueOnce( new Response( JSON.stringify({ code: 0, data: { user: { ban: { active: true, expires_at_ms: 1790000000000, permanent: false }, status: "banned", user_id: "10001", }, }, }), ), ) .mockResolvedValueOnce( new Response( JSON.stringify({ code: 0, data: { user: { ban: { active: false }, status: "active", user_id: "10001" } } }), ), ); vi.stubGlobal("fetch", fetchMock); const banned = await banAppUser("10001", { expires_at_ms: 1790000000000, reason: "risk" }); const unbanned = await unbanAppUser("10001", { reason: "cleared" }); expect(banned).toMatchObject({ ban: { active: true, expiresAtMs: 1790000000000 }, status: "banned" }); expect(unbanned).toMatchObject({ ban: { active: false }, status: "active" }); expect(JSON.parse(String(fetchMock.mock.calls[0][1]?.body))).toEqual({ expires_at_ms: 1790000000000, reason: "risk", }); expect(JSON.parse(String(fetchMock.mock.calls[1][1]?.body))).toEqual({ reason: "cleared" }); }); test("app user export creates a task from the current filter snapshot", async () => { vi.stubGlobal( "fetch", vi.fn( async () => new Response( JSON.stringify({ code: 0, data: { jobId: 77, snapshotAtMs: 1781000000000, status: "pending" } }), ), ), ); const result = await createAppUserExport({ country: "AE", status: "banned" }); const [url, init] = vi.mocked(fetch).mock.calls[0]; expect(String(url)).toContain("/api/v1/exports/app-users?"); expect(String(url)).toContain("country=AE"); expect(String(url)).toContain("status=banned"); expect(init?.method).toBe("POST"); expect(result).toEqual({ jobId: 77, snapshotAtMs: 1781000000000, status: "pending" }); });