2f0e2a48a3
- `.eslintrc.cjs`:`src/services/gameDistributionClient.ts` 及其测试加入活跃服务白名单,不再被遗留忽略列表吞掉。 - 客户端:按仓库规则修掉排序与未使用类型导入(顺带移除两个只在 re-export 中出现的多余导入)。 - 修复 `scripts/vite-retired-css-plugin.test.ts` 的边界门禁:活跃模块不得停留在 ESLint 忽略名单里。
251 lines
7.8 KiB
TypeScript
251 lines
7.8 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { clearStoredAccessToken, setStoredAccessToken } from './apiClient';
|
|
import {
|
|
cancelGameVersion,
|
|
createGameVersion,
|
|
getGame,
|
|
getGameVersion,
|
|
listGames,
|
|
listMyGames,
|
|
unpublishGame,
|
|
uploadGamePackage,
|
|
} from './gameDistributionClient';
|
|
|
|
describe('gameDistributionClient', () => {
|
|
beforeEach(() => {
|
|
clearStoredAccessToken({ emit: false });
|
|
});
|
|
|
|
it('按关键词和分类读取后端目录', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue(
|
|
new Response(
|
|
JSON.stringify({
|
|
games: [
|
|
{
|
|
id: 'game-1',
|
|
title: '星轨防线',
|
|
summary: '测试游戏',
|
|
currentVersion: { id: 'v1' },
|
|
},
|
|
],
|
|
}),
|
|
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
),
|
|
),
|
|
);
|
|
const games = await listGames({ keyword: '星轨', category: '动作' });
|
|
expect(games).toHaveLength(1);
|
|
expect(games[0]?.id).toBe('game-1');
|
|
});
|
|
|
|
it('读取详情时拒绝空 id', async () => {
|
|
expect(await getGame('')).toBeNull();
|
|
});
|
|
|
|
it('版本创建携带幂等键并保留后端失败状态', async () => {
|
|
setStoredAccessToken('test-access-token', { emit: false });
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockImplementation(
|
|
() =>
|
|
new Response(JSON.stringify({ message: '包校验失败' }), {
|
|
status: 422,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
}),
|
|
),
|
|
);
|
|
await expect(
|
|
createGameVersion(
|
|
'game-1',
|
|
{
|
|
packageSha256: 'x',
|
|
packageBytes: 1,
|
|
packageFileCount: 1,
|
|
packageEntryPath: 'index.html',
|
|
gameMetadata: {
|
|
title: '测试游戏',
|
|
summary: '测试',
|
|
category: '益智',
|
|
deviceSupport: { desktop: true, mobile: false, touch: false },
|
|
inputModes: ['keyboard'],
|
|
orientation: 'responsive',
|
|
},
|
|
},
|
|
'idem-1',
|
|
),
|
|
).rejects.toMatchObject({ status: 422 });
|
|
});
|
|
|
|
it('发行包上传使用独立版本资源和幂等键', async () => {
|
|
const fetchMock = vi.fn().mockImplementation(
|
|
() =>
|
|
new Response(
|
|
JSON.stringify({ versionId: 'version-1', status: 'uploaded' }),
|
|
{
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
},
|
|
),
|
|
);
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
setStoredAccessToken('test-access-token', { emit: false });
|
|
await uploadGamePackage('version-1', new ArrayBuffer(2), 'idem-upload');
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
'/api/game-distribution/versions/version-1/package',
|
|
expect.objectContaining({
|
|
method: 'PUT',
|
|
body: expect.any(ArrayBuffer),
|
|
headers: expect.objectContaining({
|
|
'Content-Type': 'application/zip',
|
|
'Idempotency-Key': 'idem-upload',
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('gameDistributionClient 作者接口', () => {
|
|
beforeEach(() => {
|
|
clearStoredAccessToken({ emit: false });
|
|
});
|
|
|
|
it('读取我的游戏只返回带 id 的条目', async () => {
|
|
setStoredAccessToken('author-token', { emit: false });
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockImplementation(() =>
|
|
Promise.resolve(
|
|
new Response(
|
|
JSON.stringify({
|
|
ok: true,
|
|
data: { games: [{ id: 'game-1' }, {}, null] },
|
|
error: null,
|
|
meta: { apiVersion: 'v1' },
|
|
}),
|
|
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
),
|
|
),
|
|
),
|
|
);
|
|
const games = await listMyGames();
|
|
expect(games.map((game) => game.id)).toEqual(['game-1']);
|
|
const myGamesCall = vi
|
|
.mocked(fetch)
|
|
.mock.calls.find(([url]) => String(url).includes('/my-games'));
|
|
expect(myGamesCall?.[0]).toBe('/api/game-distribution/my-games');
|
|
});
|
|
|
|
it('下架请求携带 CAS 版本与幂等键,空入参在本地失败关闭', async () => {
|
|
setStoredAccessToken('author-token', { emit: false });
|
|
const fetchMock = vi.fn().mockImplementation(() =>
|
|
Promise.resolve(
|
|
new Response(
|
|
JSON.stringify({
|
|
ok: true,
|
|
data: { game: { id: 'game-1', status: 'unpublished' } },
|
|
error: null,
|
|
meta: { apiVersion: 'v1' },
|
|
}),
|
|
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
),
|
|
),
|
|
);
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
await unpublishGame('game-1', 4, 'unpublish-key-1');
|
|
const unpublishCalls = () =>
|
|
fetchMock.mock.calls.filter(([url]) =>
|
|
String(url).includes('/unpublish'),
|
|
);
|
|
expect(unpublishCalls()).toHaveLength(1);
|
|
expect(unpublishCalls()[0]?.[0]).toBe(
|
|
'/api/game-distribution/games/game-1/unpublish',
|
|
);
|
|
expect(unpublishCalls()[0]?.[1]).toEqual(
|
|
expect.objectContaining({
|
|
method: 'POST',
|
|
body: JSON.stringify({ expectedPublicationRevision: 4 }),
|
|
headers: expect.objectContaining({
|
|
'Idempotency-Key': 'unpublish-key-1',
|
|
}),
|
|
}),
|
|
);
|
|
|
|
await expect(unpublishGame(' ', 1, 'k')).rejects.toThrow('缺少游戏编号');
|
|
await expect(unpublishGame('game-1', 1, ' ')).rejects.toThrow(
|
|
'缺少幂等键',
|
|
);
|
|
// 本地校验失败不得发出任何下架请求。
|
|
expect(unpublishCalls()).toHaveLength(1);
|
|
});
|
|
|
|
it('回读单个版本状态与服务端恢复动作', async () => {
|
|
setStoredAccessToken('test-access-token', { emit: false });
|
|
const fetchMock = vi.fn().mockImplementation(() =>
|
|
Promise.resolve(
|
|
new Response(
|
|
JSON.stringify({
|
|
game: { id: 'game-1' },
|
|
version: {
|
|
versionId: 'version-1',
|
|
status: 'upload_failed',
|
|
recoveryAction: 'reupload',
|
|
},
|
|
}),
|
|
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
),
|
|
),
|
|
);
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
const detail = await getGameVersion('version-1');
|
|
expect(detail.version.recoveryAction).toBe('reupload');
|
|
const readCall = fetchMock.mock.calls.find(([url]) =>
|
|
String(url).includes('/api/game-distribution/versions/version-1'),
|
|
);
|
|
expect(readCall?.[0]).toBe('/api/game-distribution/versions/version-1');
|
|
expect(readCall?.[1]?.method).toBe('GET');
|
|
});
|
|
|
|
it('撤回版本带幂等键与公开修订号,并拒绝空标识', async () => {
|
|
setStoredAccessToken('test-access-token', { emit: false });
|
|
const fetchMock = vi.fn().mockImplementation(() =>
|
|
Promise.resolve(
|
|
new Response(JSON.stringify({ replayed: false }), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
}),
|
|
),
|
|
);
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
await cancelGameVersion('version-1', 3, 'idem-1', '看错了');
|
|
const cancelCalls = () =>
|
|
fetchMock.mock.calls.filter(([url]) =>
|
|
String(url).includes('/versions/version-1/cancel'),
|
|
);
|
|
expect(cancelCalls()).toHaveLength(1);
|
|
const cancelCall = cancelCalls()[0];
|
|
expect(cancelCall?.[1]?.method).toBe('POST');
|
|
expect(
|
|
(cancelCall?.[1]?.headers as Record<string, string>)['Idempotency-Key'],
|
|
).toBe('idem-1');
|
|
expect(JSON.parse(String(cancelCall?.[1]?.body))).toEqual({
|
|
expectedPublicationRevision: 3,
|
|
reason: '看错了',
|
|
});
|
|
|
|
await expect(cancelGameVersion('', 1, 'idem-2')).rejects.toThrow(
|
|
'撤回版本缺少版本编号',
|
|
);
|
|
await expect(cancelGameVersion('version-1', 1, ' ')).rejects.toThrow(
|
|
'撤回版本缺少幂等键',
|
|
);
|
|
// 本地校验失败不得发出任何撤回请求。
|
|
expect(cancelCalls()).toHaveLength(1);
|
|
});
|
|
});
|