8862887baa
新增充值订单查询、通用用户详情、钱包冻结与部分退款操作 接入微信V3退款申请、回调、主动查单、交易账单对账与安全诊断 新增退款占用、权益追回、异常欠账和消费限制事务 补齐Native支付SSE自动收口及退款编号与部分退款校验 同步SpacetimeDB schema、生成绑定、测试与项目文档
265 lines
8.2 KiB
TypeScript
265 lines
8.2 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 {
|
||
getAdminDatabaseTableRows,
|
||
getAdminDatabaseTables,
|
||
} from '../api/adminApiClient';
|
||
import {
|
||
AdminDatabaseTablesPage,
|
||
resolveAdminDatabaseUserReference,
|
||
} from './AdminDatabaseTablesPage';
|
||
|
||
vi.mock('../api/adminApiClient', () => ({
|
||
formatAdminApiError: vi.fn((error: unknown) =>
|
||
error instanceof Error ? error.message : '请求失败',
|
||
),
|
||
getAdminDatabaseTableRows: vi.fn(),
|
||
getAdminDatabaseTables: vi.fn(),
|
||
isAdminApiError: vi.fn(() => false),
|
||
}));
|
||
|
||
vi.mock('../components/AdminUserReferenceButton', () => ({
|
||
AdminUserReferenceButton: ({ userId, publicUserCode }: {
|
||
userId?: string;
|
||
publicUserCode?: string;
|
||
}) => (
|
||
<button
|
||
aria-label={`查看用户 ${userId || publicUserCode}`}
|
||
data-public-user-code={publicUserCode}
|
||
data-user-id={userId}
|
||
type="button"
|
||
onClick={(event) => event.stopPropagation()}
|
||
/>
|
||
),
|
||
}));
|
||
|
||
const referralRows = [
|
||
{
|
||
cells: {
|
||
bound_at: '2026-05-02T00:00:00Z',
|
||
invitee_user_id: 'u-b',
|
||
invite_code: 'INV-1001',
|
||
inviter_user_id: 'u-a',
|
||
},
|
||
raw: ['u-b', 'u-a', 'INV-1001', '2026-05-02T00:00:00Z'],
|
||
},
|
||
{
|
||
cells: {
|
||
bound_at: '2026-05-01T00:00:00Z',
|
||
invitee_user_id: 'u-a',
|
||
invite_code: 'INV-1002',
|
||
inviter_user_id: 'u-c',
|
||
},
|
||
raw: ['u-a', 'u-c', 'INV-1002', '2026-05-01T00:00:00Z'],
|
||
},
|
||
{
|
||
cells: {
|
||
bound_at: '2026-05-03T00:00:00Z',
|
||
invitee_user_id: 'u-c',
|
||
invite_code: 'INV-1003',
|
||
inviter_user_id: 'u-a',
|
||
},
|
||
raw: ['u-c', 'u-a', 'INV-1003', '2026-05-03T00:00:00Z'],
|
||
},
|
||
];
|
||
|
||
beforeEach(() => {
|
||
window.location.hash = '#tables?table=profile_referral_relation';
|
||
vi.mocked(getAdminDatabaseTables).mockResolvedValue({
|
||
fetchErrors: [],
|
||
tables: ['profile_referral_relation'],
|
||
});
|
||
vi.mocked(getAdminDatabaseTableRows).mockResolvedValue({
|
||
columns: ['invitee_user_id', 'inviter_user_id', 'invite_code', 'bound_at'],
|
||
limit: 100,
|
||
rows: referralRows,
|
||
page: 1,
|
||
scannedCount: 3,
|
||
scanLimit: 50000,
|
||
scanLimitReached: false,
|
||
tableName: 'profile_referral_relation',
|
||
totalMatched: 3,
|
||
totalReturned: 3,
|
||
});
|
||
});
|
||
|
||
test('后台表查询页通过页面级固定栏翻页并提示扫描结果可能不完整', async () => {
|
||
const user = userEvent.setup();
|
||
vi.mocked(getAdminDatabaseTableRows).mockResolvedValue({
|
||
columns: ['invitee_user_id'],
|
||
limit: 100,
|
||
page: 1,
|
||
rows: [
|
||
{
|
||
cells: { invitee_user_id: 'u-b' },
|
||
raw: ['u-b'],
|
||
},
|
||
],
|
||
scannedCount: 50000,
|
||
scanLimit: 50000,
|
||
scanLimitReached: true,
|
||
tableName: 'profile_referral_relation',
|
||
totalMatched: 250,
|
||
totalReturned: 1,
|
||
});
|
||
|
||
const { container } = render(
|
||
<AdminDatabaseTablesPage token="admin-token" onUnauthorized={vi.fn()} />,
|
||
);
|
||
|
||
expect(await screen.findByText('第 1 / 3 页,共 250 条')).toBeTruthy();
|
||
const pagination = screen.getByRole('navigation', { name: '表查询分页' });
|
||
expect(pagination.classList.contains('admin-database-pagination')).toBe(true);
|
||
expect(pagination.closest('.admin-panel')).toBeNull();
|
||
expect(
|
||
container.querySelector('.admin-database-tables-page')?.lastElementChild,
|
||
).toBe(pagination);
|
||
expect(
|
||
screen.getByText(
|
||
'当前表超过 50000 条扫描与浏览上限,本次已扫描 50000 条;当前分页结果和匹配总数可能不完整。',
|
||
),
|
||
).toBeTruthy();
|
||
|
||
await user.type(screen.getByRole('textbox', { name: '关键词' }), '未执行条件');
|
||
await user.click(screen.getByRole('button', { name: '下一页' }));
|
||
|
||
await waitFor(() => {
|
||
expect(getAdminDatabaseTableRows).toHaveBeenLastCalledWith(
|
||
'admin-token',
|
||
'profile_referral_relation',
|
||
expect.objectContaining({
|
||
filters: '',
|
||
limit: 100,
|
||
page: 2,
|
||
search: '',
|
||
}),
|
||
);
|
||
});
|
||
});
|
||
|
||
test('后台表查询页把表头排序交给后端并从第一页展示排序结果', async () => {
|
||
const user = userEvent.setup();
|
||
const { container } = render(
|
||
<AdminDatabaseTablesPage token="admin-token" onUnauthorized={vi.fn()} />,
|
||
);
|
||
|
||
await screen.findByText('u-b');
|
||
await screen.findByText('2026-05-02 08:00:00');
|
||
|
||
const tableWrap = container.querySelector('.admin-table-wrap');
|
||
expect(tableWrap?.querySelector('.admin-database-table')).not.toBeNull();
|
||
expect(
|
||
screen
|
||
.getByRole('option', { name: '邀请关系(profile_referral_relation)' })
|
||
.getAttribute('title'),
|
||
).toBe('原始表名:profile_referral_relation。邀请关系记录表。');
|
||
expect(
|
||
screen.getByText('已选表:邀请关系(profile_referral_relation)'),
|
||
).toBeTruthy();
|
||
expect(
|
||
screen.getByRole('heading', { name: '邀请关系' }).getAttribute('title'),
|
||
).toBe('原始表名:profile_referral_relation。邀请关系记录表。');
|
||
expect(
|
||
screen.getByRole('button', { name: '被邀请人ID' }).getAttribute('title'),
|
||
).toBe('原始字段名:invitee_user_id。被邀请人的用户标识。点击可按此列排序。');
|
||
expect(readFirstColumnValues(container)).toEqual(['u-b', 'u-a', 'u-c']);
|
||
|
||
vi.mocked(getAdminDatabaseTableRows).mockResolvedValueOnce({
|
||
columns: ['invitee_user_id', 'inviter_user_id', 'invite_code', 'bound_at'],
|
||
limit: 100,
|
||
page: 1,
|
||
rows: [referralRows[0]!, referralRows[2]!, referralRows[1]!],
|
||
scannedCount: 3,
|
||
scanLimit: 50000,
|
||
scanLimitReached: false,
|
||
tableName: 'profile_referral_relation',
|
||
totalMatched: 3,
|
||
totalReturned: 3,
|
||
});
|
||
await user.click(screen.getByRole('button', { name: '邀请人ID' }));
|
||
await waitFor(() => {
|
||
expect(getAdminDatabaseTableRows).toHaveBeenLastCalledWith(
|
||
'admin-token',
|
||
'profile_referral_relation',
|
||
expect.objectContaining({
|
||
page: 1,
|
||
sortColumn: 'inviter_user_id',
|
||
sortDirection: 'asc',
|
||
}),
|
||
);
|
||
expect(readFirstColumnValues(container)).toEqual(['u-b', 'u-c', 'u-a']);
|
||
});
|
||
|
||
vi.mocked(getAdminDatabaseTableRows).mockResolvedValueOnce({
|
||
columns: ['invitee_user_id', 'inviter_user_id', 'invite_code', 'bound_at'],
|
||
limit: 100,
|
||
page: 1,
|
||
rows: [referralRows[1]!, referralRows[0]!, referralRows[2]!],
|
||
scannedCount: 3,
|
||
scanLimit: 50000,
|
||
scanLimitReached: false,
|
||
tableName: 'profile_referral_relation',
|
||
totalMatched: 3,
|
||
totalReturned: 3,
|
||
});
|
||
await user.click(screen.getByRole('button', { name: '邀请人ID' }));
|
||
await waitFor(() => {
|
||
expect(getAdminDatabaseTableRows).toHaveBeenLastCalledWith(
|
||
'admin-token',
|
||
'profile_referral_relation',
|
||
expect.objectContaining({
|
||
page: 1,
|
||
sortColumn: 'inviter_user_id',
|
||
sortDirection: 'desc',
|
||
}),
|
||
);
|
||
expect(readFirstColumnValues(container)).toEqual(['u-a', 'u-b', 'u-c']);
|
||
});
|
||
});
|
||
|
||
test('数据库用户字段显示查看按钮且点击不会打开行详情', async () => {
|
||
const user = userEvent.setup();
|
||
render(
|
||
<AdminDatabaseTablesPage token="admin-token" onUnauthorized={vi.fn()} />,
|
||
);
|
||
|
||
const userButton = await screen.findByRole('button', { name: '查看用户 u-b' });
|
||
await user.click(userButton);
|
||
expect(screen.queryByRole('dialog')).toBeNull();
|
||
});
|
||
|
||
test('数据库用户字段识别会排除后台操作者与合成邀请码字段', () => {
|
||
expect(
|
||
resolveAdminDatabaseUserReference('profile_wallet', 'owner_user_id', 'u-1'),
|
||
).toEqual({ userId: 'u-1' });
|
||
expect(
|
||
resolveAdminDatabaseUserReference(
|
||
'auth_store_projection',
|
||
'public_user_code',
|
||
'TN1001',
|
||
),
|
||
).toEqual({ publicUserCode: 'TN1001' });
|
||
expect(
|
||
resolveAdminDatabaseUserReference('audit_log', 'operator_user_id', 'u-1'),
|
||
).toBeNull();
|
||
expect(
|
||
resolveAdminDatabaseUserReference('audit_log', 'admin_user_id', 'u-1'),
|
||
).toBeNull();
|
||
expect(
|
||
resolveAdminDatabaseUserReference('profile_wallet', 'user_id', 'admin:root'),
|
||
).toBeNull();
|
||
expect(
|
||
resolveAdminDatabaseUserReference('profile_invite_code', 'user_id', 'u-1'),
|
||
).toBeNull();
|
||
});
|
||
|
||
function readFirstColumnValues(container: HTMLElement) {
|
||
return Array.from(container.querySelectorAll('tbody tr')).map(
|
||
(row) => row.querySelector('td')?.textContent?.trim() ?? '',
|
||
);
|
||
}
|