import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { WebRuntimeAPI } from '#web-runtime/api/web-runtime-api'; const fetchMock = vi.fn(); function jsonResponse(payload: unknown, status = 200): Response { return new Response(JSON.stringify(payload), { status, headers: { 'content-type': 'application/json' } }); } /** The URL the nth fetch call received, as a URL object. */ function fetchedUrl(call = 0): URL { return new URL(String(fetchMock.mock.calls[call]?.[0])); } // Next augments the global RequestInit with `next.revalidate?: number | false`, // so the recorded init has to carry the same shape. type RecordedInit = RequestInit & { next?: { revalidate?: number | false; tags?: string[] } }; function fetchedInit(call = 0): RecordedInit { return fetchMock.mock.calls[call]?.[1] as RecordedInit; } beforeEach(() => { fetchMock.mockReset(); vi.stubGlobal('fetch', fetchMock); }); afterEach(() => { vi.unstubAllGlobals(); vi.unstubAllEnvs(); }); const api = () => new WebRuntimeAPI('https://backend.test'); describe('request plumbing', () => { it('resolves pages against the resolve endpoint with locale and path params', async () => { fetchMock.mockResolvedValue(jsonResponse({ id: 1, title: 'About', path: 'about' })); await api().resolvePage({ locale: 'en', path: 'about' }); const url = fetchedUrl(); expect(url.origin).toBe('https://backend.test'); expect(url.pathname).toBe('/web-manager/public/pages/resolve'); expect(url.searchParams.get('locale')).toBe('en'); expect(url.searchParams.get('path')).toBe('about'); expect(fetchedInit().next).toEqual({ revalidate: 60, tags: ['web-manager', 'web-manager:page:en:about', 'web-manager:settings:en'] }); }); it('treats a 404 as "no such page", not an error', async () => { fetchMock.mockResolvedValue(jsonResponse({ message: 'not found' }, 404)); expect(await api().resolvePage({ locale: 'en', path: 'missing' })).toBeNull(); }); it('throws loudly on any other failure status', async () => { fetchMock.mockResolvedValue(jsonResponse({ message: 'boom' }, 500)); await expect(api().resolvePage({ locale: 'en', path: 'about' })).rejects.toThrow( 'Web Manager request to /web-manager/public/pages/resolve failed with status 500.' ); }); }); describe('getSettings', () => { it('unwraps a { settings } envelope and normalizes it', async () => { fetchMock.mockResolvedValue(jsonResponse({ settings: { footer: { copyright: '© Crux' } } })); const settings = await api().getSettings('de'); expect(fetchedUrl().pathname).toBe('/web-manager/public/settings'); expect(fetchedUrl().searchParams.get('locale')).toBe('de'); expect(settings?.locale).toBe('de'); expect(settings?.footer.copyright).toBe('© Crux'); }); it('returns null on 404', async () => { fetchMock.mockResolvedValue(jsonResponse(null, 404)); expect(await api().getSettings('en')).toBeNull(); }); }); describe('getPreview', () => { it('passes the token and opts out of caching', async () => { fetchMock.mockResolvedValue( jsonResponse({ page: { id: 3, title: 'Draft', path: 'draft', locale: 'de', status: 'draft' } }) ); const resolution = await api().getPreview('tok-123'); expect(fetchedUrl().pathname).toBe('/web-manager/public/pages/preview'); expect(fetchedUrl().searchParams.get('token')).toBe('tok-123'); expect(fetchedInit().cache).toBe('no-store'); expect(resolution?.page.locale).toBe('de'); expect(resolution?.page.status).toBe('draft'); }); it('returns null for an expired or unknown token (404)', async () => { fetchMock.mockResolvedValue(jsonResponse(null, 404)); expect(await api().getPreview('tok-dead')).toBeNull(); }); }); describe('listSitemapPages pagination', () => { it('walks every page the backend reports and concatenates the entries', async () => { fetchMock.mockImplementation((url: URL) => { const page = Number(new URL(String(url)).searchParams.get('page')); return Promise.resolve( jsonResponse({ items: [{ locale: 'en', path: `page-${page}` }], pagination: { lastPage: 3 } }) ); }); const entries = await api().listSitemapPages(); expect(fetchMock).toHaveBeenCalledTimes(3); expect( fetchMock.mock.calls.map((call) => new URL(String(call[0])).searchParams.get('page')) ).toEqual(['1', '2', '3']); expect(fetchedUrl().pathname).toBe('/web-manager/public/pages/sitemap'); expect(entries.map((entry) => entry.path)).toEqual(['page-1', 'page-2', 'page-3']); }); it('makes exactly one request when the listing fits on one page', async () => { fetchMock.mockResolvedValue( jsonResponse({ items: [{ locale: 'en', path: 'only' }], pagination: { lastPage: 1 } }) ); const entries = await api().listSitemapPages(); expect(fetchMock).toHaveBeenCalledTimes(1); expect(entries).toHaveLength(1); }); it('caps a runaway lastPage at 50 requests instead of spinning forever', async () => { fetchMock.mockImplementation(() => Promise.resolve( jsonResponse({ items: [{ locale: 'en', path: 'p' }], pagination: { lastPage: 100000 } }) ) ); const entries = await api().listSitemapPages(); expect(fetchMock).toHaveBeenCalledTimes(50); expect(entries).toHaveLength(50); }); }); describe('getWebRuntimeAPI singleton', () => { async function freshModule() { vi.resetModules(); return await import('#web-runtime/api/web-runtime-api'); } it('throws when BACKEND_API_URL is missing or blank', async () => { const mod = await freshModule(); vi.stubEnv('BACKEND_API_URL', ''); expect(() => mod.getWebRuntimeAPI()).toThrow('BACKEND_API_URL is required'); vi.stubEnv('BACKEND_API_URL', ' '); expect(() => mod.getWebRuntimeAPI()).toThrow('BACKEND_API_URL is required'); }); it('trims whitespace and a trailing slash off the base URL', async () => { const mod = await freshModule(); vi.stubEnv('BACKEND_API_URL', ' https://backend.test/ '); fetchMock.mockResolvedValue(jsonResponse(null, 404)); await mod.getWebRuntimeAPI().getSettings('en'); expect(fetchedUrl().href).toBe('https://backend.test/web-manager/public/settings?locale=en'); }); it('constructs the client once and ignores later env changes (pinned singleton behavior)', async () => { const mod = await freshModule(); vi.stubEnv('BACKEND_API_URL', 'https://first.test'); const first = mod.getWebRuntimeAPI(); vi.stubEnv('BACKEND_API_URL', 'https://second.test'); expect(mod.getWebRuntimeAPI()).toBe(first); vi.stubEnv('BACKEND_API_URL', ''); expect(() => mod.getWebRuntimeAPI()).not.toThrow(); }); });