Makelore 2.0 initial clean snapshot
This commit is contained in:
292
tests/unit/meowa-game-assets-route.test.ts
Normal file
292
tests/unit/meowa-game-assets-route.test.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
import { EventEmitter } from 'node:events';
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { handleMeowaGameAssetsRoutes } from '@electron/api/routes/meowa-game-assets';
|
||||
|
||||
const getApiKeyMock = vi.hoisted(() => vi.fn());
|
||||
const storeApiKeyMock = vi.hoisted(() => vi.fn());
|
||||
const deleteApiKeyMock = vi.hoisted(() => vi.fn());
|
||||
const readBundledMeowaApiKeyMock = vi.hoisted(() => vi.fn());
|
||||
const readEmbeddedMeowaApiKeyMock = vi.hoisted(() => vi.fn());
|
||||
const proxyAwareFetchMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('@electron/utils/secure-storage', () => ({
|
||||
getApiKey: (...args: unknown[]) => getApiKeyMock(...args),
|
||||
storeApiKey: (...args: unknown[]) => storeApiKeyMock(...args),
|
||||
deleteApiKey: (...args: unknown[]) => deleteApiKeyMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock('@electron/services/meowa-game-assets-release-credential', () => ({
|
||||
readBundledMeowaApiKey: (...args: unknown[]) => readBundledMeowaApiKeyMock(...args),
|
||||
readEmbeddedMeowaApiKey: (...args: unknown[]) => readEmbeddedMeowaApiKeyMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock('@electron/utils/proxy-fetch', () => ({
|
||||
proxyAwareFetch: (...args: unknown[]) => proxyAwareFetchMock(...args),
|
||||
}));
|
||||
|
||||
function createResponse() {
|
||||
const chunks: string[] = [];
|
||||
const response = {
|
||||
statusCode: 0,
|
||||
setHeader: vi.fn(),
|
||||
end: vi.fn((chunk?: string) => {
|
||||
if (chunk) chunks.push(chunk);
|
||||
}),
|
||||
} as unknown as ServerResponse;
|
||||
return {
|
||||
response,
|
||||
json: () => JSON.parse(chunks.join('')) as Record<string, unknown>,
|
||||
};
|
||||
}
|
||||
|
||||
function createRequest(method: string, body?: unknown): IncomingMessage {
|
||||
const request = new EventEmitter();
|
||||
Object.assign(request, {
|
||||
method,
|
||||
headers: body === undefined ? {} : { 'content-type': 'application/json' },
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
if (body !== undefined) yield Buffer.from(JSON.stringify(body));
|
||||
},
|
||||
});
|
||||
return request as IncomingMessage;
|
||||
}
|
||||
|
||||
describe('Meowa game-assets Host API routes', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
delete process.env.MEOWART_API_KEY;
|
||||
getApiKeyMock.mockResolvedValue(null);
|
||||
readBundledMeowaApiKeyMock.mockResolvedValue(null);
|
||||
readEmbeddedMeowaApiKeyMock.mockReturnValue(null);
|
||||
storeApiKeyMock.mockResolvedValue(true);
|
||||
deleteApiKeyMock.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it('reports configuration without returning the stored credential', async () => {
|
||||
getApiKeyMock.mockResolvedValue('meowa-secret');
|
||||
const { response, json } = createResponse();
|
||||
|
||||
await handleMeowaGameAssetsRoutes(
|
||||
createRequest('GET'),
|
||||
response,
|
||||
new URL('http://127.0.0.1/api/meowa/game-assets/config'),
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(json()).toMatchObject({
|
||||
configured: true,
|
||||
credentialSource: 'secure-store',
|
||||
provider: 'meowa',
|
||||
});
|
||||
expect(json()).not.toHaveProperty('quota');
|
||||
expect(JSON.stringify(json())).not.toContain('meowa-secret');
|
||||
});
|
||||
|
||||
it('hydrates and reports a packaged release credential without returning it', async () => {
|
||||
readBundledMeowaApiKeyMock.mockResolvedValue('meowa-release-secret');
|
||||
const { response, json } = createResponse();
|
||||
|
||||
await handleMeowaGameAssetsRoutes(
|
||||
createRequest('GET'),
|
||||
response,
|
||||
new URL('http://127.0.0.1/api/meowa/game-assets/config'),
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(json()).toMatchObject({
|
||||
configured: true,
|
||||
credentialSource: 'release-bundle',
|
||||
});
|
||||
expect(JSON.stringify(json())).not.toContain('meowa-release-secret');
|
||||
expect(storeApiKeyMock).toHaveBeenCalledWith('meowa-game-assets', 'meowa-release-secret');
|
||||
});
|
||||
|
||||
it('persists and reports the embedded credential without returning it', async () => {
|
||||
readEmbeddedMeowaApiKeyMock.mockReturnValue('embedded-meowa-secret');
|
||||
const { response, json } = createResponse();
|
||||
|
||||
await handleMeowaGameAssetsRoutes(
|
||||
createRequest('GET'),
|
||||
response,
|
||||
new URL('http://127.0.0.1/api/meowa/game-assets/config'),
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(json()).toMatchObject({ configured: true, credentialSource: 'embedded' });
|
||||
expect(JSON.stringify(json())).not.toContain('embedded-meowa-secret');
|
||||
expect(storeApiKeyMock).toHaveBeenCalledWith('meowa-game-assets', 'embedded-meowa-secret');
|
||||
});
|
||||
|
||||
it('prefers the packaged release credential over the embedded fallback', async () => {
|
||||
readBundledMeowaApiKeyMock.mockResolvedValue('release-secret');
|
||||
readEmbeddedMeowaApiKeyMock.mockReturnValue('embedded-secret');
|
||||
const { response, json } = createResponse();
|
||||
|
||||
await handleMeowaGameAssetsRoutes(
|
||||
createRequest('GET'),
|
||||
response,
|
||||
new URL('http://127.0.0.1/api/meowa/game-assets/config'),
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(json()).toMatchObject({ credentialSource: 'release-bundle' });
|
||||
expect(storeApiKeyMock).toHaveBeenCalledWith('meowa-game-assets', 'release-secret');
|
||||
expect(readEmbeddedMeowaApiKeyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not expose the removed local quota route', async () => {
|
||||
getApiKeyMock.mockResolvedValue('meowa-secret');
|
||||
const { response } = createResponse();
|
||||
|
||||
const handled = await handleMeowaGameAssetsRoutes(
|
||||
createRequest('GET'),
|
||||
response,
|
||||
new URL('http://127.0.0.1/api/meowa/game-assets/quota'),
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(handled).toBe(false);
|
||||
expect(response.end).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('proxies template info with the stored credential kept in Main', async () => {
|
||||
getApiKeyMock.mockResolvedValue('meowa-secret');
|
||||
proxyAwareFetchMock.mockResolvedValue(new Response(JSON.stringify({ templates: [{ name: 'pixel-character' }] }), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
}));
|
||||
const { response, json } = createResponse();
|
||||
|
||||
await handleMeowaGameAssetsRoutes(
|
||||
createRequest('GET'),
|
||||
response,
|
||||
new URL('http://127.0.0.1/api/meowa/game-assets/template-info?kind=pixel'),
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(json()).toEqual({ templates: [{ name: 'pixel-character' }] });
|
||||
expect(proxyAwareFetchMock).toHaveBeenCalledWith(
|
||||
'https://api.meowa.ai/api/pixel-gen/template-info',
|
||||
expect.objectContaining({ headers: expect.objectContaining({ Authorization: 'Bearer meowa-secret' }) }),
|
||||
);
|
||||
});
|
||||
|
||||
it('converts generation JSON into Meowa multipart form data', async () => {
|
||||
getApiKeyMock.mockResolvedValue('meowa-secret');
|
||||
proxyAwareFetchMock.mockResolvedValue(new Response(JSON.stringify({ api_job_id: 'job-1' }), {
|
||||
status: 202,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
}));
|
||||
const { response, json } = createResponse();
|
||||
|
||||
await handleMeowaGameAssetsRoutes(
|
||||
createRequest('POST', {
|
||||
kind: 'pixel',
|
||||
templateName: 'pixel-character',
|
||||
requirement: '透明背景的像素角色',
|
||||
templateConfig: { size: 64 },
|
||||
}),
|
||||
response,
|
||||
new URL('http://127.0.0.1/api/meowa/game-assets/generate'),
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(202);
|
||||
expect(json()).toEqual({ api_job_id: 'job-1' });
|
||||
const [, init] = proxyAwareFetchMock.mock.calls[0] as [string, RequestInit];
|
||||
const form = init.body as FormData;
|
||||
expect(form.get('template_name')).toBe('pixel-character');
|
||||
expect(form.get('requirement')).toBe('透明背景的像素角色');
|
||||
expect(form.get('template_config')).toBe('{"size":64}');
|
||||
expect(JSON.stringify([...form.entries()])).not.toContain('meowa-secret');
|
||||
});
|
||||
|
||||
it('forwards generation without a local daily quota check', async () => {
|
||||
getApiKeyMock.mockResolvedValue('meowa-secret');
|
||||
proxyAwareFetchMock.mockResolvedValue(new Response(JSON.stringify({ api_job_id: 'job-after-ten' }), {
|
||||
status: 202,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
}));
|
||||
const { response, json } = createResponse();
|
||||
|
||||
await handleMeowaGameAssetsRoutes(
|
||||
createRequest('POST', {
|
||||
kind: 'pixel',
|
||||
templateName: 'pixel-character',
|
||||
requirement: '透明背景的像素角色',
|
||||
}),
|
||||
response,
|
||||
new URL('http://127.0.0.1/api/meowa/game-assets/generate'),
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(202);
|
||||
expect(json()).toEqual({ api_job_id: 'job-after-ten' });
|
||||
expect(proxyAwareFetchMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('forwards Meowa rejection when a generation submission fails', async () => {
|
||||
getApiKeyMock.mockResolvedValue('meowa-secret');
|
||||
proxyAwareFetchMock.mockResolvedValue(new Response(JSON.stringify({ error: 'invalid template' }), {
|
||||
status: 400,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
}));
|
||||
const { response } = createResponse();
|
||||
|
||||
await handleMeowaGameAssetsRoutes(
|
||||
createRequest('POST', {
|
||||
kind: 'pixel',
|
||||
templateName: 'pixel-character',
|
||||
requirement: '透明背景的像素角色',
|
||||
}),
|
||||
response,
|
||||
new URL('http://127.0.0.1/api/meowa/game-assets/generate'),
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('downloads bytes through Main without exposing a signed upstream URL', async () => {
|
||||
getApiKeyMock.mockResolvedValue('meowa-secret');
|
||||
proxyAwareFetchMock.mockResolvedValue(new Response(Buffer.from('png-bytes'), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'image/png' },
|
||||
}));
|
||||
const { response, json } = createResponse();
|
||||
|
||||
await handleMeowaGameAssetsRoutes(
|
||||
createRequest('GET'),
|
||||
response,
|
||||
new URL('http://127.0.0.1/api/meowa/game-assets/download?kind=pixel&id=job-1'),
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(json()).toMatchObject({ success: true, mimeType: 'image/png', dataBase64: Buffer.from('png-bytes').toString('base64') });
|
||||
expect(JSON.stringify(json())).not.toContain('meowa-secret');
|
||||
});
|
||||
|
||||
it('blocks generation before network access when no credential is configured', async () => {
|
||||
const { response, json } = createResponse();
|
||||
|
||||
await handleMeowaGameAssetsRoutes(
|
||||
createRequest('POST', { kind: 'pixel', templateName: 'pixel-character', requirement: 'test' }),
|
||||
response,
|
||||
new URL('http://127.0.0.1/api/meowa/game-assets/generate'),
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(409);
|
||||
expect(json()).toMatchObject({
|
||||
code: 'MEOWA_API_KEY_MISSING',
|
||||
error: 'Meowa 素材服务尚未配置,请联系管理员。',
|
||||
});
|
||||
expect(proxyAwareFetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user