272 lines
8.7 KiB
TypeScript
272 lines
8.7 KiB
TypeScript
/* @vitest-environment jsdom */
|
|
|
|
import {render, screen, waitFor} from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import {beforeEach, expect, test, vi} from 'vitest';
|
|
|
|
import {
|
|
getAdminUserDetail,
|
|
reconcileAdminUserConsumption,
|
|
updateAdminWalletRestriction,
|
|
} from '../api/adminApiClient';
|
|
import type {
|
|
AdminProfileWalletPayload,
|
|
AdminUserDetailResponse,
|
|
} from '../api/adminApiTypes';
|
|
import {AdminUserReferenceButton} from './AdminUserReferenceButton';
|
|
|
|
vi.mock('../api/adminApiClient', () => ({
|
|
formatAdminApiError: vi.fn((error: unknown) =>
|
|
error instanceof Error ? error.message : '请求失败',
|
|
),
|
|
getAdminUserDetail: vi.fn(),
|
|
isAdminApiError: vi.fn(() => false),
|
|
reconcileAdminUserConsumption: vi.fn(),
|
|
updateAdminWalletRestriction: vi.fn(),
|
|
}));
|
|
|
|
const wallet: AdminProfileWalletPayload = {
|
|
userId: 'user-1',
|
|
totalBalance: 96,
|
|
spendableBalance: 40,
|
|
dailyFreePoints: 6,
|
|
membershipLimitedPoints: 20,
|
|
permanentPoints: 70,
|
|
heldPoints: 5,
|
|
refundDebtPoints: 25,
|
|
manualFrozen: false,
|
|
refundDebtFrozen: true,
|
|
walletFrozen: true,
|
|
manualRestriction: null,
|
|
};
|
|
|
|
const detail: AdminUserDetailResponse = {
|
|
userId: 'user-1',
|
|
publicUserCode: 'TN1001',
|
|
displayName: '陶泥用户',
|
|
avatarUrl: 'https://example.com/avatar.png',
|
|
phoneNumberMasked: '138****5678',
|
|
loginMethod: 'phone',
|
|
bindingStatus: 'bound',
|
|
phoneBound: true,
|
|
wechatBound: true,
|
|
historicalConsumedPoints: 1234,
|
|
canReconcileConsumption: true,
|
|
wallet,
|
|
rechargeOrders: [
|
|
{
|
|
orderId: 'order-1',
|
|
userId: 'user-1',
|
|
user: null,
|
|
productId: 'points_60',
|
|
productTitle: '60泥点',
|
|
productKind: 'points',
|
|
amountCents: 600,
|
|
status: 'paid',
|
|
paymentChannel: 'wechat_native',
|
|
paidAtMicros: 1_720_000_000_000_000,
|
|
providerTransactionId: 'wx-1',
|
|
createdAtMicros: 1_720_000_000_000_000,
|
|
pointsDelta: 60,
|
|
cumulativeSuccessRefundCents: 300,
|
|
targetRecoveryPoints: 30,
|
|
recoveredPoints: 5,
|
|
unrecoveredPoints: 25,
|
|
recoveryStatus: 'shortfall',
|
|
wallet,
|
|
refunds: [],
|
|
activeHold: null,
|
|
remainingRefundableCents: 300,
|
|
refundEligible: false,
|
|
refundBlockReasonCode: 'refund_reconciliation_pending',
|
|
},
|
|
],
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.mocked(getAdminUserDetail).mockResolvedValue(detail);
|
|
vi.mocked(reconcileAdminUserConsumption).mockResolvedValue({
|
|
userId: 'user-1',
|
|
previousHistoricalConsumedPoints: 1234,
|
|
historicalConsumedPoints: 1300,
|
|
changed: true,
|
|
reconciledAtMicros: 1_720_000_000_000_000,
|
|
});
|
|
vi.mocked(updateAdminWalletRestriction).mockResolvedValue({wallet});
|
|
});
|
|
|
|
test('用户查看按钮按内部 ID 查询并展示脱敏资料、余额与退款限制', async () => {
|
|
const user = userEvent.setup();
|
|
const parentClick = vi.fn();
|
|
render(
|
|
<div onClick={parentClick}>
|
|
<AdminUserReferenceButton
|
|
token="admin-token"
|
|
userId="user-1"
|
|
onUnauthorized={vi.fn()}
|
|
/>
|
|
</div>,
|
|
);
|
|
|
|
const trigger = screen.getByRole('button', {name: '查看用户信息'});
|
|
await user.click(trigger);
|
|
expect(parentClick).not.toHaveBeenCalled();
|
|
expect(await screen.findByRole('dialog', {name: '用户详情'})).toBeTruthy();
|
|
expect(getAdminUserDetail).toHaveBeenCalledWith('admin-token', {
|
|
userId: 'user-1',
|
|
publicUserCode: undefined,
|
|
});
|
|
expect(screen.getByText('陶泥用户')).toBeTruthy();
|
|
expect(screen.getAllByText('TN1001').length).toBeGreaterThanOrEqual(1);
|
|
expect(screen.getByText('138****5678')).toBeTruthy();
|
|
expect(screen.getByText('退款欠账限制')).toBeTruthy();
|
|
expect(screen.getByText('25', {selector: 'strong'})).toBeTruthy();
|
|
expect(screen.getByText('历史花费')).toBeTruthy();
|
|
expect(screen.getByText('1234', {selector: 'strong'})).toBeTruthy();
|
|
expect(screen.getByText('order-1')).toBeTruthy();
|
|
|
|
await user.keyboard('{Escape}');
|
|
await waitFor(() => expect(screen.queryByRole('dialog', {name: '用户详情'})).toBeNull());
|
|
await waitFor(() => expect(document.activeElement).toBe(trigger));
|
|
});
|
|
|
|
test('只有陶泥号时按 publicUserCode 查询用户', async () => {
|
|
const user = userEvent.setup();
|
|
render(
|
|
<AdminUserReferenceButton
|
|
token="admin-token"
|
|
publicUserCode="TN1001"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
await user.click(screen.getByRole('button', {name: '查看用户信息'}));
|
|
await screen.findByText('陶泥用户');
|
|
expect(getAdminUserDetail).toHaveBeenCalledWith('admin-token', {
|
|
userId: undefined,
|
|
publicUserCode: 'TN1001',
|
|
});
|
|
});
|
|
|
|
test('历史花费支持手动对账并用权威结果校准展示', async () => {
|
|
const user = userEvent.setup();
|
|
render(
|
|
<AdminUserReferenceButton
|
|
token="admin-token"
|
|
userId="user-1"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
await user.click(screen.getByRole('button', {name: '查看用户信息'}));
|
|
await screen.findByText('陶泥用户');
|
|
await user.click(screen.getByRole('button', {name: '手动对账历史花费'}));
|
|
await user.click(screen.getByRole('button', {name: '确认'}));
|
|
|
|
await waitFor(() => {
|
|
expect(reconcileAdminUserConsumption).toHaveBeenCalledWith('admin-token', {
|
|
userId: 'user-1',
|
|
});
|
|
});
|
|
expect(await screen.findByText('对账完成,历史花费已校准')).toBeTruthy();
|
|
expect(screen.getByText('1300', {selector: 'strong'})).toBeTruthy();
|
|
});
|
|
|
|
test('没有独立操作权限时不显示历史花费对账按钮', async () => {
|
|
vi.mocked(getAdminUserDetail).mockResolvedValue({
|
|
...detail,
|
|
canReconcileConsumption: false,
|
|
});
|
|
const user = userEvent.setup();
|
|
render(
|
|
<AdminUserReferenceButton
|
|
token="admin-token"
|
|
userId="user-1"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
await user.click(screen.getByRole('button', {name: '查看用户信息'}));
|
|
await screen.findByText('陶泥用户');
|
|
|
|
expect(screen.queryByRole('button', {name: '手动对账历史花费'})).toBeNull();
|
|
});
|
|
|
|
test('人工冻结和解除人工冻结分别提交原因且不解除退款欠账限制', async () => {
|
|
const user = userEvent.setup();
|
|
const manuallyFrozenWallet: AdminProfileWalletPayload = {
|
|
...wallet,
|
|
manualFrozen: true,
|
|
walletFrozen: true,
|
|
manualRestriction: {
|
|
frozen: true,
|
|
reason: '风险核查',
|
|
createdByAdminUserId: 'admin:root',
|
|
createdByAdminDisplayName: '后台负责人',
|
|
createdAtMicros: 1_720_000_000_000_000,
|
|
updatedByAdminUserId: 'admin:root',
|
|
updatedByAdminDisplayName: '后台负责人',
|
|
updatedAtMicros: 1_720_000_000_000_000,
|
|
},
|
|
};
|
|
vi.mocked(updateAdminWalletRestriction)
|
|
.mockResolvedValueOnce({wallet: manuallyFrozenWallet})
|
|
.mockResolvedValueOnce({wallet: {...wallet, manualFrozen: false}});
|
|
render(
|
|
<AdminUserReferenceButton
|
|
token="admin-token"
|
|
userId="user-1"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
await user.click(screen.getByRole('button', {name: '查看用户信息'}));
|
|
await screen.findByText('陶泥用户');
|
|
|
|
await user.type(screen.getByRole('textbox', {name: '人工冻结操作原因'}), '异常登录');
|
|
await user.click(screen.getByRole('button', {name: '人工冻结钱包'}));
|
|
await user.click(screen.getByRole('button', {name: '确认'}));
|
|
await waitFor(() => {
|
|
expect(updateAdminWalletRestriction).toHaveBeenNthCalledWith(1, 'admin-token', {
|
|
userId: 'user-1',
|
|
frozen: true,
|
|
reason: '异常登录',
|
|
});
|
|
});
|
|
expect(await screen.findByText(/后台负责人/)).toBeTruthy();
|
|
expect(screen.queryByText(/admin:root/)).toBeNull();
|
|
|
|
expect(await screen.findByText('解除人工冻结后,退款欠账限制仍会保留。')).toBeTruthy();
|
|
await user.type(screen.getByRole('textbox', {name: '人工冻结操作原因'}), '核查完成');
|
|
await user.click(screen.getByRole('button', {name: '解除人工冻结'}));
|
|
await user.click(screen.getByRole('button', {name: '确认'}));
|
|
await waitFor(() => {
|
|
expect(updateAdminWalletRestriction).toHaveBeenNthCalledWith(2, 'admin-token', {
|
|
userId: 'user-1',
|
|
frozen: false,
|
|
reason: '核查完成',
|
|
});
|
|
});
|
|
expect(screen.getByText('退款欠账限制')).toBeTruthy();
|
|
});
|
|
|
|
test('用户详情读取失败后可以重试', async () => {
|
|
const user = userEvent.setup();
|
|
vi.mocked(getAdminUserDetail)
|
|
.mockRejectedValueOnce(new Error('读取失败'))
|
|
.mockResolvedValueOnce(detail);
|
|
render(
|
|
<AdminUserReferenceButton
|
|
token="admin-token"
|
|
userId="user-1"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
await user.click(screen.getByRole('button', {name: '查看用户信息'}));
|
|
expect(await screen.findByText('读取失败')).toBeTruthy();
|
|
await user.click(screen.getByRole('button', {name: '重试'}));
|
|
expect(await screen.findByText('陶泥用户')).toBeTruthy();
|
|
expect(getAdminUserDetail).toHaveBeenCalledTimes(2);
|
|
});
|