import type { IncomingMessage, ServerResponse } from 'node:http'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { createImagePromptMuseumRouteHandler } from '@electron/api/routes/image-prompt-museum'; import type { PromptMuseumCard } from '../../shared/image-prompt-museum'; function createResponse() { const chunks: string[] = []; const res = { statusCode: 0, setHeader: vi.fn(), end: vi.fn((chunk?: string) => { if (chunk) chunks.push(chunk); }), } as unknown as ServerResponse; return { res, get json() { return JSON.parse(chunks.join('')) as Record; }, }; } function card(imageUrl = '/api/image-prompt-museum/prompt-one/media/thumbnail'): PromptMuseumCard { return { id: 'prompt-one', slug: 'prompt-one', title: '示例', summary: '示例摘要', thumbnail: { url: imageUrl, width: 1200, height: 900, alt: '示例' }, categories: [], model: { id: 'image-model', name: 'Image Model' }, language: 'zh-CN', attribution: { author: { name: '作者' }, source: { name: '来源', url: 'https://example.com/source' }, license: { name: 'CC BY 4.0', attributionText: '作者 / 来源 / CC BY 4.0' }, }, publishedAt: '2026-08-01T00:00:00Z', updatedAt: '2026-08-01T00:00:00Z', }; } describe('Prompt Museum Main route boundary', () => { const fetchImpl = vi.fn(); const getAccessToken = vi.fn(); beforeEach(() => { fetchImpl.mockReset(); getAccessToken.mockReset(); }); it('does not claim unrelated routes', async () => { const handler = createImagePromptMuseumRouteHandler({ fetchImpl, getAccessToken }); const response = createResponse(); await expect(handler( { method: 'GET' } as IncomingMessage, response.res, new URL('http://127.0.0.1/api/works/projects'), {} as never, )).resolves.toBe(false); expect(fetchImpl).not.toHaveBeenCalled(); }); it('forwards only the allowed list query and the Works Square token', async () => { getAccessToken.mockResolvedValue('works-token'); fetchImpl.mockResolvedValue(new Response(JSON.stringify({ success: true, data: { items: [], facets: { useCases: [], styles: [], subjects: [] }, nextCursor: null }, }), { status: 200, headers: { 'Content-Type': 'application/json' } })); const handler = createImagePromptMuseumRouteHandler({ fetchImpl, getAccessToken, apiBaseUrl: 'https://square.example', }); const response = createResponse(); await expect(handler( { method: 'GET' } as IncomingMessage, response.res, new URL('http://127.0.0.1/api/works/image-prompt-museum?q=editorial&unknown=do-not-forward&limit=24'), {} as never, )).resolves.toBe(true); expect(fetchImpl).toHaveBeenCalledWith( 'https://square.example/api/image-prompt-museum?q=editorial&limit=24', expect.objectContaining({ method: 'GET', headers: { Accept: 'application/json', Authorization: 'Bearer works-token', }, }), ); expect(response.json).toMatchObject({ success: true }); }); it('accepts controlled relative media URLs in list and detail DTOs', async () => { getAccessToken.mockResolvedValue('works-token'); fetchImpl .mockResolvedValueOnce(new Response(JSON.stringify({ success: true, data: { items: [card()], facets: { useCases: [], styles: [], subjects: [] }, nextCursor: null, }, }), { status: 200, headers: { 'Content-Type': 'application/json' } })) .mockResolvedValueOnce(new Response(JSON.stringify({ success: true, data: { ...card(), prompt: 'Create an image', variables: [], images: [{ url: '/api/image-prompt-museum/prompt-one/media/0', width: 1200, height: 900, alt: '详情' }], requiresReferenceImages: false, }, }), { status: 200, headers: { 'Content-Type': 'application/json' } })); const handler = createImagePromptMuseumRouteHandler({ fetchImpl, getAccessToken }); const listResponse = createResponse(); const detailResponse = createResponse(); await handler( { method: 'GET' } as IncomingMessage, listResponse.res, new URL('http://127.0.0.1/api/works/image-prompt-museum'), {} as never, ); await handler( { method: 'GET' } as IncomingMessage, detailResponse.res, new URL('http://127.0.0.1/api/works/image-prompt-museum/prompt-one'), {} as never, ); expect(listResponse.json).toMatchObject({ success: true, data: { items: [{ thumbnail: { url: '/api/image-prompt-museum/prompt-one/media/thumbnail' } }] }, }); expect(detailResponse.json).toMatchObject({ success: true, data: { images: [{ url: '/api/image-prompt-museum/prompt-one/media/0' }] }, }); }); it('accepts list attribution sources with a missing or null optional URL', async () => { getAccessToken.mockResolvedValue('works-token'); const itemWithoutUrl = card(); itemWithoutUrl.attribution.source = { name: 'PromptHero' }; const itemWithNullUrl = card(); itemWithNullUrl.id = 'prompt-two'; itemWithNullUrl.attribution.source = { name: 'Community archive', url: null }; fetchImpl.mockResolvedValueOnce(new Response(JSON.stringify({ success: true, data: { items: [itemWithoutUrl, itemWithNullUrl], facets: { useCases: [], styles: [], subjects: [] }, nextCursor: null, }, }), { status: 200, headers: { 'Content-Type': 'application/json' } })); const handler = createImagePromptMuseumRouteHandler({ fetchImpl, getAccessToken }); const response = createResponse(); await handler( { method: 'GET' } as IncomingMessage, response.res, new URL('http://127.0.0.1/api/works/image-prompt-museum'), {} as never, ); expect(response.res.statusCode).toBe(200); expect(response.json).toMatchObject({ success: true, data: { items: [ { attribution: { source: { name: 'PromptHero' } } }, { attribution: { source: { name: 'Community archive', url: null } } }, ] }, }); const sources = (response.json.data as { items: Array<{ attribution: { source: unknown } }> }) .items.map((item) => item.attribution.source); expect(sources).toEqual([ { name: 'PromptHero' }, { name: 'Community archive', url: null }, ]); }); it('proxies a fixed media path with Bearer auth and returns only bounded raster data', async () => { getAccessToken.mockResolvedValue('works-token'); fetchImpl.mockResolvedValue(new Response(Uint8Array.from([1, 2, 3]), { status: 200, headers: { 'Content-Type': 'image/png; charset=binary', 'Content-Length': '3' }, })); const handler = createImagePromptMuseumRouteHandler({ fetchImpl, getAccessToken, apiBaseUrl: 'https://square.example', }); const response = createResponse(); await handler( { method: 'GET' } as IncomingMessage, response.res, new URL('http://127.0.0.1/api/works/image-prompt-museum/entry-1:en/media/thumbnail'), {} as never, ); expect(fetchImpl).toHaveBeenCalledWith( 'https://square.example/api/image-prompt-museum/entry-1:en/media/thumbnail', expect.objectContaining({ headers: expect.objectContaining({ Authorization: 'Bearer works-token' }), redirect: 'manual', }), ); expect(response.json).toEqual({ dataBase64: 'AQID', mimeType: 'image/png' }); }); it.each([ ['non-image media', { 'Content-Type': 'text/html' }], ['oversized media', { 'Content-Type': 'image/webp', 'Content-Length': String(10 * 1024 * 1024 + 1) }], ])('rejects %s responses', async (_name, headers) => { getAccessToken.mockResolvedValue('works-token'); fetchImpl.mockResolvedValue(new Response('unsafe', { status: 200, headers })); const handler = createImagePromptMuseumRouteHandler({ fetchImpl, getAccessToken }); const response = createResponse(); await handler( { method: 'GET' } as IncomingMessage, response.res, new URL('http://127.0.0.1/api/works/image-prompt-museum/prompt-one/media/0'), {} as never, ); expect(response.res.statusCode).toBe(502); expect(response.json).toMatchObject({ code: 'PROMPT_MUSEUM_INVALID_RESPONSE' }); }); it('does not claim unsafe or unknown media paths', async () => { const handler = createImagePromptMuseumRouteHandler({ fetchImpl, getAccessToken }); const unsafeResponse = createResponse(); const unknownResponse = createResponse(); await expect(handler( { method: 'GET' } as IncomingMessage, unsafeResponse.res, new URL('http://127.0.0.1/api/works/image-prompt-museum/../media/thumbnail'), {} as never, )).resolves.toBe(false); await expect(handler( { method: 'GET' } as IncomingMessage, unknownResponse.res, new URL('http://127.0.0.1/api/works/image-prompt-museum/prompt-one/media/original'), {} as never, )).resolves.toBe(false); expect(fetchImpl).not.toHaveBeenCalled(); }); it('returns a stable auth error without calling the upstream service', async () => { getAccessToken.mockResolvedValue(null); const handler = createImagePromptMuseumRouteHandler({ fetchImpl, getAccessToken }); const response = createResponse(); await handler( { method: 'GET' } as IncomingMessage, response.res, new URL('http://127.0.0.1/api/works/image-prompt-museum'), {} as never, ); expect(response.res.statusCode).toBe(401); expect(response.json).toMatchObject({ success: false, code: 'PROMPT_MUSEUM_AUTH_REQUIRED', }); expect(fetchImpl).not.toHaveBeenCalled(); }); it('refreshes an upstream 401 once and retries with the new Bearer token', async () => { getAccessToken .mockResolvedValueOnce('expired-token') .mockResolvedValueOnce('fresh-token'); fetchImpl .mockResolvedValueOnce(new Response(JSON.stringify({ success: false }), { status: 401, headers: { 'Content-Type': 'application/json' }, })) .mockResolvedValueOnce(new Response(JSON.stringify({ success: true, data: { items: [], facets: { useCases: [], styles: [], subjects: [] }, nextCursor: null }, }), { status: 200, headers: { 'Content-Type': 'application/json' } })); const handler = createImagePromptMuseumRouteHandler({ fetchImpl, getAccessToken, apiBaseUrl: 'https://square.example', }); const response = createResponse(); await handler( { method: 'GET' } as IncomingMessage, response.res, new URL('http://127.0.0.1/api/works/image-prompt-museum'), {} as never, ); expect(getAccessToken).toHaveBeenNthCalledWith(1, { fetchImpl }); expect(getAccessToken).toHaveBeenNthCalledWith(2, { fetchImpl, forceRefresh: true }); expect(fetchImpl).toHaveBeenCalledTimes(2); expect(fetchImpl).toHaveBeenNthCalledWith( 1, 'https://square.example/api/image-prompt-museum', expect.objectContaining({ headers: expect.objectContaining({ Authorization: 'Bearer expired-token' }) }), ); expect(fetchImpl).toHaveBeenNthCalledWith( 2, 'https://square.example/api/image-prompt-museum', expect.objectContaining({ headers: expect.objectContaining({ Authorization: 'Bearer fresh-token' }) }), ); expect(response.res.statusCode).toBe(200); expect(response.json).toEqual({ success: true, data: { items: [], facets: { useCases: [], styles: [], subjects: [] }, nextCursor: null }, }); }); it('returns the stable auth error when an upstream 401 cannot refresh the session', async () => { getAccessToken .mockResolvedValueOnce('expired-token') .mockResolvedValueOnce(null); fetchImpl.mockResolvedValueOnce(new Response(JSON.stringify({ success: false, error: 'upstream credential detail', }), { status: 401, headers: { 'Content-Type': 'application/json' } })); const handler = createImagePromptMuseumRouteHandler({ fetchImpl, getAccessToken }); const response = createResponse(); await handler( { method: 'GET' } as IncomingMessage, response.res, new URL('http://127.0.0.1/api/works/image-prompt-museum'), {} as never, ); expect(response.res.statusCode).toBe(401); expect(response.json).toEqual({ success: false, status: 401, code: 'PROMPT_MUSEUM_AUTH_REQUIRED', error: '请先登录后再获取灵感', }); expect(fetchImpl).toHaveBeenCalledTimes(1); }); it('redacts upstream and local failure details', async () => { getAccessToken.mockResolvedValue('works-token'); fetchImpl.mockResolvedValueOnce(new Response(JSON.stringify({ success: false, code: 'INTERNAL_DATABASE_FAILURE', error: 'postgres://secret-host/private-table', }), { status: 503, headers: { 'Content-Type': 'application/json' } })); const handler = createImagePromptMuseumRouteHandler({ fetchImpl, getAccessToken }); const upstreamResponse = createResponse(); await handler( { method: 'GET' } as IncomingMessage, upstreamResponse.res, new URL('http://127.0.0.1/api/works/image-prompt-museum'), {} as never, ); expect(upstreamResponse.json).toEqual({ success: false, status: 503, code: 'PROMPT_MUSEUM_UNAVAILABLE', error: '提示词博物馆暂时不可用', }); fetchImpl.mockRejectedValueOnce(new Error('C:\\private\\network.log')); const localResponse = createResponse(); await handler( { method: 'GET' } as IncomingMessage, localResponse.res, new URL('http://127.0.0.1/api/works/image-prompt-museum'), {} as never, ); expect(localResponse.json).toEqual({ success: false, status: 502, code: 'PROMPT_MUSEUM_UNAVAILABLE', error: '提示词博物馆暂时不可用', }); }); it('rejects malformed success DTOs and unsafe image URLs', async () => { getAccessToken.mockResolvedValue('works-token'); fetchImpl.mockResolvedValueOnce(new Response(JSON.stringify({ success: true, data: { items: [card('http://internal.example/secret.png')], facets: { useCases: [], styles: [], subjects: [] }, nextCursor: null, }, }), { status: 200, headers: { 'Content-Type': 'application/json' } })); const handler = createImagePromptMuseumRouteHandler({ fetchImpl, getAccessToken }); const response = createResponse(); await handler( { method: 'GET' } as IncomingMessage, response.res, new URL('http://127.0.0.1/api/works/image-prompt-museum'), {} as never, ); expect(response.res.statusCode).toBe(502); expect(response.json).toEqual({ success: false, status: 502, code: 'PROMPT_MUSEUM_INVALID_RESPONSE', error: '提示词博物馆返回了无效数据', }); }); });