import { beforeEach, describe, expect, it, vi } from 'vitest'; import { hostApiFetch } from '@/lib/host-api'; import { fetchPromptMuseumEntry, fetchPromptMuseumMedia, fetchPromptMuseumPage, PromptMuseumApiError, } from '@/lib/image-prompt-museum'; vi.mock('@/lib/host-api', () => ({ hostApiFetch: vi.fn(), })); const hostApiFetchMock = vi.mocked(hostApiFetch); describe('Prompt Museum renderer API boundary', () => { beforeEach(() => { hostApiFetchMock.mockReset(); }); it('requests the server-backed list with a whitelisted, encoded query', async () => { hostApiFetchMock.mockResolvedValueOnce({ success: true, status: 200, data: { items: [], facets: { useCases: [], styles: [], subjects: [] }, nextCursor: null }, }); await fetchPromptMuseumPage({ q: '编辑感 海报', useCase: 'poster', style: 'editorial', subject: 'product', language: 'zh-CN', model: 'gpt-image-2', cursor: 'next cursor', limit: 24, }); expect(hostApiFetchMock).toHaveBeenCalledWith( '/api/works/image-prompt-museum?q=%E7%BC%96%E8%BE%91%E6%84%9F+%E6%B5%B7%E6%8A%A5&use_case=poster&style=editorial&subject=product&language=zh-CN&model=gpt-image-2&cursor=next+cursor&limit=24', ); }); it('encodes detail ids and preserves the server entry', async () => { const entry = { id: 'entry/one', title: '一条灵感' }; hostApiFetchMock.mockResolvedValueOnce({ success: true, status: 200, data: entry }); await expect(fetchPromptMuseumEntry('entry/one')).resolves.toBe(entry); expect(hostApiFetchMock).toHaveBeenCalledWith('/api/works/image-prompt-museum/entry%2Fone'); }); it('keeps structured server errors available to the page', async () => { hostApiFetchMock.mockResolvedValueOnce({ success: false, status: 503, code: 'PROMPT_MUSEUM_UNAVAILABLE', error: '灵感库正在维护', }); await expect(fetchPromptMuseumPage()).rejects.toEqual( expect.objectContaining>({ status: 503, code: 'PROMPT_MUSEUM_UNAVAILABLE', message: '灵感库正在维护', }), ); }); it('loads a controlled relative media path through Main and creates a data URL', async () => { hostApiFetchMock.mockResolvedValueOnce({ mimeType: 'image/webp', dataBase64: 'AQID' }); await expect(fetchPromptMuseumMedia( '/api/image-prompt-museum/entry-1:en/media/thumbnail', )).resolves.toBe('data:image/webp;base64,AQID'); expect(hostApiFetchMock).toHaveBeenCalledWith( '/api/works/image-prompt-museum/entry-1:en/media/thumbnail', ); }); it.each([ 'https://cdn.example/image.webp', '/api/image-prompt-museum/../media/thumbnail', '/api/image-prompt-museum/entry/media/original', ])('rejects unsafe media URL %s before IPC', async (url) => { await expect(fetchPromptMuseumMedia(url)).rejects.toMatchObject({ status: 400, code: 'PROMPT_MUSEUM_INVALID_MEDIA_URL', }); expect(hostApiFetchMock).not.toHaveBeenCalled(); }); it('rejects an untrusted Main media payload', async () => { hostApiFetchMock.mockResolvedValueOnce({ mimeType: 'text/html', dataBase64: 'PGgxPg==' }); await expect(fetchPromptMuseumMedia( '/api/image-prompt-museum/entry/media/0', )).rejects.toMatchObject({ status: 502, code: 'PROMPT_MUSEUM_INVALID_MEDIA_RESPONSE', }); }); });