328ac31844
- 主站:游戏广场、详情、在线游玩、网页发布与作者中心,以及共享契约与客户端服务 - 后端:module-game-distribution 领域层、SpacetimeDB 表/迁移/绑定、spacetime-client facade、api-server 路由与发行网关 - 后台:游戏审核页(待审列表、通过/拒绝、安全下架) - AGC:发布面板、本地导出包读取命令与发布服务,含默认跳过的真实链路测试 - 运维:发行来源 nginx 模板与门禁、game-distribution:publish 灰度发布开关、OSS PutObject 受控重试 - 文档:主规范、里程碑与实施计划、决策日志与踩坑记录
187 lines
5.8 KiB
TypeScript
187 lines
5.8 KiB
TypeScript
/* @vitest-environment jsdom */
|
|
|
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
import { beforeEach, expect, test, vi } from 'vitest';
|
|
|
|
import {
|
|
listAdminGameDistributionReviews,
|
|
reviewAdminGameDistributionVersion,
|
|
suspendAdminGameDistributionGame,
|
|
} from '../api/adminApiClient';
|
|
import type { AdminGameDistributionReviewEntry } from '../api/adminApiTypes';
|
|
import {
|
|
AdminGameDistributionReviewPage,
|
|
resolveGameReleaseEntryUrlError,
|
|
} from './AdminGameDistributionReviewPage';
|
|
|
|
vi.mock('../api/adminApiClient', () => ({
|
|
isAdminApiError: vi.fn(
|
|
(error: unknown) =>
|
|
typeof error === 'object' &&
|
|
error !== null &&
|
|
'status' in error &&
|
|
typeof error.status === 'number',
|
|
),
|
|
formatAdminApiError: vi.fn((error: unknown) =>
|
|
error instanceof Error ? error.message : '请求失败',
|
|
),
|
|
listAdminGameDistributionReviews: vi.fn(),
|
|
reviewAdminGameDistributionVersion: vi.fn(),
|
|
suspendAdminGameDistributionGame: vi.fn(),
|
|
}));
|
|
|
|
const entry: AdminGameDistributionReviewEntry = {
|
|
versionId: 'version-1',
|
|
gameId: 'game_1',
|
|
versionNumber: 2,
|
|
packageSha256: 'a'.repeat(64),
|
|
packageBytes: 2048,
|
|
status: 'pending_review',
|
|
publicationRevision: 4,
|
|
reviewReason: null,
|
|
createdAt: '2026-09-20T08:00:00Z',
|
|
updatedAt: '2026-09-20T08:00:00Z',
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.mocked(listAdminGameDistributionReviews).mockReset();
|
|
vi.mocked(reviewAdminGameDistributionVersion).mockReset();
|
|
vi.mocked(suspendAdminGameDistributionGame).mockReset();
|
|
vi.mocked(listAdminGameDistributionReviews).mockResolvedValue({
|
|
entries: [entry],
|
|
nextCursor: null,
|
|
});
|
|
});
|
|
|
|
test('发行入口必须是带完整来源的 HTTPS 地址', () => {
|
|
expect(resolveGameReleaseEntryUrlError('')).toBe('请填写发行入口');
|
|
expect(
|
|
resolveGameReleaseEntryUrlError('http://games.test/a/index.html'),
|
|
).toBe('发行入口必须以 https:// 开头');
|
|
expect(
|
|
resolveGameReleaseEntryUrlError('https://games.test/a/index.html?token=1'),
|
|
).toBe('发行入口不能包含 query 或 fragment');
|
|
expect(
|
|
resolveGameReleaseEntryUrlError('https://u:p@games.test/a/index.html'),
|
|
).toBe('发行入口不能包含凭据');
|
|
expect(
|
|
resolveGameReleaseEntryUrlError('https://games.test/a/index.html'),
|
|
).toBe('');
|
|
});
|
|
|
|
test('通过审核时提交当前 publicationRevision 与发行入口并刷新列表', async () => {
|
|
vi.mocked(reviewAdminGameDistributionVersion).mockResolvedValue({
|
|
version: { ...entry, status: 'published' },
|
|
replayed: false,
|
|
});
|
|
|
|
render(
|
|
<AdminGameDistributionReviewPage
|
|
token="admin-token"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
await screen.findByText('game_1');
|
|
|
|
fireEvent.change(screen.getByLabelText('发行入口'), {
|
|
target: { value: 'https://games.test/releases/game_1/index.html' },
|
|
});
|
|
fireEvent.click(screen.getByRole('button', { name: '通过' }));
|
|
|
|
await waitFor(() =>
|
|
expect(reviewAdminGameDistributionVersion).toHaveBeenCalledTimes(1),
|
|
);
|
|
const [token, versionId, idempotencyKey, payload] =
|
|
vi.mocked(reviewAdminGameDistributionVersion).mock.calls[0] ?? [];
|
|
expect(token).toBe('admin-token');
|
|
expect(versionId).toBe('version-1');
|
|
expect(String(idempotencyKey)).toContain('version-1');
|
|
expect(payload).toEqual({
|
|
decision: 'approve',
|
|
expectedPublicationRevision: 4,
|
|
entryUrl: 'https://games.test/releases/game_1/index.html',
|
|
});
|
|
await waitFor(() =>
|
|
expect(vi.mocked(listAdminGameDistributionReviews)).toHaveBeenCalledTimes(
|
|
2,
|
|
),
|
|
);
|
|
});
|
|
|
|
test('缺少拒绝理由时不调用审核接口', async () => {
|
|
render(
|
|
<AdminGameDistributionReviewPage
|
|
token="admin-token"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
await screen.findByText('game_1');
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '拒绝' }));
|
|
|
|
expect(await screen.findByText('拒绝审核必须填写理由')).toBeTruthy();
|
|
expect(reviewAdminGameDistributionVersion).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('安全下架需要二次确认,并携带公开修订号与原因', async () => {
|
|
vi.mocked(suspendAdminGameDistributionGame).mockResolvedValue({
|
|
game: {
|
|
id: 'game_1',
|
|
title: '测试游戏',
|
|
status: 'suspended',
|
|
publicationRevision: 5,
|
|
},
|
|
replayed: false,
|
|
});
|
|
|
|
render(
|
|
<AdminGameDistributionReviewPage
|
|
token="admin-token"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
await screen.findByText('game_1');
|
|
|
|
fireEvent.change(screen.getByLabelText('下架原因'), {
|
|
target: { value: '盗用素材' },
|
|
});
|
|
fireEvent.click(screen.getByRole('button', { name: '安全下架' }));
|
|
|
|
// 第一次点击只弹出确认面板,不直接调用后端。
|
|
expect(suspendAdminGameDistributionGame).not.toHaveBeenCalled();
|
|
expect(await screen.findByRole('dialog')).toBeTruthy();
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
|
|
|
await waitFor(() =>
|
|
expect(suspendAdminGameDistributionGame).toHaveBeenCalledTimes(1),
|
|
);
|
|
const [token, gameId, idempotencyKey, payload] =
|
|
vi.mocked(suspendAdminGameDistributionGame).mock.calls[0] ?? [];
|
|
expect(token).toBe('admin-token');
|
|
expect(gameId).toBe('game_1');
|
|
expect(String(idempotencyKey)).toContain('game_1');
|
|
expect(payload).toEqual({
|
|
expectedPublicationRevision: 4,
|
|
reason: '盗用素材',
|
|
});
|
|
expect(await screen.findByText(/已安全下架/u)).toBeTruthy();
|
|
});
|
|
|
|
test('取消确认时不下架', async () => {
|
|
render(
|
|
<AdminGameDistributionReviewPage
|
|
token="admin-token"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
await screen.findByText('game_1');
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '安全下架' }));
|
|
await screen.findByRole('dialog');
|
|
fireEvent.click(screen.getByRole('button', { name: '取消' }));
|
|
|
|
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
|
|
expect(suspendAdminGameDistributionGame).not.toHaveBeenCalled();
|
|
});
|