import { afterEach, expect, test, vi } from "vitest"; import { buildOpsUrl, fetchLuckyGiftDraws, fetchOpsDashboard, OPS_API_PREFIX, saveLuckyGiftConfig } from "./api.js"; afterEach(() => { vi.restoreAllMocks(); }); function jsonResponse(data) { return new Response(JSON.stringify({ code: 0, data }), { headers: { "Content-Type": "application/json" }, status: 200 }); } test("builds ops api urls under the dedicated prefix", () => { const url = buildOpsUrl("/apps", { app_code: "lalu", empty: "", status: "active" }); expect(OPS_API_PREFIX).toBe("/api/v1/admin/ops-center"); expect(url).toBe("http://localhost:3000/api/v1/admin/ops-center/apps?app_code=lalu&status=active"); }); test("loads every pool config per app through the plural configs endpoint", async () => { const calls = []; vi.spyOn(window, "fetch").mockImplementation(async (url) => { const text = String(url); calls.push(text); if (text.endsWith("/apps")) { return jsonResponse({ items: [{ app_code: "lalu", status: "active" }] }); } if (text.includes("/lucky-gifts/configs?")) { // lalu 在 default 之外还有 lucky/super_lucky 两个已发布奖池,复数接口必须全部带回。 return jsonResponse([ { app_code: "lalu", enabled: true, pool_id: "lucky", rule_version: 3, target_rtp_percent: 95 }, { app_code: "lalu", enabled: false, pool_id: "super_lucky", rule_version: 2, target_rtp_percent: 92 } ]); } if (text.includes("/lucky-gifts/pools?")) { return jsonResponse({ items: [ { app_code: "lalu", available_balance: 37950017, balance: 38559017, materialized: true, pool_id: "lucky", reserve_floor: 609000 }, { app_code: "lalu", available_balance: 361500, balance: 382500, materialized: false, pool_id: "default", reserve_floor: 21000 } ] }); } if (text.includes("/lucky-gifts/summary?")) { return jsonResponse({ granted_draws: 9, pending_draws: 1, total_draws: 10, total_reward_coins: 900, total_spent_coins: 1000 }); } return jsonResponse({ items: [] }); }); const data = await fetchOpsDashboard(); expect(calls).toEqual([ "http://localhost:3000/api/v1/admin/ops-center/apps", "http://localhost:3000/api/v1/admin/ops-center/lucky-gifts/configs?app_code=lalu", "http://localhost:3000/api/v1/admin/ops-center/lucky-gifts/pools?app_code=lalu", "http://localhost:3000/api/v1/admin/ops-center/lucky-gifts/summary?app_code=lalu" ]); expect(data.luckyGifts).toEqual([ expect.objectContaining({ app_code: "lalu", is_default: false, pool_id: "lucky", rule_version: 3 }), expect.objectContaining({ app_code: "lalu", is_default: false, pool_id: "super_lucky", rule_version: 2 }) ]); expect(data.summary).toMatchObject({ activeApps: 1, configuredPools: 2, enabledPools: 1, poolBalanceTotal: 38941517, totalDraws: 10 }); }); test("falls back to the server default draft when an app has no published config", async () => { const calls = []; vi.spyOn(window, "fetch").mockImplementation(async (url) => { const text = String(url); calls.push(text); if (text.endsWith("/apps")) { return jsonResponse({ items: [{ app_code: "yumi", status: "active" }] }); } if (text.includes("/lucky-gifts/configs?")) { return jsonResponse([]); } if (text.includes("/lucky-gifts/config?")) { return jsonResponse({ app_code: "yumi", enabled: false, pool_id: "default", rule_version: 0, target_rtp_percent: 95 }); } return jsonResponse({ items: [] }); }); const data = await fetchOpsDashboard(); expect(calls).toContain("http://localhost:3000/api/v1/admin/ops-center/lucky-gifts/config?app_code=yumi"); expect(data.luckyGifts).toEqual([ expect.objectContaining({ app_code: "yumi", is_default: true, pool_id: "default", rule_version: 0 }) ]); }); test("fetches draws with numeric ids as user_id and other ids as external_user_id", async () => { const calls = []; vi.spyOn(window, "fetch").mockImplementation(async (url) => { calls.push(String(url)); return jsonResponse({ items: [{ draw_id: "draw-1" }], page: 1, pageSize: 20, total: 1 }); }); const numericResult = await fetchLuckyGiftDraws({ appCode: "lalu", poolId: "lucky", userId: "10001" }); await fetchLuckyGiftDraws({ appCode: "lalu", userId: "ext-abc" }); const numericParams = new URL(calls[0]).searchParams; expect(numericParams.get("app_code")).toBe("lalu"); expect(numericParams.get("pool_id")).toBe("lucky"); expect(numericParams.get("user_id")).toBe("10001"); expect(numericParams.get("external_user_id")).toBeNull(); const externalParams = new URL(calls[1]).searchParams; expect(externalParams.get("external_user_id")).toBe("ext-abc"); expect(externalParams.get("user_id")).toBeNull(); expect(numericResult).toMatchObject({ items: [{ draw_id: "draw-1" }], page: 1, total: 1 }); }); test("saves lucky gift config through admin ops route", async () => { const calls = []; vi.spyOn(window, "fetch").mockImplementation(async (url, init) => { calls.push({ body: init.body, method: init.method, url: String(url) }); return jsonResponse({ app_code: "aslan", pool_id: "default" }); }); await saveLuckyGiftConfig({ app_code: "aslan", pool_id: "default", enabled: true, target_rtp_percent: 95, pool_rate_percent: 96, settlement_window_wager: 1_000_000, control_band_percent: 1, gift_price_reference: 100, stages: [{ stage: "novice", tiers: [{ multiplier: 1, probabilityPercent: 100, enabled: true }] }] }); expect(calls[0].method).toBe("PUT"); expect(calls[0].url).toBe("http://localhost:3000/api/v1/admin/ops-center/lucky-gifts/config?app_code=aslan&pool_id=default"); expect(JSON.parse(calls[0].body)).toMatchObject({ app_code: "aslan", enabled: true, pool_id: "default", stages: [{ stage: "novice", tiers: [{ probability_percent: 100 }] }], target_rtp_percent: 95 }); });