Files
vue-faq-layer/testing/fixture-api.mjs
Jakub Zych 2d62924b78 feat: FaqBlock layer for the SummerCMS FAQ API
- typed client and useFaq/useFaqApi (SSR, faq:<slug>:<locale> key,
  server and public bases, x-forwarded-for forwarded also on cached renders)
- FaqBlock, FaqList and FaqItem: native details, exclusive and openFirst,
  server-sanitized description and answers, nothing or #fallback on 404,
  error or no items
- FAQ tokens and base CSS with the .faq-html rules, README
- node client tests and a playground SSR smoke against a fixture API
2026-10-12 18:22:03 +02:00

68 lines
2.5 KiB
JavaScript

// A small node:http stand-in for the anonymous FAQ API (/_faq/api/v1),
// answering GET /faqs/<slug> from testing/fixtures/faqs/<slug>.json and
// 404 {"error":"FAQ not found"} otherwise. It records every request so a
// smoke run can assert header forwarding.
//
// Plain .mjs with JSDoc types: Node does not strip TypeScript under
// node_modules, and consuming apps may import this file from there.
import { createServer } from 'node:http'
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
const PREFIX = '/_faq/api/v1'
/**
* @typedef {{ method: string, url: string, xff: string | null }} FixtureRequest
* @typedef {{ url: string, requests: FixtureRequest[], close: () => Promise<void> }} FixtureApi
*/
/**
* Starts the fixture API on 127.0.0.1.
* @param {{ dir: string, port?: number }} opts
* @returns {Promise<FixtureApi>}
*/
export function startFixtureApi({ dir, port = 0 }) {
/** @type {FixtureRequest[]} */
const requests = []
/** @param {import('node:http').IncomingMessage} req */
const route = (req) => {
const url = new URL(req.url ?? '/', 'http://fixture.local')
if (req.method !== 'GET' && req.method !== 'HEAD') return { status: 405, body: { error: 'Method not allowed' } }
const m = /^\/faqs\/([a-z0-9][a-z0-9-]*)$/.exec(url.pathname.slice(PREFIX.length))
if (!url.pathname.startsWith(PREFIX + '/') || !m) return { status: 404, body: { error: 'FAQ not found' } }
try {
return { status: 200, body: JSON.parse(readFileSync(join(dir, 'faqs', `${m[1]}.json`), 'utf8')) }
} catch (err) {
if (/** @type {any} */ (err).code === 'ENOENT') return { status: 404, body: { error: 'FAQ not found' } }
throw err
}
}
const server = createServer((req, res) => {
const xff = req.headers['x-forwarded-for']
requests.push({ method: req.method ?? 'GET', url: req.url ?? '/', xff: Array.isArray(xff) ? xff.join(', ') : (xff ?? null) })
let out
try {
out = route(req)
} catch (err) {
out = { status: 500, body: { error: String(err) } }
}
res.writeHead(out.status, { 'content-type': 'application/json' })
res.end(req.method === 'HEAD' ? undefined : JSON.stringify(out.body))
})
return new Promise((resolve, reject) => {
server.once('error', reject)
server.listen(port, '127.0.0.1', () => {
const addr = /** @type {import('node:net').AddressInfo} */ (server.address())
resolve({
url: `http://127.0.0.1:${addr.port}`,
requests,
close: () => new Promise((done) => server.close(() => done())),
})
})
})
}