// The FAQ client against a fake fetcher: the URL built from base, slug and // locale, the envelope unwrapped, malformed items dropped, 404 as null. No // network. import { test } from 'node:test' import assert from 'node:assert/strict' import { createFaqClient, faqItems, faqPath, isNotFound } from '../app/utils/client.ts' type Call = { path: string; query?: Record } const FAQ = { data: { name: 'Shipping', slug: 'shipping', locale: 'en', description: '

About shipping.

', content: [ { question: 'How long?', answer: '

Two days.

' }, { question: 'Where?', answer: '

Everywhere.

' }, ], }, } function fakeFetcher(answer: () => unknown) { const calls: Call[] = [] const fetcher = async (path: string, query?: Record) => { calls.push(query === undefined ? { path } : { path, query }) return answer() } return { fetcher, calls } } test('faqPath encodes the slug', () => { assert.equal(faqPath('shipping'), '/faqs/shipping') assert.equal(faqPath('a b/c'), '/faqs/a%20b%2Fc') }) test('getFaq calls /faqs/ and passes locale only when set', async () => { const { fetcher, calls } = fakeFetcher(() => FAQ) const client = createFaqClient(fetcher) const faq = await client.getFaq('shipping') assert.equal(faq?.name, 'Shipping') assert.equal(faq?.content.length, 2) await client.getFaq('shipping', 'pl') assert.deepEqual(calls, [{ path: '/faqs/shipping' }, { path: '/faqs/shipping', query: { locale: 'pl' } }]) }) test('the fetcher base plus the path is the API URL', async () => { const urls: string[] = [] const base = 'http://127.0.0.1:8096/_faq/api/v1' const client = createFaqClient(async (path, query) => { const u = new URL(base + path) for (const [k, v] of Object.entries(query ?? {})) u.searchParams.set(k, v) urls.push(u.toString()) return FAQ }) await client.getFaq('what-mushrooms-can-give-you', 'pl') assert.deepEqual(urls, ['http://127.0.0.1:8096/_faq/api/v1/faqs/what-mushrooms-can-give-you?locale=pl']) }) test('a 404 resolves to null; other errors are thrown', async () => { const notFound = createFaqClient(async () => { throw Object.assign(new Error('Not Found'), { response: { status: 404 } }) }) assert.equal(await notFound.getFaq('nope'), null) const failing = createFaqClient(async () => { throw Object.assign(new Error('boom'), { statusCode: 502 }) }) await assert.rejects(failing.getFaq('x'), /boom/) }) test('a body without data is null; a null description stays null', async () => { assert.equal(await createFaqClient(async () => ({})).getFaq('x'), null) const faq = await createFaqClient(async () => ({ data: { ...FAQ.data, description: null } })).getFaq('x') assert.equal(faq?.description, null) }) test('faqItems keeps only items with a string question and answer', () => { assert.deepEqual(faqItems(null), []) assert.deepEqual(faqItems({}), []) assert.deepEqual(faqItems([{ question: 'Q', answer: 'A' }, { question: 'Q' }, null, 'x', { question: 1, answer: 'A' }]), [ { question: 'Q', answer: 'A' }, ]) }) test('isNotFound reads status, statusCode and response.status', () => { assert.equal(isNotFound({ status: 404 }), true) assert.equal(isNotFound({ statusCode: 404 }), true) assert.equal(isNotFound({ response: { status: 404 } }), true) assert.equal(isNotFound({ status: 500 }), false) assert.equal(isNotFound(undefined), false) }) // Plan 06 coverage: the encoded slug and locale, non-string descriptions, // and useFaqApi picking 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' test('the slug is encoded, the locale passed as it is', async () => { const { fetcher, calls } = fakeFetcher(() => FAQ) await createFaqClient(fetcher).getFaq('a b?c#d', 'pt-BR') assert.deepEqual(calls, [{ path: '/faqs/a%20b%3Fc%23d', query: { locale: 'pt-BR' } }]) // An empty locale is no locale. await createFaqClient(fetcher).getFaq('x', '') assert.deepEqual(calls[1], { path: '/faqs/x' }) }) test('a description that is not a string is null; content that is not a list is empty', async () => { const faq = await createFaqClient(async () => ({ data: { ...FAQ.data, description: 5, content: 'x' } })).getFaq('x') assert.equal(faq?.description, null) assert.deepEqual(faq?.content, []) }) type FetchCall = { path: string; opts: { baseURL: string; query?: Record; headers?: Record } } let loads = 0 async function useFaqApiAs(server: boolean, request: { context?: Record; xff?: string; peer?: string } = {}) { const calls: FetchCall[] = [] const g = globalThis as Record g.__faqLayerServer = server g.useRuntimeConfig = () => ({ faq: { apiBase: 'http://127.0.0.1:8096/_faq/api/v1' }, public: { faq: { apiBase: '/_faq/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 FAQ } const source = join(dirname(fileURLToPath(import.meta.url)), '..', 'app', 'composables', 'useFaqApi.ts') const out = join(dirname(source), `.useFaqApi-${process.pid}-${++loads}.compiled.ts`) writeFileSync(out, readFileSync(source, 'utf8').replaceAll('import.meta.server', '(globalThis as Record).__faqLayerServer')) try { const mod = (await import(pathToFileURL(out).href)) as { useFaqApi: () => ReturnType } return { client: mod.useFaqApi(), calls } } finally { rmSync(out, { force: true }) } } test('the server base is used on the server, with the visitor forwarded', async () => { const { client, calls } = await useFaqApiAs(true, { xff: '203.0.113.7', peer: '10.0.0.2' }) await client.getFaq('shipping', 'pl') assert.deepEqual(calls, [ { path: '/faqs/shipping', opts: { baseURL: 'http://127.0.0.1:8096/_faq/api/v1', query: { locale: 'pl' }, headers: { 'x-forwarded-for': '203.0.113.7, 10.0.0.2' } } }, ]) const recorded = await useFaqApiAs(true, { context: { faqForwardedFor: '198.51.100.9' }, xff: '1.1.1.1', peer: '10.0.0.2' }) await recorded.client.getFaq('shipping') assert.deepEqual(recorded.calls[0]!.opts.headers, { 'x-forwarded-for': '198.51.100.9' }) const bare = await useFaqApiAs(true) await bare.client.getFaq('shipping') assert.deepEqual(bare.calls[0]!.opts, { baseURL: 'http://127.0.0.1:8096/_faq/api/v1', query: undefined, headers: {} }) }) test('the public base is used in the browser, without forwarded headers', async () => { const { client, calls } = await useFaqApiAs(false, { xff: '203.0.113.7', peer: '10.0.0.2' }) await client.getFaq('shipping', 'en') assert.deepEqual(calls, [{ path: '/faqs/shipping', opts: { baseURL: '/_faq/api/v1', query: { locale: 'en' } } }]) })