Files
Genarrative/apps/admin-web/src/api/adminApiClient.test.ts
T
kdletters 93ed7f2c57
Project CI / Repository checks (push) Successful in 1m0s
Project CI / Backend tests (push) Successful in 3m30s
Project CI / Frontend tests (push) Successful in 2m57s
Project CI / Native shell tests (push) Successful in 12m11s
将每日免费泥点纳入后台配置
在账号配置页一次维护初始泥点和每日免费额度
将每日额度贯穿钱包配置、日切重置与充值中心投影
补齐迁移兼容、生成绑定、测试和文档
2026-07-31 13:04:27 +08:00

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