2026-07-10 14:31:56 +08:00

147 lines
5.9 KiB
JavaScript

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
});
});