import type { IncomingMessage, ServerResponse } from 'node:http'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { createImagePromptMuseumRouteHandler } from '@electron/api/routes/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; }, }; } 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('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: [{ id: 'prompt-one', slug: 'prompt-one', title: '示例', summary: '示例摘要', thumbnail: { url: 'http://internal.example/secret.png', 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', }], 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: '提示词博物馆返回了无效数据', }); }); });