feat(plugins): move game resources behind marketplace
This commit is contained in:
129
tests/unit/game-resource-client.test.ts
Normal file
129
tests/unit/game-resource-client.test.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
// @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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user