Files
study-bible-project/server/iqbible.test.js
nmemmertandClaude Sonnet 5.5 31ac6b9179 Make fewer IQ Bible calls
- 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>
2026-10-06 13:25:51 -04:00

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; }
});