2026-09-08 17:12:25 +01:00
|
|
|
import { getFlags, FLAGS_REVALIDATE } from '@/lib/flags';
|
2026-08-23 10:55:32 +01:00
|
|
|
|
|
|
|
|
// jsdom provides no global fetch, so there is nothing for jest.spyOn to attach
|
|
|
|
|
// to — assign it and restore the original afterwards. This is the first test
|
|
|
|
|
// here to mock fetch; later ones should follow this shape.
|
|
|
|
|
const realFetch = global.fetch;
|
|
|
|
|
|
|
|
|
|
function mockFetch(impl: () => Promise<unknown>) {
|
|
|
|
|
global.fetch = jest.fn(impl) as unknown as typeof fetch;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('getFlags', () => {
|
|
|
|
|
afterEach(() => { global.fetch = realFetch; });
|
|
|
|
|
|
|
|
|
|
it('returns the flags the API reports', async () => {
|
|
|
|
|
mockFetch(async () => ({
|
|
|
|
|
ok: true,
|
|
|
|
|
json: async () => ({ admission_distance: true }),
|
|
|
|
|
}));
|
|
|
|
|
await expect(getFlags()).resolves.toEqual({ admission_distance: true });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns no flags rather than throwing when the API is down', async () => {
|
|
|
|
|
// A page that cannot read flags must render everything dark, not 500.
|
|
|
|
|
// Fail-closed is the same direction as the backend's default.
|
|
|
|
|
mockFetch(async () => { throw new Error('ECONNREFUSED'); });
|
|
|
|
|
await expect(getFlags()).resolves.toEqual({});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns no flags rather than throwing on a non-200', async () => {
|
|
|
|
|
mockFetch(async () => ({ ok: false, status: 503 }));
|
|
|
|
|
await expect(getFlags()).resolves.toEqual({});
|
|
|
|
|
});
|
2026-09-08 17:12:25 +01:00
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Reading a flag pins the calling route's ISR floor: Next uses the LOWEST
|
|
|
|
|
* revalidate among a route's fetches for the whole route. That is why the
|
|
|
|
|
* revalidate is an argument rather than the constant.
|
|
|
|
|
*
|
|
|
|
|
* Every SEO route here declares `revalidate = 604800`. A gate that read
|
|
|
|
|
* flags at the 300s default would drop the whole school and place corpus
|
|
|
|
|
* from a weekly cache to a 5-minute one, which is a large origin-load
|
|
|
|
|
* regression to pay for a feature flag.
|
|
|
|
|
*/
|
|
|
|
|
it('reads at the 300s floor by default', async () => {
|
|
|
|
|
mockFetch(async () => ({ ok: true, json: async () => ({}) }));
|
|
|
|
|
await getFlags();
|
|
|
|
|
expect((global.fetch as jest.Mock).mock.calls[0][1])
|
|
|
|
|
.toEqual({ next: { revalidate: FLAGS_REVALIDATE } });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('lets a caller pass its own route floor instead', async () => {
|
|
|
|
|
mockFetch(async () => ({ ok: true, json: async () => ({}) }));
|
|
|
|
|
await getFlags(604800);
|
|
|
|
|
expect((global.fetch as jest.Mock).mock.calls[0][1])
|
|
|
|
|
.toEqual({ next: { revalidate: 604800 } });
|
|
|
|
|
});
|
2026-08-23 10:55:32 +01:00
|
|
|
});
|