f1b282f3cd
- 合并 origin/master(304 个提交:DirectProject 聊天容器重构、Project Supervisor 退役、策划附件导入、CI 隔离编译缓存等)。 - 接受 master 对 ProjectSupervisorView / SupervisorChatOnlyView 的退役与预览快捷测试收敛;发布入口改由 DirectProject 聊天头承载。 - DirectProjectChatHeader 新增「发布到游戏广场」入口(无回调不渲染、回合忙态禁用),DirectProjectChatView 透传 onRequestGamePublish。 - App.tsx 继续由工作台壳持有试玩包导出与 GameDistributionPublishPanel,沿用 project.export_package 权限确认队列;check-config 把该命令从 native-only 清单移回 App invoke。 - 后台游戏审核 API / 类型 / 路由测试与 master 新增的 AGC 模板管理按双方保留合并,并修掉拼接造成的接口与用例闭合缺陷。 - 修正 master 自带的 viteProxyConfig 断言:/api/creation-entry 属退役路由,测试改为断言不进入代理。 - 记录合并踩坑:语法结构内部的冲突不能简单按「双方保留」拼接,必须按某一侧骨架重建并跑 tsc 与单文件测试。 - 验证:全量 vitest 393 文件 / 4374 用例通过,root / AGC / admin-web 三端 typecheck,cargo check 与游戏分发 Rust 测试,encoding、doc-index、rustfmt、SpacetimeDB schema guard。
591 lines
17 KiB
TypeScript
591 lines
17 KiB
TypeScript
import { afterEach, expect, test, vi } from 'vitest';
|
|
|
|
import {
|
|
createAdminAccount,
|
|
executeAdminRechargeRefund,
|
|
getAdminAgcTemplates,
|
|
getAdminFeatureGateConfig,
|
|
getAdminUserDetail,
|
|
importAdminAgcTemplates,
|
|
listAdminGameDistributionReviews,
|
|
listAdminRechargeOrders,
|
|
reconcileAdminUserConsumption,
|
|
resolveAdminRechargeRefundManualReview,
|
|
reviewAdminGameDistributionVersion,
|
|
suspendAdminGameDistributionGame,
|
|
updateAdminAccount,
|
|
updateAdminAgcTemplate,
|
|
uploadAdminEditorShowcaseCampaignImage,
|
|
upsertAdminFeatureGateConfig,
|
|
upsertProfileWalletConfig,
|
|
} from './adminApiClient';
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
test('模板管理读取和更新复用认证封装,提交 revision 和封面但不提交 ZIP 或版本', async () => {
|
|
const library = { revision: 'revision-new', writable: true, templates: [] };
|
|
const fetchMock = vi.fn().mockImplementation(
|
|
async () =>
|
|
new Response(JSON.stringify({ ok: true, data: library }), {
|
|
status: 200,
|
|
}),
|
|
);
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
const controller = new AbortController();
|
|
expect(await getAdminAgcTemplates('admin-token', controller.signal)).toEqual(
|
|
library,
|
|
);
|
|
const update = {
|
|
expectedRevision: 'revision-old',
|
|
title: '空白模板',
|
|
summary: '简介',
|
|
tags: ['2D'],
|
|
enabled: true,
|
|
cover: { contentType: 'image/png', dataBase64: 'aW1hZ2U=' },
|
|
};
|
|
expect(
|
|
await updateAdminAgcTemplate('admin-token', 'template/1', update),
|
|
).toEqual(library);
|
|
expect(fetchMock.mock.calls[0]).toEqual([
|
|
'/admin/api/agc-templates',
|
|
expect.objectContaining({
|
|
method: 'GET',
|
|
signal: controller.signal,
|
|
headers: expect.objectContaining({ Authorization: 'Bearer admin-token' }),
|
|
}),
|
|
]);
|
|
expect(fetchMock.mock.calls[1]).toEqual([
|
|
'/admin/api/agc-templates/template%2F1',
|
|
expect.objectContaining({
|
|
method: 'PUT',
|
|
headers: expect.objectContaining({
|
|
Authorization: 'Bearer admin-token',
|
|
'Content-Type': 'application/json',
|
|
}),
|
|
body: JSON.stringify(update),
|
|
}),
|
|
]);
|
|
});
|
|
|
|
test('模板批量导入走 multipart,不预设 JSON Content-Type', async () => {
|
|
const imported = {
|
|
revision: 'rev-2',
|
|
writable: true,
|
|
templates: [],
|
|
imported: [
|
|
{
|
|
id: 'alpha',
|
|
templateVersion: '0.1.0',
|
|
zipSizeBytes: 4,
|
|
zipSha256: 'a'.repeat(64),
|
|
reusedObjects: false,
|
|
},
|
|
],
|
|
};
|
|
const fetchMock = vi.fn().mockImplementation(
|
|
async () =>
|
|
new Response(JSON.stringify({ ok: true, data: imported }), {
|
|
status: 200,
|
|
}),
|
|
);
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
const form = new FormData();
|
|
form.append(
|
|
'manifest',
|
|
JSON.stringify({ expectedRevision: 'rev-1', templates: [] }),
|
|
);
|
|
form.append(
|
|
'zip_0',
|
|
new File([new Uint8Array([1])], 'alpha.zip', { type: 'application/zip' }),
|
|
);
|
|
|
|
expect(await importAdminAgcTemplates('admin-token', form)).toEqual(imported);
|
|
const [url, init] = fetchMock.mock.calls[0]!;
|
|
expect(url).toBe('/admin/api/agc-templates/import');
|
|
expect(init.method).toBe('POST');
|
|
expect(init.body).toBe(form);
|
|
expect(init.headers).not.toHaveProperty('Content-Type');
|
|
expect(init.headers.Authorization).toBe('Bearer admin-token');
|
|
});
|
|
|
|
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',
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('游戏审核列表与审核动作使用约定的 URL、方法和幂等键', async () => {
|
|
const fetchMock = vi.fn().mockImplementation(() =>
|
|
Promise.resolve(
|
|
new Response(JSON.stringify({ entries: [], nextCursor: null }), {
|
|
status: 200,
|
|
}),
|
|
),
|
|
);
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
await listAdminGameDistributionReviews('admin-token');
|
|
await reviewAdminGameDistributionVersion(
|
|
'admin-token',
|
|
'gamever/1',
|
|
'game-review-key-1',
|
|
{
|
|
decision: 'approve',
|
|
expectedPublicationRevision: 3,
|
|
entryUrl: 'https://games.example.test/releases/game_1/index.html',
|
|
},
|
|
);
|
|
|
|
expect(fetchMock.mock.calls[0]?.[0]).toBe(
|
|
'/admin/api/game-distribution/reviews?limit=48',
|
|
);
|
|
expect(fetchMock.mock.calls[1]?.[0]).toBe(
|
|
'/admin/api/game-distribution/versions/gamever%2F1/review',
|
|
);
|
|
expect(fetchMock.mock.calls[1]?.[1]).toEqual(
|
|
expect.objectContaining({
|
|
method: 'POST',
|
|
headers: expect.objectContaining({
|
|
Authorization: 'Bearer admin-token',
|
|
'Idempotency-Key': 'game-review-key-1',
|
|
}),
|
|
body: JSON.stringify({
|
|
decision: 'approve',
|
|
expectedPublicationRevision: 3,
|
|
entryUrl: 'https://games.example.test/releases/game_1/index.html',
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('安全下架请求携带公开修订号、原因与幂等键', async () => {
|
|
const fetchMock = vi.fn().mockImplementation(() =>
|
|
Promise.resolve(
|
|
new Response(JSON.stringify({ game: {}, replayed: false }), {
|
|
status: 200,
|
|
}),
|
|
),
|
|
);
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
await suspendAdminGameDistributionGame(
|
|
'admin-token',
|
|
'game/1',
|
|
'game-suspend-key-1',
|
|
{ expectedPublicationRevision: 7, reason: '版权投诉' },
|
|
);
|
|
|
|
expect(fetchMock.mock.calls[0]?.[0]).toBe(
|
|
'/admin/api/game-distribution/games/game%2F1/suspend',
|
|
);
|
|
expect(fetchMock.mock.calls[0]?.[1]).toEqual(
|
|
expect.objectContaining({
|
|
method: 'POST',
|
|
headers: expect.objectContaining({
|
|
Authorization: 'Bearer admin-token',
|
|
'Idempotency-Key': 'game-suspend-key-1',
|
|
}),
|
|
body: JSON.stringify({
|
|
expectedPublicationRevision: 7,
|
|
reason: '版权投诉',
|
|
}),
|
|
}),
|
|
);
|
|
|
|
expect(() =>
|
|
suspendAdminGameDistributionGame('admin-token', ' ', 'key', {
|
|
expectedPublicationRevision: 1,
|
|
}),
|
|
).toThrow('缺少游戏 ID');
|
|
expect(() =>
|
|
suspendAdminGameDistributionGame('admin-token', 'game-1', ' ', {
|
|
expectedPublicationRevision: 1,
|
|
}),
|
|
).toThrow('下架幂等键必须是 1 到 128 个字符');
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('游戏审核拒绝请求携带理由,空幂等键在本地失败关闭', async () => {
|
|
const fetchMock = vi.fn().mockImplementation(() =>
|
|
Promise.resolve(
|
|
new Response(JSON.stringify({ version: {}, replayed: false }), {
|
|
status: 200,
|
|
}),
|
|
),
|
|
);
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
await reviewAdminGameDistributionVersion(
|
|
'admin-token',
|
|
'version-1',
|
|
'game-review-key-2',
|
|
{
|
|
decision: 'reject',
|
|
expectedPublicationRevision: 0,
|
|
reviewReason: '运行时报错',
|
|
},
|
|
);
|
|
expect(fetchMock.mock.calls[0]?.[1]).toEqual(
|
|
expect.objectContaining({
|
|
body: JSON.stringify({
|
|
decision: 'reject',
|
|
expectedPublicationRevision: 0,
|
|
reviewReason: '运行时报错',
|
|
}),
|
|
}),
|
|
);
|
|
|
|
expect(() =>
|
|
reviewAdminGameDistributionVersion('admin-token', 'version-1', ' ', {
|
|
decision: 'reject',
|
|
expectedPublicationRevision: 0,
|
|
reviewReason: 'x',
|
|
}),
|
|
).toThrow('审核幂等键必须是 1 到 128 个字符');
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
});
|