feat: vue-seo-layer, the Nuxt half of the SummerCMS SEO plugin
This commit is contained in:
114
testing/fixture-api.mjs
Normal file
114
testing/fixture-api.mjs
Normal file
@@ -0,0 +1,114 @@
|
||||
// A small node:http stand-in for the SEO plugin's public API
|
||||
// (/_seo/api/v1):
|
||||
//
|
||||
// GET /resolve?url=… the answer for that exact url from
|
||||
// <dir>/resolve.json, else not_found
|
||||
// POST /notfound 204 with `Authorization: Bearer <token>`,
|
||||
// 401 with another token, 404 without one
|
||||
// GET /verification/<name> <dir>/verification/<name>, else 404
|
||||
//
|
||||
// It records every request (with the parsed JSON body of a POST) so a smoke
|
||||
// run can assert header forwarding and the 404 reports.
|
||||
//
|
||||
// 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 = '/_seo/api/v1'
|
||||
|
||||
/** The bearer token the fixture accepts on POST /notfound. */
|
||||
export const FIXTURE_NOTFOUND_TOKEN = 'fixture-seo-notfound-token-0123456789'
|
||||
|
||||
/**
|
||||
* @typedef {{ method: string, url: string, xff: string | null, authorization: string | null, body: unknown }} FixtureRequest
|
||||
* @typedef {{ url: string, requests: FixtureRequest[], close: () => Promise<void> }} FixtureApi
|
||||
*/
|
||||
|
||||
/**
|
||||
* Starts the fixture API on 127.0.0.1.
|
||||
* @param {{ dir: string, port?: number, token?: string }} opts
|
||||
* @returns {Promise<FixtureApi>}
|
||||
*/
|
||||
export function startFixtureApi({ dir, port = 0, token = FIXTURE_NOTFOUND_TOKEN }) {
|
||||
/** @type {FixtureRequest[]} */
|
||||
const requests = []
|
||||
/** @type {Record<string, unknown>} */
|
||||
const answers = JSON.parse(readFileSync(join(dir, 'resolve.json'), 'utf8'))
|
||||
|
||||
/**
|
||||
* @param {import('node:http').IncomingMessage} req
|
||||
* @returns {{ status: number, body?: unknown, text?: string, type?: string }}
|
||||
*/
|
||||
const route = (req) => {
|
||||
const url = new URL(req.url ?? '/', 'http://fixture.local')
|
||||
if (!url.pathname.startsWith(PREFIX + '/')) return { status: 404, body: { error: 'not_found' } }
|
||||
const path = url.pathname.slice(PREFIX.length)
|
||||
if (path === '/resolve' && req.method === 'GET') {
|
||||
const target = url.searchParams.get('url') ?? ''
|
||||
if (!target.startsWith('/')) return { status: 422, body: { error: 'bad_url' } }
|
||||
const hit = answers[target]
|
||||
if (hit) return { status: 200, body: hit }
|
||||
return { status: 200, body: { action: 'not_found', status: 404, url: target, locale: '', staging: false, seo: null } }
|
||||
}
|
||||
if (path === '/notfound' && req.method === 'POST') {
|
||||
if (!token) return { status: 404, body: { error: 'not_found' } }
|
||||
return req.headers.authorization === `Bearer ${token}` ? { status: 204 } : { status: 401, body: { error: 'unauthorized' } }
|
||||
}
|
||||
const v = /^\/verification\/([A-Za-z0-9._-]+)$/.exec(path)
|
||||
if (v && req.method === 'GET') {
|
||||
try {
|
||||
return { status: 200, text: readFileSync(join(dir, 'verification', v[1]), 'utf8'), type: 'text/html; charset=utf-8' }
|
||||
} catch {
|
||||
return { status: 404, body: { error: 'not_found' } }
|
||||
}
|
||||
}
|
||||
return { status: 404, body: { error: 'not_found' } }
|
||||
}
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
const chunks = /** @type {Buffer[]} */ ([])
|
||||
req.on('data', (c) => chunks.push(c))
|
||||
req.on('end', () => {
|
||||
const raw = Buffer.concat(chunks).toString('utf8')
|
||||
let body = null
|
||||
try {
|
||||
body = raw ? JSON.parse(raw) : null
|
||||
} catch {
|
||||
body = raw
|
||||
}
|
||||
const header = (/** @type {string} */ name) => {
|
||||
const v = req.headers[name]
|
||||
return Array.isArray(v) ? v.join(', ') : (v ?? null)
|
||||
}
|
||||
requests.push({ method: req.method ?? 'GET', url: req.url ?? '/', xff: header('x-forwarded-for'), authorization: header('authorization'), body })
|
||||
let out
|
||||
try {
|
||||
out = route(req)
|
||||
} catch (err) {
|
||||
out = { status: 500, body: { error: String(err) } }
|
||||
}
|
||||
if (out.text !== undefined) {
|
||||
res.writeHead(out.status, { 'content-type': out.type ?? 'text/plain' })
|
||||
res.end(out.text)
|
||||
return
|
||||
}
|
||||
res.writeHead(out.status, out.body === undefined ? {} : { 'content-type': 'application/json' })
|
||||
res.end(out.body === undefined ? 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())),
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user