- Load the narrator list only when the audio dropdown is opened - Keep today's verse and devotional in memory for an hour (not saved) - Back off for a minute after 'allowance used up' or unreachable; keep 'nothing here' answers - Server shares one upstream call between identical simultaneous requests (nothing stored) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
62 lines
3.1 KiB
JavaScript
62 lines
3.1 KiB
JavaScript
// @vitest-environment node
|
|
import { test, expect, vi } from 'vitest';
|
|
import { resolveIqPath } from './iqbible.js';
|
|
|
|
test('only listed IQ Bible routes are reachable', () => {
|
|
expect(resolveIqPath('commentaries/gill/JHN/3/16')?.key).toBe('/commentaries/gill/JHN/3/16');
|
|
expect(resolveIqPath('/geo/JHN/3')?.cache).toBe(true);
|
|
expect(resolveIqPath('/me')).toBeNull();
|
|
expect(resolveIqPath('/licensing')).toBeNull();
|
|
expect(resolveIqPath('/commentaries/gill/../me')).toBeNull();
|
|
expect(resolveIqPath('/lexicon/greek/strongs/G26')).not.toBeNull();
|
|
});
|
|
|
|
test('lexicon sources and audio routes', () => {
|
|
expect(resolveIqPath('/lexicon/greek/lsj/G0026')).not.toBeNull();
|
|
expect(resolveIqPath('/lexicon/hebrew/bdb/BDB1005')).not.toBeNull();
|
|
expect(resolveIqPath('/audio/eng_kjv/JHN/3')?.cache).toBe(false);
|
|
expect(resolveIqPath('/ai/context', { ref: 'JHN.3.16' })?.heavy).toBe(true);
|
|
});
|
|
|
|
test('the browser cannot send its own key and the query is normalised', () => {
|
|
const r = resolveIqPath('/bibles/eng_kjv/reading-plan', { days: '30', apikey: 'x', 'bad key': '1', format: 'ical' });
|
|
expect(r.key).toBe('/bibles/eng_kjv/reading-plan?days=30&format=ical');
|
|
});
|
|
|
|
test('only verse and chapter text counts as cacheable text', async () => {
|
|
const { isTextPath } = await import('./iqbible.js');
|
|
expect(isTextPath('/bibles/eng_kjv/JHN/3')).toBe(true);
|
|
expect(isTextPath('/bibles/eng_kjv/JHN/3/16')).toBe(true);
|
|
expect(isTextPath('/bibles/eng_kjv/random/verse')).toBe(false);
|
|
expect(isTextPath('/bibles/eng_kjv/verse-of-the-day')).toBe(false);
|
|
expect(isTextPath('/commentaries/gill/JHN/3')).toBe(false);
|
|
expect(resolveIqPath('/bibles/parallel', { v: 'eng_kjv,spa_rv' })).not.toBeNull();
|
|
});
|
|
|
|
test('identical requests made at the same moment share one upstream call, and nothing is kept afterwards', async () => {
|
|
vi.resetModules();
|
|
process.env.IQBIBLE_API_KEY = 'k';
|
|
const realFetch = globalThis.fetch;
|
|
let upstreamCalls = 0;
|
|
vi.stubGlobal('fetch', vi.fn(async (url, opts) => {
|
|
if (String(url).startsWith('http://localhost')) return realFetch(url, opts); // the test's own requests
|
|
upstreamCalls += 1;
|
|
await new Promise((r) => setTimeout(r, 30));
|
|
return new Response(JSON.stringify({ data: 1 }), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
}));
|
|
const { registerIqBibleRoutes } = await import('./iqbible.js');
|
|
const express = (await import('express')).default;
|
|
const app = express();
|
|
app.use((req, _res, next) => { req.session = { userId: 'u1' }; next(); });
|
|
registerIqBibleRoutes(app, (_req, _res, next) => next());
|
|
const srv = await new Promise((resolve) => { const s = app.listen(0, () => resolve(s)); });
|
|
const url = `http://localhost:${srv.address().port}/api/iq/devotionals/today`;
|
|
try {
|
|
const together = await Promise.all([realFetch(url), realFetch(url), realFetch(url)]);
|
|
expect(together.map((r) => r.status)).toEqual([200, 200, 200]);
|
|
expect(upstreamCalls).toBe(1);
|
|
await realFetch(url); // afterwards: not remembered, so it asks again
|
|
expect(upstreamCalls).toBe(2);
|
|
} finally { srv.close(); vi.unstubAllGlobals(); delete process.env.IQBIBLE_API_KEY; }
|
|
});
|