From 2deac88c0e2410fde8d9705dec7f7cff70c1edea Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Mon, 12 Oct 2026 19:46:29 +0200 Subject: [PATCH] test(17-06): menu layer link and client tests - every refused scheme and spacing renders as text; protocol-relative URLs are anchors, never site paths or active; rel only for _blank; active matching rules and same-host absolute URLs; the trail marks ancestors only - useMenuApi uses the server base with the forwarded visitor on the server and the public base in the browser --- tests/client.test.ts | 56 +++++++++++++++++++++++++++++++++ tests/menuLinks.test.ts | 69 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/tests/client.test.ts b/tests/client.test.ts index 7946d56..e047550 100644 --- a/tests/client.test.ts +++ b/tests/client.test.ts @@ -76,3 +76,59 @@ test('isNotFound reads status, statusCode and response.status', () => { assert.equal(isNotFound({ status: 500 }), false) assert.equal(isNotFound(null), false) }) + +// Plan 06 coverage: useMenuApi picks the base. The composable reads Nuxt +// globals and import.meta.server, so the test loads its source with +// import.meta.server replaced by a flag, next to the original (its relative +// import resolves), with stubbed globals. +import { readFileSync, rmSync, writeFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' + +type FetchCall = { path: string; opts: { baseURL: string; query?: Record; headers?: Record } } + +let loads = 0 + +async function useMenuApiAs(server: boolean, request: { context?: Record; xff?: string; peer?: string } = {}) { + const calls: FetchCall[] = [] + const g = globalThis as Record + g.__menuLayerServer = server + g.useRuntimeConfig = () => ({ menu: { apiBase: 'http://127.0.0.1:8096/_menu/api/v1' }, public: { menu: { apiBase: '/_menu/api/v1' } } }) + g.useRequestEvent = () => ({ context: request.context ?? {}, node: { req: { socket: { remoteAddress: request.peer } } } }) + g.useRequestHeaders = () => (request.xff === undefined ? {} : { 'x-forwarded-for': request.xff }) + g.$fetch = async (path: string, opts: FetchCall['opts']) => { + calls.push({ path, opts }) + return MENU + } + const source = join(dirname(fileURLToPath(import.meta.url)), '..', 'app', 'composables', 'useMenuApi.ts') + const out = join(dirname(source), `.useMenuApi-${process.pid}-${++loads}.compiled.ts`) + writeFileSync(out, readFileSync(source, 'utf8').replaceAll('import.meta.server', '(globalThis as Record).__menuLayerServer')) + try { + const mod = (await import(pathToFileURL(out).href)) as { useMenuApi: () => ReturnType } + return { client: mod.useMenuApi(), calls } + } finally { + rmSync(out, { force: true }) + } +} + +test('the server base is used on the server, with the visitor forwarded', async () => { + const { client, calls } = await useMenuApiAs(true, { xff: '203.0.113.7', peer: '10.0.0.2' }) + await client.getMenu('main menu', 'pl') + assert.deepEqual(calls, [ + { path: '/menus/main%20menu', opts: { baseURL: 'http://127.0.0.1:8096/_menu/api/v1', query: { locale: 'pl' }, headers: { 'x-forwarded-for': '203.0.113.7, 10.0.0.2' } } }, + ]) + // A value recorded on the request context (route cache) wins. + const recorded = await useMenuApiAs(true, { context: { menuForwardedFor: '198.51.100.9' }, xff: '1.1.1.1', peer: '10.0.0.2' }) + await recorded.client.getMenu('main') + assert.deepEqual(recorded.calls[0]!.opts.headers, { 'x-forwarded-for': '198.51.100.9' }) + // Nothing to forward: no header at all. + const bare = await useMenuApiAs(true) + await bare.client.getMenu('main') + assert.deepEqual(bare.calls[0]!.opts, { baseURL: 'http://127.0.0.1:8096/_menu/api/v1', query: undefined, headers: {} }) +}) + +test('the public base is used in the browser, without forwarded headers', async () => { + const { client, calls } = await useMenuApiAs(false, { xff: '203.0.113.7', peer: '10.0.0.2' }) + await client.getMenu('main', 'en') + assert.deepEqual(calls, [{ path: '/menus/main', opts: { baseURL: '/_menu/api/v1', query: { locale: 'en' } } }]) +}) diff --git a/tests/menuLinks.test.ts b/tests/menuLinks.test.ts index f6b8bb1..820e21d 100644 --- a/tests/menuLinks.test.ts +++ b/tests/menuLinks.test.ts @@ -135,3 +135,72 @@ test('rendered: an entry with children and a URL keeps its link and gets a label assert.match(html, /