392 lines
11 KiB
TypeScript
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/',
|
|
});
|
|
});
|
|
});
|