283 lines
10 KiB
JavaScript
283 lines
10 KiB
JavaScript
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
|
import { ToastProvider } from "@/shared/ui/ToastProvider.jsx";
|
|
import { FinanceApp } from "./FinanceApp.jsx";
|
|
import {
|
|
approveOperationsWithdrawalApplication,
|
|
fetchFinanceOptions,
|
|
fetchFinanceRechargeApps,
|
|
fetchFinanceRechargeOverview,
|
|
fetchFinanceRechargeSummaries,
|
|
fetchFinanceSession,
|
|
fetchFinanceWithdrawalOptions,
|
|
listFinanceCoinSellerRechargeOrders,
|
|
listFinanceRechargeDetails,
|
|
listFinanceRechargeRegions,
|
|
listFinanceWithdrawalApplications,
|
|
listOperationsWithdrawalApplications,
|
|
rejectOperationsWithdrawalApplication,
|
|
} from "./api.js";
|
|
import { EMPTY_RECHARGE_OVERVIEW, EMPTY_RECHARGE_SUMMARY } from "./format.js";
|
|
|
|
vi.mock("./api.js", () => ({
|
|
approveFinanceWithdrawalApplication: vi.fn(),
|
|
approveOperationsWithdrawalApplication: vi.fn(),
|
|
createFinanceCoinSellerRechargeOrder: vi.fn(),
|
|
exportFinanceRechargeBills: vi.fn(),
|
|
fetchFinanceOptions: vi.fn(),
|
|
fetchFinanceRechargeApps: vi.fn(),
|
|
fetchFinanceRechargeOverview: vi.fn(),
|
|
fetchFinanceRechargeSummaries: vi.fn(),
|
|
fetchFinanceSession: vi.fn(),
|
|
fetchFinanceWithdrawalOptions: vi.fn(),
|
|
grantFinanceCoinSellerRechargeOrder: vi.fn(),
|
|
getFinanceCoinSellerRechargeExchangeRate: vi.fn(),
|
|
getFinanceUSDTAddresses: vi.fn(),
|
|
listFinanceCoinSellerRechargeOrders: vi.fn(),
|
|
listFinanceRechargeDetails: vi.fn(),
|
|
listFinanceRechargeRegions: vi.fn(),
|
|
listFinanceWithdrawalApplications: vi.fn(),
|
|
listOperationsWithdrawalApplications: vi.fn(),
|
|
rejectFinanceWithdrawalApplication: vi.fn(),
|
|
rejectOperationsWithdrawalApplication: vi.fn(),
|
|
refreshFinanceGoogleRechargePaid: vi.fn(),
|
|
replaceFinanceCoinSellerRechargeExchangeRate: vi.fn(),
|
|
upsertFinanceUSDTAddress: vi.fn(),
|
|
quoteFinanceCoinSellerRechargeOrder: vi.fn(),
|
|
verifyFinanceCoinSellerRechargeOrder: vi.fn(),
|
|
verifyFinanceCoinSellerRechargeReceipt: vi.fn()
|
|
}));
|
|
|
|
const defaultSession = {
|
|
canAccessFinance: true,
|
|
canAuditOperationsWithdrawalApplications: false,
|
|
canAuditWithdrawalApplications: false,
|
|
canConfigureCoinSellerRechargeExchangeRate: false,
|
|
canCreateCoinSellerRechargeOrder: false,
|
|
canGrantCoinSellerRechargeOrder: false,
|
|
canUpdateUSDTAddress: false,
|
|
canVerifyCoinSellerRechargeOrder: false,
|
|
canViewAppRechargeDetails: false,
|
|
canViewCoinSellerRechargeOrders: false,
|
|
canViewOperationsWithdrawalApplications: false,
|
|
canViewWithdrawalApplications: true,
|
|
permissions: ["finance-withdrawal:view"],
|
|
user: { account: "admin", name: "admin" }
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.mocked(fetchFinanceOptions).mockResolvedValue({ apps: [{ appCode: "lalu", appName: "lalu" }] });
|
|
vi.mocked(fetchFinanceRechargeApps).mockResolvedValue([{ appCode: "lalu", appName: "Lalu" }]);
|
|
vi.mocked(fetchFinanceRechargeOverview).mockResolvedValue(EMPTY_RECHARGE_OVERVIEW);
|
|
vi.mocked(fetchFinanceRechargeSummaries).mockResolvedValue({ merged: EMPTY_RECHARGE_SUMMARY, perApp: [] });
|
|
vi.mocked(fetchFinanceSession).mockResolvedValue(defaultSession);
|
|
vi.mocked(fetchFinanceWithdrawalOptions).mockResolvedValue({ apps: [{ appCode: "lalu", appName: "lalu" }] });
|
|
vi.mocked(listFinanceCoinSellerRechargeOrders).mockResolvedValue({ items: [], page: 1, pageSize: 50, total: 0 });
|
|
vi.mocked(listFinanceRechargeDetails).mockResolvedValue({ items: [], page: 1, pageSize: 50, total: 0 });
|
|
vi.mocked(listFinanceRechargeRegions).mockResolvedValue([]);
|
|
vi.mocked(listFinanceWithdrawalApplications).mockResolvedValue({ items: [], page: 1, pageSize: 50, total: 0 });
|
|
vi.mocked(listOperationsWithdrawalApplications).mockResolvedValue({ items: [], page: 1, pageSize: 50, total: 0 });
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
vi.clearAllMocks();
|
|
window.history.pushState(null, "", "/");
|
|
});
|
|
|
|
test("preserves the finance entry when the expired session asks the user to log in again", async () => {
|
|
vi.mocked(fetchFinanceSession).mockRejectedValueOnce(new Error("登录已失效"));
|
|
window.history.pushState(null, "", "/finance/?view=rechargeDetails");
|
|
|
|
render(
|
|
<ToastProvider>
|
|
<FinanceApp />
|
|
</ToastProvider>
|
|
);
|
|
|
|
expect(await screen.findByRole("link", { name: "重新登录" })).toHaveAttribute(
|
|
"href",
|
|
"/login?redirect=%2Ffinance%2F%3Fview%3DrechargeDetails"
|
|
);
|
|
});
|
|
|
|
test("defaults recharge finance views to yesterday in China timezone", async () => {
|
|
vi.spyOn(Date, "now").mockReturnValue(Date.UTC(2026, 6, 8, 4));
|
|
vi.mocked(fetchFinanceSession).mockResolvedValue({
|
|
...defaultSession,
|
|
canViewAppRechargeDetails: true,
|
|
permissions: ["finance-recharge-detail:view"]
|
|
});
|
|
|
|
render(
|
|
<ToastProvider>
|
|
<FinanceApp />
|
|
</ToastProvider>
|
|
);
|
|
|
|
const yesterdayButton = await screen.findByRole("button", { name: "昨天" });
|
|
expect(yesterdayButton).toHaveClass("is-on");
|
|
expect(screen.getByRole("button", { name: "今天" })).toBeInTheDocument();
|
|
|
|
await waitFor(() => expect(listFinanceRechargeDetails).toHaveBeenCalled());
|
|
const [query] = vi.mocked(listFinanceRechargeDetails).mock.calls.at(-1);
|
|
expect(query.start_at_ms).toBe(Date.UTC(2026, 6, 6, 16));
|
|
expect(query.end_at_ms).toBe(Date.UTC(2026, 6, 7, 16));
|
|
});
|
|
|
|
test("opens the permitted operations withdrawal deep link and synchronizes menu changes", async () => {
|
|
vi.mocked(fetchFinanceSession).mockResolvedValue({
|
|
...defaultSession,
|
|
canAuditOperationsWithdrawalApplications: true,
|
|
canViewOperationsWithdrawalApplications: true,
|
|
permissions: ["finance-withdrawal:view", "operations-withdrawal:audit"]
|
|
});
|
|
window.history.pushState(null, "", "/finance/?view=withdrawalOperations");
|
|
|
|
render(
|
|
<ToastProvider>
|
|
<FinanceApp />
|
|
</ToastProvider>
|
|
);
|
|
|
|
expect(await screen.findByRole("heading", { name: "用户提现运营审核" })).toBeInTheDocument();
|
|
await waitFor(() => expect(listOperationsWithdrawalApplications).toHaveBeenCalled());
|
|
fireEvent.click(screen.getByRole("button", { name: "用户提现财务审核" }));
|
|
expect(window.location.search).toBe("?view=withdrawalFinance");
|
|
expect(screen.getByRole("heading", { name: "用户提现财务审核" })).toBeInTheDocument();
|
|
});
|
|
|
|
test("falls back from an unauthorized operations deep link to the finance review queue", async () => {
|
|
window.history.pushState(null, "", "/finance/?view=withdrawalOperations");
|
|
|
|
render(
|
|
<ToastProvider>
|
|
<FinanceApp />
|
|
</ToastProvider>
|
|
);
|
|
|
|
expect(await screen.findByRole("heading", { name: "用户提现财务审核" })).toBeInTheDocument();
|
|
expect(window.location.search).toBe("?view=withdrawalFinance");
|
|
expect(screen.queryByRole("button", { name: "用户提现运营审核" })).not.toBeInTheDocument();
|
|
});
|
|
|
|
test("withdrawal app filter uses only the MoneyAccess scope catalog", async () => {
|
|
vi.mocked(fetchFinanceOptions).mockResolvedValue({
|
|
apps: [
|
|
{ appCode: "lalu", appName: "Lalu" },
|
|
{ appCode: "aslan", appName: "Aslan" }
|
|
]
|
|
});
|
|
vi.mocked(fetchFinanceWithdrawalOptions).mockResolvedValue({
|
|
apps: [{ appCode: "lalu", appName: "Lalu" }]
|
|
});
|
|
|
|
render(
|
|
<ToastProvider>
|
|
<FinanceApp />
|
|
</ToastProvider>
|
|
);
|
|
|
|
const appFilter = await screen.findByRole("combobox", { name: "APP" });
|
|
fireEvent.mouseDown(appFilter);
|
|
|
|
expect(await screen.findByRole("option", { name: "全部 APP" })).toBeInTheDocument();
|
|
expect(screen.getByRole("option", { name: "Lalu" })).toBeInTheDocument();
|
|
expect(screen.queryByRole("option", { name: "Aslan" })).not.toBeInTheDocument();
|
|
});
|
|
|
|
test("submits operations review with the withdrawal record app instead of the global app", async () => {
|
|
vi.mocked(fetchFinanceSession).mockResolvedValue({
|
|
...defaultSession,
|
|
canAuditOperationsWithdrawalApplications: true,
|
|
canViewOperationsWithdrawalApplications: true,
|
|
canViewWithdrawalApplications: false,
|
|
permissions: ["operations-withdrawal:audit"]
|
|
});
|
|
vi.mocked(listOperationsWithdrawalApplications).mockResolvedValue({
|
|
items: [
|
|
{
|
|
appCode: "aslan",
|
|
createdAtMs: 1760000000000,
|
|
id: 16,
|
|
operationsStatus: "pending",
|
|
status: "pending",
|
|
userId: "10002",
|
|
withdrawAddress: "TRX-operations",
|
|
withdrawAmount: 20,
|
|
withdrawMethod: "USDT-TRC20"
|
|
}
|
|
],
|
|
page: 1,
|
|
pageSize: 50,
|
|
total: 1
|
|
});
|
|
vi.mocked(approveOperationsWithdrawalApplication).mockResolvedValue({ id: 16 });
|
|
window.history.pushState(null, "", "/finance/?view=withdrawalOperations");
|
|
|
|
render(
|
|
<ToastProvider>
|
|
<FinanceApp />
|
|
</ToastProvider>
|
|
);
|
|
|
|
fireEvent.click(await screen.findByRole("button", { name: "通过" }));
|
|
fireEvent.click(await screen.findByRole("button", { name: "确认" }));
|
|
|
|
await waitFor(() => {
|
|
expect(approveOperationsWithdrawalApplication).toHaveBeenCalledWith(16, "aslan", {});
|
|
});
|
|
});
|
|
|
|
test("retries a claimed operations rejection with its saved remark", async () => {
|
|
vi.mocked(fetchFinanceSession).mockResolvedValue({
|
|
...defaultSession,
|
|
canAuditOperationsWithdrawalApplications: true,
|
|
canViewOperationsWithdrawalApplications: true,
|
|
canViewWithdrawalApplications: false,
|
|
permissions: ["operations-withdrawal:audit"]
|
|
});
|
|
vi.mocked(listOperationsWithdrawalApplications).mockResolvedValue({
|
|
items: [
|
|
{
|
|
appCode: "aslan",
|
|
createdAtMs: 1760000000000,
|
|
id: 17,
|
|
operationsAuditRemark: "命中运营风控",
|
|
operationsStatus: "rejecting",
|
|
status: "pending",
|
|
userId: "10003",
|
|
withdrawAddress: "TRX-rejecting",
|
|
withdrawAmount: 30,
|
|
withdrawMethod: "USDT-TRC20"
|
|
}
|
|
],
|
|
page: 1,
|
|
pageSize: 50,
|
|
total: 1
|
|
});
|
|
vi.mocked(rejectOperationsWithdrawalApplication).mockResolvedValue({ id: 17 });
|
|
window.history.pushState(null, "", "/finance/?view=withdrawalOperations");
|
|
|
|
render(
|
|
<ToastProvider>
|
|
<FinanceApp />
|
|
</ToastProvider>
|
|
);
|
|
|
|
expect(await screen.findByText("拒绝处理中")).toBeInTheDocument();
|
|
expect(screen.queryByRole("button", { name: "通过" })).not.toBeInTheDocument();
|
|
fireEvent.click(screen.getByRole("button", { name: "重试拒绝" }));
|
|
const savedRemark = await screen.findByRole("textbox", { name: "审核备注" });
|
|
expect(savedRemark).toHaveValue("命中运营风控");
|
|
expect(savedRemark).toBeDisabled();
|
|
fireEvent.click(screen.getByRole("button", { name: "确认" }));
|
|
|
|
await waitFor(() => {
|
|
expect(rejectOperationsWithdrawalApplication).toHaveBeenCalledWith(17, "aslan", {
|
|
auditRemark: "命中运营风控"
|
|
});
|
|
});
|
|
expect(approveOperationsWithdrawalApplication).not.toHaveBeenCalled();
|
|
});
|