130 lines
4.6 KiB
TypeScript
130 lines
4.6 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
GameResourceClient,
|
|
GameResourceClientError,
|
|
isTerminalGameResourceStatus,
|
|
} from '../../electron/services/game-resource-client';
|
|
|
|
const EXECUTION_ID = '11111111-1111-4111-8111-111111111111';
|
|
const RELEASE_ID = '22222222-2222-4222-8222-222222222222';
|
|
const PROJECT_ID = '33333333-3333-4333-8333-333333333333';
|
|
|
|
function generation(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
schema_version: 1,
|
|
plugin_id: 'makelore.game-resource',
|
|
execution_id: EXECUTION_ID,
|
|
release_id: RELEASE_ID,
|
|
project_id: PROJECT_ID,
|
|
logical_operation_id: 'pi:run-a:resource-a',
|
|
kind: 'pixel',
|
|
template_name: 'character',
|
|
status: 'accepted',
|
|
output_count: 0,
|
|
poll_interval_seconds: 3,
|
|
billing: {
|
|
mode: 'platform_metered',
|
|
status: 'dispatched',
|
|
reserved_points: '2.00',
|
|
usage_amount: 1,
|
|
unit: 'generation',
|
|
},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function jsonResponse(value: unknown, status = 200): Response {
|
|
return new Response(JSON.stringify(value), {
|
|
status,
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
describe('GameResourceClient', () => {
|
|
it('refreshes one 401 and parses the provider-neutral metered contract', async () => {
|
|
const fetchImpl = vi.fn<typeof fetch>()
|
|
.mockResolvedValueOnce(new Response(null, { status: 401 }))
|
|
.mockResolvedValueOnce(jsonResponse(generation({ provider_job_id: 'must-not-project' }), 202));
|
|
const getAccessToken = vi.fn(async (options?: { forceRefresh?: boolean }) => (
|
|
options?.forceRefresh ? 'fresh-token' : 'stale-token'
|
|
));
|
|
const client = new GameResourceClient({
|
|
apiBaseUrl: 'https://works.example/',
|
|
fetchImpl,
|
|
getAccessToken: getAccessToken as never,
|
|
});
|
|
|
|
const result = await client.generate({
|
|
releaseAdmissionId: 'admission-a',
|
|
releaseId: RELEASE_ID,
|
|
projectId: PROJECT_ID,
|
|
logicalOperationId: 'pi:run-a:resource-a',
|
|
kind: 'pixel',
|
|
templateName: 'character',
|
|
templateConfig: {},
|
|
requirement: 'A blue-armored hero',
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
executionId: EXECUTION_ID,
|
|
releaseId: RELEASE_ID,
|
|
projectId: PROJECT_ID,
|
|
logicalOperationId: 'pi:run-a:resource-a',
|
|
kind: 'pixel',
|
|
templateName: 'character',
|
|
status: 'accepted',
|
|
outputCount: 0,
|
|
pollIntervalSeconds: 3,
|
|
billing: {
|
|
mode: 'platform_metered', status: 'dispatched', reserved_points: '2.00',
|
|
usage_amount: 1, unit: 'generation',
|
|
},
|
|
});
|
|
expect(JSON.stringify(result)).not.toContain('provider_job_id');
|
|
expect(fetchImpl).toHaveBeenCalledTimes(2);
|
|
expect(fetchImpl.mock.calls[0]?.[1]?.headers).toMatchObject({ Authorization: 'Bearer stale-token' });
|
|
expect(fetchImpl.mock.calls[1]?.[1]?.headers).toMatchObject({ Authorization: 'Bearer fresh-token' });
|
|
expect(getAccessToken).toHaveBeenNthCalledWith(2, { fetchImpl, forceRefresh: true });
|
|
});
|
|
|
|
it('fails closed on a malformed billing receipt', async () => {
|
|
const client = new GameResourceClient({
|
|
fetchImpl: vi.fn<typeof fetch>().mockResolvedValue(jsonResponse(generation({
|
|
billing: { mode: 'platform_metered', status: 'settled', reserved_points: '2.00', unit: 'credit' },
|
|
}))),
|
|
getAccessToken: vi.fn(async () => 'token') as never,
|
|
});
|
|
|
|
await expect(client.get(EXECUTION_ID)).rejects.toMatchObject<GameResourceClientError>({
|
|
code: 'plugin_backend_invalid', status: 502,
|
|
});
|
|
});
|
|
|
|
it('downloads bounded output bytes without accepting provider redirects', async () => {
|
|
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValue(new Response(new Uint8Array([1, 2, 3]), {
|
|
status: 200,
|
|
headers: {
|
|
'content-type': 'image/png',
|
|
'content-disposition': "attachment; filename*=UTF-8''hero.png",
|
|
},
|
|
}));
|
|
const client = new GameResourceClient({
|
|
apiBaseUrl: 'https://works.example',
|
|
fetchImpl,
|
|
getAccessToken: vi.fn(async () => 'token') as never,
|
|
});
|
|
|
|
await expect(client.download(EXECUTION_ID, 1)).resolves.toMatchObject({
|
|
fileName: 'hero.png', contentType: 'image/png', bytes: new Uint8Array([1, 2, 3]),
|
|
});
|
|
expect(fetchImpl.mock.calls[0]?.[0]).toBe(
|
|
`https://works.example/api/plugins/v1/hosted/game-resource/generations/${EXECUTION_ID}/content?output_index=1`,
|
|
);
|
|
expect(fetchImpl.mock.calls[0]?.[1]).toMatchObject({ redirect: 'manual' });
|
|
expect(isTerminalGameResourceStatus('succeeded')).toBe(true);
|
|
expect(isTerminalGameResourceStatus('running')).toBe(false);
|
|
});
|
|
});
|