// Serves the fixture feeds so the daemon under test has something real to scan. import http from 'node:http'; import fs from 'node:fs'; import path from 'node:path'; const dir = import.meta.dirname; const port = Number(process.env.FIXTURE_PORT || 8792); http.createServer((req, res) => { // Jev, for Signals (#254): sure of an item whose text has a question's keyword in it. Refuses // a request in the shape TypeSafe refuses, as it does (#257). if (req.method === 'POST' && req.url === '/jev') { let body = ''; req.on('data', c => { body += c; }); req.on('end', () => { // As TypeSafe answers a key it does not know (#271). if (req.headers.authorization === 'Bearer bad-key') { res.writeHead(401, { 'content-type': 'application/json' }).end('{"detail":"invalid api key"}'); return; } // As ipx asks TypeSafe's Jev: the item, and a question a Signal. const { state, questions } = JSON.parse(body) as { state: unknown, questions: Record }; if (Object.values(questions).some(q => typeof q.criteria !== 'object')) { res.writeHead(422, { 'content-type': 'application/json' }).end('{"detail":"criteria should be an object"}'); return; } const title = String(state).toLowerCase(); const answers: Record = {}; for (const [id, q] of Object.entries(questions)) { // A key's check asks about no Signal's words. const words = /described by these words: ([^.]*)\./.exec(q.instructions)?.[1].split(', ') ?? []; answers[id] = { noul: words.some(w => title.includes(w.toLowerCase())) ? 0.9 : 0.1 }; } res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ answers })); }); return; } // Dated today, since a Signal looks at the last 30 days only. if (req.url === '/signal.xml') { const now = new Date().toUTCString(); const item = (guid: string, title: string) => `${guid}${title}${now}`; res.writeHead(200, { 'content-type': 'application/xml' }).end(` Signal Show${item('sig-1', 'Monaco qualifying')}${item('sig-2', 'Baking bread')}`); return; } // A show to download from by hand (#404): one episode sent slowly enough for its progress to be // seen, and one that always fails. Their files are theirs alone, as enclosure URLs must be. if (req.url === '/live.xml') { const item = (guid: string, title: string, file: string) => `${guid}${title}`; res.writeHead(200, { 'content-type': 'application/xml' }).end(` Live Show${item('live-1', 'Slow episode', 'live-slow.mp3')}${item('live-2', 'Broken episode', 'live-broken.mp3')}`); return; } // Four episodes, newest first, for Download latest and a schedule's window (#406). if (req.url === '/latest.xml') { const item = (n: number) => `latest-${n}Latest ${n}http://127.0.0.1:${port}/latest-${n}.html${new Date(Date.UTC(2026, 0, 10 - n)).toUTCString()}` + ``; res.writeHead(200, { 'content-type': 'application/xml' }).end(` Latest Show${[1, 2, 3, 4].map(item).join('')}`); return; } if (/^\/latest-\d\.mp3$/.test(req.url ?? '')) { const body = fs.readFileSync(path.join(dir, 'ep1.mp3')); res.writeHead(200, { 'content-type': 'audio/mpeg', 'content-length': body.length }).end(body); return; } if (req.url === '/live-broken.mp3') { res.writeHead(500).end('broken'); return; } if (req.url === '/live-slow.mp3') { const body = fs.readFileSync(path.join(dir, 'ep1.mp3')); res.writeHead(200, { 'content-type': 'audio/mpeg', 'content-length': body.length }); // Ten parts, a fifth of a second apart: two seconds of progress. let part = 0; const step = Math.ceil(body.length / 10); const next = () => { if (part * step >= body.length) { res.end(); return; } res.write(body.subarray(part * step, ++part * step)); setTimeout(next, 200); }; next(); return; } const name = decodeURIComponent((req.url ?? '/').split('?')[0]!.replace(/^\//, '')) || 'index'; const file = path.join(dir, path.basename(name)); fs.readFile(file, (err, body) => { if (err) { res.writeHead(404).end('no'); return; } const type = file.endsWith('.mp3') ? 'audio/mpeg' : file.endsWith('.opml') ? 'text/x-opml' // Artwork as an image, or /api/art refuses it as not one. : file.endsWith('.jpg') ? 'image/jpeg' // A picture a browser can draw: the JPEGs are a few bytes that only say they are one. : file.endsWith('.png') ? 'image/png' : file.endsWith('.html') ? 'text/html' : 'application/xml'; res.writeHead(200, { 'content-type': type, 'content-length': body.length }); res.end(body); }); }).listen(port, '127.0.0.1', () => console.log(`fixtures on ${port}`));