367 lines
11 KiB
TypeScript
367 lines
11 KiB
TypeScript
import { afterEach, expect, test, vi } from 'vitest';
|
|
|
|
import {
|
|
createAdminAccount,
|
|
executeAdminRechargeRefund,
|
|
getAdminFeatureGateConfig,
|
|
getAdminUserDetail,
|
|
listAdminRechargeOrders,
|
|
reconcileAdminUserConsumption,
|
|
resolveAdminRechargeRefundManualReview,
|
|
updateAdminAccount,
|
|
uploadAdminEditorShowcaseCampaignImage,
|
|
upsertAdminFeatureGateConfig,
|
|
upsertProfileWalletConfig,
|
|
} from './adminApiClient';
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
test('后台账号创建和更新同时携带 Tab 与独立操作权限', async () => {
|
|
const fetchMock = vi.fn().mockImplementation(() =>
|
|
Promise.resolve(
|
|
new Response(JSON.stringify({ account: { accountId: 'member-1' } }), {
|
|
status: 200,
|
|
}),
|
|
),
|
|
);
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
await createAdminAccount('owner-token', {
|
|
username: 'operator',
|
|
displayName: '运营',
|
|
password: 'secret123',
|
|
tabPermissions: ['dashboard', 'tracking'],
|
|
actionPermissions: ['profile-wallet-consumption-reconcile'],
|
|
enabled: true,
|
|
});
|
|
await updateAdminAccount('owner-token', 'member/1', {
|
|
displayName: '运营二组',
|
|
tabPermissions: ['tracking'],
|
|
actionPermissions: [],
|
|
enabled: false,
|
|
});
|
|
|
|
expect(fetchMock.mock.calls[0]?.[0]).toBe('/admin/api/accounts');
|
|
expect(fetchMock.mock.calls[0]?.[1]).toEqual(
|
|
expect.objectContaining({
|
|
method: 'POST',
|
|
headers: expect.objectContaining({ Authorization: 'Bearer owner-token' }),
|
|
body: JSON.stringify({
|
|
username: 'operator',
|
|
displayName: '运营',
|
|
password: 'secret123',
|
|
tabPermissions: ['dashboard', 'tracking'],
|
|
actionPermissions: ['profile-wallet-consumption-reconcile'],
|
|
enabled: true,
|
|
}),
|
|
}),
|
|
);
|
|
expect(fetchMock.mock.calls[1]?.[0]).toBe('/admin/api/accounts/member%2F1');
|
|
expect(fetchMock.mock.calls[1]?.[1]).toEqual(
|
|
expect.objectContaining({
|
|
method: 'PUT',
|
|
body: JSON.stringify({
|
|
displayName: '运营二组',
|
|
tabPermissions: ['tracking'],
|
|
actionPermissions: [],
|
|
enabled: false,
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('账号配置一次提交初始和每日免费泥点', async () => {
|
|
const fetchMock = vi.fn().mockResolvedValue(
|
|
new Response(JSON.stringify({configId: 'profile_wallet'}), {
|
|
status: 200,
|
|
headers: {'content-type': 'application/json'},
|
|
}),
|
|
);
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
await upsertProfileWalletConfig('owner-token', {
|
|
initialMudPoints: 100,
|
|
dailyFreePointsPerDay: 35,
|
|
});
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
'/admin/api/profile/wallet-config',
|
|
expect.objectContaining({
|
|
method: 'POST',
|
|
headers: expect.objectContaining({Authorization: 'Bearer owner-token'}),
|
|
body: JSON.stringify({
|
|
initialMudPoints: 100,
|
|
dailyFreePointsPerDay: 35,
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('灰度配置读写只使用通用 feature-gates 管理接口', async () => {
|
|
const fetchMock = vi.fn().mockImplementation(() =>
|
|
Promise.resolve(
|
|
new Response(JSON.stringify({ gates: [] }), {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
}),
|
|
),
|
|
);
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
await getAdminFeatureGateConfig('gray-token');
|
|
await upsertAdminFeatureGateConfig('gray-token', {
|
|
gateKey: 'image-editor:agent-sidebar',
|
|
enabled: true,
|
|
rolloutPercent: 25,
|
|
allowUserIds: ['user-1'],
|
|
allowUserTags: ['beta'],
|
|
denyUserIds: ['blocked-1'],
|
|
description: '画布 Agent 入口灰度',
|
|
});
|
|
|
|
expect(fetchMock.mock.calls[0]?.[0]).toBe('/admin/api/feature-gates');
|
|
expect(fetchMock.mock.calls[0]?.[1]).toEqual(
|
|
expect.objectContaining({
|
|
headers: expect.objectContaining({ Authorization: 'Bearer gray-token' }),
|
|
}),
|
|
);
|
|
expect(fetchMock.mock.calls[1]?.[0]).toBe('/admin/api/feature-gates');
|
|
expect(fetchMock.mock.calls[1]?.[1]).toEqual(
|
|
expect.objectContaining({
|
|
method: 'PUT',
|
|
headers: expect.objectContaining({ Authorization: 'Bearer gray-token' }),
|
|
body: JSON.stringify({
|
|
gateKey: 'image-editor:agent-sidebar',
|
|
enabled: true,
|
|
rolloutPercent: 25,
|
|
allowUserIds: ['user-1'],
|
|
allowUserTags: ['beta'],
|
|
denyUserIds: ['blocked-1'],
|
|
description: '画布 Agent 入口灰度',
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('活动卡图片上传成功后先确认正式私有对象再返回图片引用', async () => {
|
|
const closeBitmap = vi.fn();
|
|
vi.stubGlobal(
|
|
'createImageBitmap',
|
|
vi
|
|
.fn()
|
|
.mockResolvedValue({ width: 1024, height: 1536, close: closeBitmap }),
|
|
);
|
|
const fetchMock = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(
|
|
new Response(
|
|
JSON.stringify({
|
|
upload: {
|
|
bucket: 'genarrative-release',
|
|
host: 'https://genarrative-release.oss.example.com',
|
|
objectKey:
|
|
'generated-character-drafts/editor/showcase-campaign/current/card.png',
|
|
legacyPublicPath:
|
|
'/generated-character-drafts/editor/showcase-campaign/current/card.png',
|
|
contentType: 'image/png',
|
|
formFields: { key: 'campaign-key', policy: 'signed-policy' },
|
|
},
|
|
}),
|
|
{ status: 200, headers: { 'content-type': 'application/json' } },
|
|
),
|
|
)
|
|
.mockResolvedValueOnce(new Response('', { status: 200 }))
|
|
.mockResolvedValueOnce(
|
|
new Response(
|
|
JSON.stringify({ assetObject: { assetObjectId: 'assetobj-1' } }),
|
|
{
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
},
|
|
),
|
|
);
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
const file = new File(['image-bytes'], 'card.png', { type: 'image/png' });
|
|
|
|
const uploaded = await uploadAdminEditorShowcaseCampaignImage(
|
|
'admin-token',
|
|
file,
|
|
);
|
|
|
|
expect(fetchMock.mock.calls[0]?.[0]).toBe(
|
|
'/admin/api/editor-showcase/campaign/image-upload-ticket',
|
|
);
|
|
expect(fetchMock.mock.calls[1]?.[0]).toBe(
|
|
'https://genarrative-release.oss.example.com',
|
|
);
|
|
expect(fetchMock.mock.calls[2]?.[0]).toBe(
|
|
'/admin/api/editor-showcase/campaign/image-upload-confirm',
|
|
);
|
|
expect(fetchMock.mock.calls[2]?.[1]).toEqual(
|
|
expect.objectContaining({
|
|
method: 'POST',
|
|
headers: expect.objectContaining({ Authorization: 'Bearer admin-token' }),
|
|
body: JSON.stringify({
|
|
bucket: 'genarrative-release',
|
|
objectKey:
|
|
'generated-character-drafts/editor/showcase-campaign/current/card.png',
|
|
contentType: 'image/png',
|
|
contentLength: file.size,
|
|
}),
|
|
}),
|
|
);
|
|
expect(uploaded).toEqual({
|
|
imageSrc:
|
|
'/generated-character-drafts/editor/showcase-campaign/current/card.png',
|
|
imageObjectKey:
|
|
'generated-character-drafts/editor/showcase-campaign/current/card.png',
|
|
imageWidth: 1024,
|
|
imageHeight: 1536,
|
|
legacyPublicPath:
|
|
'/generated-character-drafts/editor/showcase-campaign/current/card.png',
|
|
});
|
|
expect(closeBitmap).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
test('充值订单查询按后台契约序列化筛选参数', async () => {
|
|
const fetchMock = vi.fn().mockResolvedValue(
|
|
new Response(JSON.stringify({ entries: [] }), {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
}),
|
|
);
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
await listAdminRechargeOrders('token-1', {
|
|
orderId: 'order 1',
|
|
userId: 'user-1',
|
|
providerTransactionId: 'wx-1',
|
|
paymentChannel: 'wechat_native',
|
|
status: 'paid',
|
|
createdAfter: '2026-07-01T00:00:00.000Z',
|
|
createdBefore: '2026-07-13T23:59:00.000Z',
|
|
limit: 50,
|
|
});
|
|
|
|
const requestUrl = String(fetchMock.mock.calls[0]?.[0]);
|
|
const parsed = new URL(requestUrl, 'http://admin.local');
|
|
expect(parsed.pathname).toBe('/admin/api/profile/recharge-orders');
|
|
expect(Object.fromEntries(parsed.searchParams)).toEqual({
|
|
orderId: 'order 1',
|
|
providerTransactionId: 'wx-1',
|
|
userId: 'user-1',
|
|
paymentChannel: 'wechat_native',
|
|
status: 'paid',
|
|
createdAfter: '2026-07-01T00:00:00.000Z',
|
|
createdBefore: '2026-07-13T23:59:00.000Z',
|
|
limit: '50',
|
|
});
|
|
});
|
|
|
|
test('用户详情只发送实际提供的用户定位字段', async () => {
|
|
const fetchMock = vi
|
|
.fn()
|
|
.mockResolvedValue(
|
|
new Response(JSON.stringify({ userId: 'user-1' }), { status: 200 }),
|
|
);
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
await getAdminUserDetail('token-1', { publicUserCode: 'TN1001' });
|
|
|
|
const requestUrl = String(fetchMock.mock.calls[0]?.[0]);
|
|
const parsed = new URL(requestUrl, 'http://admin.local');
|
|
expect(parsed.pathname).toBe('/admin/api/profile/users/detail');
|
|
expect(Object.fromEntries(parsed.searchParams)).toEqual({
|
|
publicUserCode: 'TN1001',
|
|
});
|
|
});
|
|
|
|
test('历史花费手动对账使用独立管理员写接口', async () => {
|
|
const fetchMock = vi.fn().mockResolvedValue(
|
|
new Response(
|
|
JSON.stringify({
|
|
userId: 'user-1',
|
|
historicalConsumedPoints: 1300,
|
|
changed: true,
|
|
}),
|
|
{ status: 200 },
|
|
),
|
|
);
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
await reconcileAdminUserConsumption('token-1', { userId: 'user-1' });
|
|
|
|
expect(String(fetchMock.mock.calls[0]?.[0])).toBe(
|
|
'/admin/api/profile/users/reconcile-consumption',
|
|
);
|
|
expect(fetchMock.mock.calls[0]?.[1]).toEqual(
|
|
expect.objectContaining({
|
|
method: 'POST',
|
|
headers: expect.objectContaining({ Authorization: 'Bearer token-1' }),
|
|
body: JSON.stringify({ userId: 'user-1' }),
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('退款执行使用独立 execute 管理员路由', async () => {
|
|
const fetchMock = vi.fn().mockResolvedValue(
|
|
new Response(JSON.stringify({ outRefundNo: 'refund-1' }), {
|
|
status: 200,
|
|
}),
|
|
);
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
await executeAdminRechargeRefund('token-1', {
|
|
orderId: 'order-1',
|
|
refundAmountCents: 300,
|
|
requestId: 'request-1',
|
|
reason: '用户申请',
|
|
});
|
|
|
|
expect(String(fetchMock.mock.calls[0]?.[0])).toBe(
|
|
'/admin/api/profile/recharge-refunds/execute',
|
|
);
|
|
expect(fetchMock.mock.calls[0]?.[1]).toEqual(
|
|
expect.objectContaining({
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
orderId: 'order-1',
|
|
refundAmountCents: 300,
|
|
requestId: 'request-1',
|
|
reason: '用户申请',
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('退款人工复核使用独立 resolve 管理员路由', async () => {
|
|
const fetchMock = vi.fn().mockResolvedValue(
|
|
new Response(JSON.stringify({ outRefundNo: 'refund-1' }), {
|
|
status: 200,
|
|
}),
|
|
);
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
await resolveAdminRechargeRefundManualReview('token-1', {
|
|
outRefundNo: 'refund-1',
|
|
reason: '已核对微信商户平台原始账单',
|
|
expectedErrorCode: 'provider_transaction_id_mismatch',
|
|
});
|
|
|
|
expect(String(fetchMock.mock.calls[0]?.[0])).toBe(
|
|
'/admin/api/profile/recharge-refunds/manual-review/resolve',
|
|
);
|
|
expect(fetchMock.mock.calls[0]?.[1]).toEqual(
|
|
expect.objectContaining({
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
outRefundNo: 'refund-1',
|
|
reason: '已核对微信商户平台原始账单',
|
|
expectedErrorCode: 'provider_transaction_id_mismatch',
|
|
}),
|
|
}),
|
|
);
|
|
});
|