Files
makelore/tests/unit/game-resource-client.test.ts

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);
});
});