Files
makelore/tests/unit/works-square.test.ts
2026-07-29 17:22:35 +08:00

392 lines
11 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
createWorksProject,
downloadWorksAssetToProject,
fetchMyWorksProjectStatus,
fetchMyWorksProjects,
fetchWorksAsset,
fetchWorksAssets,
fetchWorksTokenUsage,
fetchWorksProjectVersions,
fetchWorksProjects,
toPlazaCard,
uploadWorksProjectZip,
WorksSquareApiError,
type ProjectPublic,
} from '@/lib/works-square';
const hostApiFetchMock = vi.hoisted(() => vi.fn());
vi.mock('@/lib/host-api', () => ({
hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args),
}));
describe('works square client', () => {
beforeEach(() => {
hostApiFetchMock.mockReset();
});
it('loads public projects through the host api proxy', async () => {
const page = {
items: [
{
app_id: 'space-cleaner',
title: '太空清洁队',
summary: '收集漂浮垃圾的小游戏。',
cover_url: null,
category: 'game',
age_band: '8-12',
difficulty: 'beginner',
updated_at: '2026-06-20T22:55:37.790408+08:00',
},
],
next_cursor: null,
limit: 12,
};
hostApiFetchMock.mockResolvedValueOnce({ success: true, page });
const result = await fetchWorksProjects({ q: 'space game', category: 'game', limit: 12 });
expect(result).toEqual(page);
expect(hostApiFetchMock).toHaveBeenCalledWith(
'/api/works/projects?q=space+game&category=game&limit=12',
);
});
it('loads public assets through the host api proxy', async () => {
const page = {
items: [
{
slug: 'tiny-farm',
title: 'Tiny Farm',
summary: 'Farm pixel resource package',
category: '2D',
source: 'Kenney',
source_url: 'https://kenney.nl/assets/tiny-farm',
license: 'CC0 1.0 Universal',
license_url: 'https://creativecommons.org/publicdomain/zero/1.0/',
series: 'Tiny',
tags: ['farm', 'pixel'],
preview_url: 'https://example.com/tiny-farm.png',
image_urls: ['https://example.com/tiny-farm.png'],
archive_size_bytes: 204800,
extracted_file_count: 141,
published_at: '2026-07-09T00:00:00Z',
updated_at: '2026-07-09T08:30:00Z',
},
],
next_cursor: null,
limit: 12,
};
hostApiFetchMock.mockResolvedValueOnce({ success: true, assets: page });
const result = await fetchWorksAssets({
q: 'farm pack',
category: '2D',
tag: 'pixel',
source: 'Kenney',
limit: 12,
});
expect(result).toEqual(page);
expect(hostApiFetchMock).toHaveBeenCalledWith(
'/api/works/assets?q=farm+pack&category=2D&tag=pixel&source=Kenney&limit=12',
);
});
it('loads a public asset detail by slug', async () => {
const asset = {
slug: 'tiny-farm',
title: 'Tiny Farm',
summary: 'Farm pixel resource package',
category: '2D',
source: 'Kenney',
tags: ['farm'],
preview_url: null,
image_urls: [],
archive_size_bytes: null,
extracted_file_count: null,
published_at: null,
updated_at: null,
};
hostApiFetchMock.mockResolvedValueOnce({ success: true, asset });
const result = await fetchWorksAsset('tiny farm');
expect(result).toEqual(asset);
expect(hostApiFetchMock).toHaveBeenCalledWith('/api/works/assets/tiny%20farm');
});
it('downloads a public asset into a selected project', async () => {
const download = {
slug: 'tiny-farm',
filePath: 'D:/repo/game/assets/resource-square/tiny-farm.zip',
relativePath: 'assets/resource-square/tiny-farm.zip',
bytesWritten: 11,
};
hostApiFetchMock.mockResolvedValueOnce({ success: true, download });
const result = await downloadWorksAssetToProject({
slug: 'tiny farm',
projectId: 'prj_123',
});
expect(result).toEqual(download);
expect(hostApiFetchMock).toHaveBeenCalledWith('/api/works/assets/tiny%20farm/download', {
method: 'POST',
body: JSON.stringify({ projectId: 'prj_123' }),
});
});
it('creates project metadata with the current access token', async () => {
const project = {
app_id: 'space-cleaner',
title: '太空清洁队',
summary: '收集漂浮垃圾的小游戏。',
cover_url: null,
category: 'game',
age_band: '8-12',
difficulty: 'beginner',
};
hostApiFetchMock.mockResolvedValueOnce({ success: true, project });
await createWorksProject('access-token', project);
expect(hostApiFetchMock).toHaveBeenCalledWith('/api/works/projects', {
method: 'POST',
body: JSON.stringify({ accessToken: 'access-token', project }),
});
});
it('throws api errors with upstream status for project metadata creation failures', async () => {
hostApiFetchMock.mockResolvedValueOnce({
success: false,
status: 409,
error: 'App ID already exists',
});
await expect(createWorksProject('access-token', {
app_id: 'space-cleaner',
title: 'Space Cleaner',
summary: 'Catch space trash',
})).rejects.toMatchObject({
name: 'WorksSquareApiError',
statusCode: 409,
message: 'App ID already exists',
});
});
it('loads the current user projects with the current access token', async () => {
const page = {
items: [
{
app_id: 'space-cleaner',
title: 'Space Cleaner',
summary: 'Catch space trash',
cover_url: null,
category: 'game',
age_band: '8-12',
difficulty: 'beginner',
status: 'draft',
updated_at: '2026-06-20T22:55:37.790408+08:00',
playable: false,
runtime_url: null,
},
],
next_cursor: null,
limit: 50,
};
hostApiFetchMock.mockResolvedValueOnce({ success: true, page });
const result = await fetchMyWorksProjects('access-token', { limit: 50 });
expect(result).toEqual(page);
expect(hostApiFetchMock).toHaveBeenCalledWith(
'/api/works/projects/mine?limit=50',
{
headers: {
'X-NianCode-Access-Token': 'access-token',
},
},
);
});
it('loads billing token usage with the current access token', async () => {
const usage = {
five_hour_remaining_percent: 72.5,
weekly_remaining_percent: 48,
plan_code: 'pro',
plan_name: '专业版',
};
hostApiFetchMock.mockResolvedValueOnce({ success: true, usage });
const result = await fetchWorksTokenUsage('access-token');
expect(result).toEqual(usage);
expect(result.plan_code).toBe('pro');
expect(result.plan_name).toBe('专业版');
expect(hostApiFetchMock).toHaveBeenCalledWith(
'/api/works/billing/token-usage',
{
headers: {
'X-NianCode-Access-Token': 'access-token',
},
},
);
});
it('loads the current user project status with versions', async () => {
const status = {
project: {
app_id: 'space-cleaner',
title: 'Space Cleaner',
summary: 'Catch space trash',
cover_url: null,
category: 'game',
age_band: '8-12',
difficulty: 'beginner',
status: 'draft',
updated_at: '2026-06-20T22:55:37.790408+08:00',
playable: false,
runtime_url: null,
},
latest_version: {
id: 'version-new',
version_name: 'v1.1.0',
review_status: 'building',
change_log: 'Add score board',
build_job_id: 'job-new',
created_at: '2026-06-21T10:30:00+08:00',
},
versions: [],
};
hostApiFetchMock.mockResolvedValueOnce({ success: true, status });
const result = await fetchMyWorksProjectStatus('access-token', 'space cleaner');
expect(result).toEqual(status);
expect(hostApiFetchMock).toHaveBeenCalledWith(
'/api/works/projects/mine/space%20cleaner/status',
{
headers: {
'X-NianCode-Access-Token': 'access-token',
},
},
);
});
it('exposes an api error class for status-based recovery', () => {
const error = new WorksSquareApiError('Conflict', 409);
expect(error.name).toBe('WorksSquareApiError');
expect(error.statusCode).toBe(409);
expect(error.message).toBe('Conflict');
});
it('uploads a project zip version with the current access token', async () => {
hostApiFetchMock.mockResolvedValueOnce({
success: true,
upload: {
version_id: 'b2f6f1dd-cf4b-4f03-a4b8-a71e2c8dd5f1',
review_status: 'building',
},
});
const result = await uploadWorksProjectZip({
accessToken: 'access-token',
appId: 'space cleaner',
projectId: 'project-1',
versionName: 'v1.0.0',
changeLog: 'Initial upload',
zipFilePath: '/tmp/project.zip',
});
expect(result).toEqual({
version_id: 'b2f6f1dd-cf4b-4f03-a4b8-a71e2c8dd5f1',
review_status: 'building',
});
expect(hostApiFetchMock).toHaveBeenCalledWith(
'/api/works/projects/space%20cleaner/versions/upload',
{
method: 'POST',
body: JSON.stringify({
accessToken: 'access-token',
projectId: 'project-1',
versionName: 'v1.0.0',
changeLog: 'Initial upload',
zipFilePath: '/tmp/project.zip',
}),
},
);
});
it('loads uploaded versions with the current access token', async () => {
hostApiFetchMock.mockResolvedValueOnce({
success: true,
versions: {
items: [
{
id: 'ver_1',
version_name: 'v1.0.0',
review_status: 'building',
change_log: 'First submitted version',
build_job_id: 'job_1',
created_at: '2026-06-21T10:30:00+08:00',
},
],
},
});
const result = await fetchWorksProjectVersions('access-token', 'space cleaner');
expect(result).toEqual({
items: [
{
id: 'ver_1',
version_name: 'v1.0.0',
review_status: 'building',
change_log: 'First submitted version',
build_job_id: 'job_1',
created_at: '2026-06-21T10:30:00+08:00',
},
],
});
expect(hostApiFetchMock).toHaveBeenCalledWith(
'/api/works/projects/space%20cleaner/versions',
{
headers: {
'X-NianCode-Access-Token': 'access-token',
},
},
);
});
it('maps api projects to gallery cards', () => {
const project: ProjectPublic = {
app_id: 'space-cleaner',
title: '太空清洁队',
summary: '收集漂浮垃圾的小游戏。',
cover_url: 'https://example.com/cover.png',
category: 'game',
age_band: '8-12',
difficulty: 'beginner',
updated_at: '2026-06-20T22:55:37.790408+08:00',
playable: true,
runtime_url: '/apps/space-cleaner/',
};
expect(toPlazaCard(project)).toEqual({
id: 'space-cleaner',
title: '太空清洁队',
summary: '收集漂浮垃圾的小游戏。',
coverImage: 'https://example.com/cover.png',
type: 'game',
ageBand: '8-12',
difficulty: 'beginner',
updatedAt: '2026-06-20T22:55:37.790408+08:00',
playable: true,
runtimeUrl: '/apps/space-cleaner/',
});
});
});