mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
Second pass over the suite, driven by the inventory in the gaps document. Nine agents wrote suites in parallel against private databases, then a tenth read all of it adversarially and five of its findings were fixed. unit + component 2076 -> 2079 (+888 over the round) api 647 -> 1015 e2e 18 -> 29 What was closed: - lib/route-access.ts, the page-level authorization layer, went from zero tests to 48. Every API route was guarded and none of the pages were. - The five media proxy routes now have a real 2xx beside every 403. The blocker was the positive control, solved by stubbing r2Client.send() and leaving lib/r2-media-proxy.ts itself real. - Every remaining server-side lib module: invitations, email verification, the upload tokens, the logger, request origin, the whole R2 and Bunny lifecycle, notifications and admin stats. - Six video-page hooks, and the chunking arithmetic extracted out of lib/client/r2-video-upload.ts as a pure module. - Five end-to-end flows: workspace members, bulk operations, the admin area, player interaction and failure recovery. Three things about the harness itself turned out to be wrong: - Two @/lib/r2 stubs in tests/setup/api.ts had the wrong return shape, so every route reaching finalizeR2VideoUpload silently took the "not a valid video" branch and no test noticed. - The auth matrix asserted only "not 2xx", which two entries satisfied without their guard existing. It now requires 401 or 403, which makes both load-bearing, and all 60 routes pass the stricter form. - Both admin API routes had no positive control anywhere: replacing their guard with an unconditional refusal left the entire suite green. Found by the adversarial review, now covered. Process: - bun run test:mutation runs StrykerJS over the authorization and validation modules. Diagnostic, not a gate, weekly in CI rather than on a push. - playwright.config.ts gains an opt-in webkit project for the player spec. - AGENTS.md now requires a batch of new tests to be reviewed by somebody who did not write them. Only two production files change, both deliberate: lib/auth.ts loses a verbatim copy of its own permission formulas, and lib/client/r2-video-upload.ts calls the extracted arithmetic. No behaviour change in either.
479 lines
16 KiB
TypeScript
479 lines
16 KiB
TypeScript
// Bunny download source resolution. Everything the module decides is a
|
|
// function of what the CDN answers to a HEAD, so `fetch` is the only boundary
|
|
// stubbed here. Urls are asserted in full because they are fully deterministic;
|
|
// nothing in this module is signed.
|
|
//
|
|
// The module keeps a 60 second in-process cache keyed on
|
|
// videoId:quality:preference, so every test uses its own video id unless it is
|
|
// deliberately exercising the cache.
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
fetchWithTimeout,
|
|
resolveBunnyCdnHostname,
|
|
resolveBunnyDownloadSource,
|
|
} from '@/lib/bunny-download';
|
|
|
|
const HOST = 'cdn.example.b-cdn.net';
|
|
|
|
type FetchCall = [string, RequestInit];
|
|
|
|
let fetchMock: ReturnType<typeof vi.fn>;
|
|
|
|
function ok(body = ''): Response {
|
|
return { ok: true, status: 200, text: async () => body } as unknown as Response;
|
|
}
|
|
|
|
function notFound(): Response {
|
|
return { ok: false, status: 404, text: async () => '' } as unknown as Response;
|
|
}
|
|
|
|
/**
|
|
* Answer 200 for the listed urls and 404 for everything else. `playlist` is
|
|
* served as the body of any playlist.m3u8 request.
|
|
*/
|
|
function stubCdn(available: string[], playlist?: string): void {
|
|
fetchMock.mockImplementation(async (url: string) => {
|
|
if (url.endsWith('/playlist.m3u8')) {
|
|
return playlist === undefined ? notFound() : ok(playlist);
|
|
}
|
|
return available.includes(url) ? ok() : notFound();
|
|
});
|
|
}
|
|
|
|
function requestedUrls(): string[] {
|
|
return (fetchMock.mock.calls as FetchCall[]).map((call) => call[0]);
|
|
}
|
|
|
|
beforeEach(() => {
|
|
fetchMock = vi.fn(async () => notFound());
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
vi.stubEnv('BUNNY_CDN_URL', `https://${HOST}`);
|
|
vi.stubEnv('NEXT_PUBLIC_BUNNY_CDN_URL', undefined);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
vi.unstubAllGlobals();
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
describe('resolveBunnyCdnHostname', () => {
|
|
it('reduces a configured url to its hostname', () => {
|
|
expect(resolveBunnyCdnHostname()).toBe(HOST);
|
|
});
|
|
|
|
it('drops a path and a trailing slash', () => {
|
|
vi.stubEnv('BUNNY_CDN_URL', `https://${HOST}/some/path/`);
|
|
|
|
expect(resolveBunnyCdnHostname()).toBe(HOST);
|
|
});
|
|
|
|
it('accepts a bare hostname with no scheme', () => {
|
|
vi.stubEnv('BUNNY_CDN_URL', `${HOST}/`);
|
|
|
|
expect(resolveBunnyCdnHostname()).toBe(HOST);
|
|
});
|
|
|
|
it('falls back to the public variable', () => {
|
|
vi.stubEnv('BUNNY_CDN_URL', undefined);
|
|
vi.stubEnv('NEXT_PUBLIC_BUNNY_CDN_URL', `https://${HOST}`);
|
|
|
|
expect(resolveBunnyCdnHostname()).toBe(HOST);
|
|
});
|
|
|
|
it('returns null when neither variable is set', () => {
|
|
vi.stubEnv('BUNNY_CDN_URL', undefined);
|
|
|
|
expect(resolveBunnyCdnHostname()).toBeNull();
|
|
});
|
|
|
|
it('returns null for a blank value rather than an empty hostname', () => {
|
|
vi.stubEnv('BUNNY_CDN_URL', ' ');
|
|
|
|
expect(resolveBunnyCdnHostname()).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('with no CDN configured', () => {
|
|
it('resolves to null without making a request', async () => {
|
|
vi.stubEnv('BUNNY_CDN_URL', undefined);
|
|
|
|
await expect(resolveBunnyDownloadSource('vid-nohost', null, 'auto')).resolves.toBeNull();
|
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('the original preference', () => {
|
|
it('returns the original url when the CDN has one', async () => {
|
|
stubCdn([`https://${HOST}/vid-orig-1/original`]);
|
|
|
|
await expect(resolveBunnyDownloadSource('vid-orig-1', null, 'original')).resolves.toEqual({
|
|
sourceType: 'original',
|
|
quality: null,
|
|
url: `https://${HOST}/vid-orig-1/original`,
|
|
});
|
|
});
|
|
|
|
// Asking for the original explicitly means the caller wants the master file
|
|
// or nothing; falling back to a transcode would silently hand back a
|
|
// lower-quality file under the same name.
|
|
it('returns null rather than a transcode when the original is absent', async () => {
|
|
stubCdn([`https://${HOST}/vid-orig-2/play_1080p.mp4`]);
|
|
|
|
await expect(resolveBunnyDownloadSource('vid-orig-2', null, 'original')).resolves.toBeNull();
|
|
expect(requestedUrls()).toEqual([`https://${HOST}/vid-orig-2/original`]);
|
|
});
|
|
|
|
it('probes with a HEAD that bypasses the cache', async () => {
|
|
stubCdn([`https://${HOST}/vid-orig-3/original`]);
|
|
|
|
await resolveBunnyDownloadSource('vid-orig-3', null, 'original');
|
|
|
|
expect(fetchMock.mock.calls[0][1]).toMatchObject({ method: 'HEAD', cache: 'no-store' });
|
|
});
|
|
});
|
|
|
|
describe('the compressed preference', () => {
|
|
it('never asks for the original', async () => {
|
|
stubCdn([`https://${HOST}/vid-comp-1/original`, `https://${HOST}/vid-comp-1/play_720p.mp4`]);
|
|
|
|
const source = await resolveBunnyDownloadSource('vid-comp-1', null, 'compressed');
|
|
|
|
expect(source?.sourceType).toBe('compressed');
|
|
expect(requestedUrls().some((url) => url.endsWith('/original'))).toBe(false);
|
|
});
|
|
|
|
it('uses the requested quality when that rendition exists', async () => {
|
|
stubCdn([
|
|
`https://${HOST}/vid-comp-2/play_720p.mp4`,
|
|
`https://${HOST}/vid-comp-2/play_1080p.mp4`,
|
|
]);
|
|
|
|
await expect(resolveBunnyDownloadSource('vid-comp-2', 720, 'compressed')).resolves.toEqual({
|
|
sourceType: 'compressed',
|
|
quality: 720,
|
|
url: `https://${HOST}/vid-comp-2/play_720p.mp4`,
|
|
});
|
|
});
|
|
|
|
it('falls back to the highest available rendition when the requested one is missing', async () => {
|
|
stubCdn([`https://${HOST}/vid-comp-3/play_480p.mp4`]);
|
|
|
|
await expect(resolveBunnyDownloadSource('vid-comp-3', 1080, 'compressed')).resolves.toEqual({
|
|
sourceType: 'compressed',
|
|
quality: 480,
|
|
url: `https://${HOST}/vid-comp-3/play_480p.mp4`,
|
|
});
|
|
});
|
|
|
|
it.each([0, -720, Number.NaN])(
|
|
'ignores a requested quality of %s and goes straight to the fallback',
|
|
async (quality) => {
|
|
stubCdn([`https://${HOST}/vid-comp-q${quality}/play_360p.mp4`]);
|
|
|
|
const source = await resolveBunnyDownloadSource(
|
|
`vid-comp-q${quality}`,
|
|
quality,
|
|
'compressed'
|
|
);
|
|
|
|
expect(source?.quality).toBe(360);
|
|
expect(requestedUrls().some((url) => url.includes(`play_${quality}p`))).toBe(false);
|
|
}
|
|
);
|
|
|
|
it('reports an empty url when nothing is available at all', async () => {
|
|
stubCdn([]);
|
|
|
|
await expect(resolveBunnyDownloadSource('vid-comp-4', null, 'compressed')).resolves.toEqual({
|
|
sourceType: 'compressed',
|
|
quality: null,
|
|
url: '',
|
|
});
|
|
});
|
|
|
|
it('walks the fallback ladder from highest to lowest', async () => {
|
|
stubCdn([]);
|
|
|
|
await resolveBunnyDownloadSource('vid-comp-5', null, 'compressed');
|
|
|
|
expect(requestedUrls()).toEqual([
|
|
`https://${HOST}/vid-comp-5/playlist.m3u8`,
|
|
`https://${HOST}/vid-comp-5/play_2160p.mp4`,
|
|
`https://${HOST}/vid-comp-5/play_1440p.mp4`,
|
|
`https://${HOST}/vid-comp-5/play_1080p.mp4`,
|
|
`https://${HOST}/vid-comp-5/play_720p.mp4`,
|
|
`https://${HOST}/vid-comp-5/play_480p.mp4`,
|
|
`https://${HOST}/vid-comp-5/play_360p.mp4`,
|
|
`https://${HOST}/vid-comp-5/play_240p.mp4`,
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('the playlist hint', () => {
|
|
it('tries the heights the playlist advertises before the static ladder', async () => {
|
|
stubCdn(
|
|
[`https://${HOST}/vid-pl-1/play_720p.mp4`],
|
|
'#EXTM3U\n#EXT-X-STREAM-INF:RESOLUTION=1280x720\n720.m3u8\n'
|
|
);
|
|
|
|
const source = await resolveBunnyDownloadSource('vid-pl-1', null, 'compressed');
|
|
|
|
expect(source?.url).toBe(`https://${HOST}/vid-pl-1/play_720p.mp4`);
|
|
// 720 came from the playlist, so it is probed before 2160.
|
|
expect(requestedUrls()[1]).toBe(`https://${HOST}/vid-pl-1/play_720p.mp4`);
|
|
});
|
|
|
|
it('sorts the advertised heights from highest to lowest', async () => {
|
|
stubCdn(
|
|
[],
|
|
'#EXT-X-STREAM-INF:RESOLUTION=640x360\na\n#EXT-X-STREAM-INF:RESOLUTION=1920x1080\nb\n'
|
|
);
|
|
|
|
await resolveBunnyDownloadSource('vid-pl-2', null, 'compressed');
|
|
|
|
expect(requestedUrls().slice(1, 3)).toEqual([
|
|
`https://${HOST}/vid-pl-2/play_1080p.mp4`,
|
|
`https://${HOST}/vid-pl-2/play_360p.mp4`,
|
|
]);
|
|
});
|
|
|
|
it('ignores an advertised height that is not a Bunny rendition', async () => {
|
|
stubCdn([], '#EXT-X-STREAM-INF:RESOLUTION=1600x900\na\n');
|
|
|
|
await resolveBunnyDownloadSource('vid-pl-3', null, 'compressed');
|
|
|
|
expect(requestedUrls().some((url) => url.includes('play_900p'))).toBe(false);
|
|
expect(requestedUrls()[1]).toBe(`https://${HOST}/vid-pl-3/play_2160p.mp4`);
|
|
});
|
|
|
|
it('does not probe a playlist height twice when the ladder repeats it', async () => {
|
|
stubCdn([], '#EXT-X-STREAM-INF:RESOLUTION=1920x1080\na\n');
|
|
|
|
await resolveBunnyDownloadSource('vid-pl-4', null, 'compressed');
|
|
|
|
const probes = requestedUrls().filter((url) => url.includes('play_1080p'));
|
|
expect(probes).toHaveLength(1);
|
|
});
|
|
|
|
it('falls back to the static ladder when the playlist request fails', async () => {
|
|
fetchMock.mockImplementation(async (url: string) => {
|
|
if (url.endsWith('/playlist.m3u8')) throw new Error('connection reset');
|
|
return url.endsWith('play_1440p.mp4') ? ok() : notFound();
|
|
});
|
|
|
|
const source = await resolveBunnyDownloadSource('vid-pl-5', null, 'compressed');
|
|
|
|
expect(source?.quality).toBe(1440);
|
|
});
|
|
});
|
|
|
|
describe('the auto preference', () => {
|
|
it('prefers the original when the CDN has one', async () => {
|
|
stubCdn([`https://${HOST}/vid-auto-1/original`, `https://${HOST}/vid-auto-1/play_1080p.mp4`]);
|
|
|
|
const source = await resolveBunnyDownloadSource('vid-auto-1', null, 'auto');
|
|
|
|
expect(source).toEqual({
|
|
sourceType: 'original',
|
|
quality: null,
|
|
url: `https://${HOST}/vid-auto-1/original`,
|
|
});
|
|
expect(requestedUrls()).toEqual([`https://${HOST}/vid-auto-1/original`]);
|
|
});
|
|
|
|
it('falls through to a transcode when there is no original', async () => {
|
|
stubCdn([`https://${HOST}/vid-auto-2/play_1080p.mp4`]);
|
|
|
|
await expect(resolveBunnyDownloadSource('vid-auto-2', null, 'auto')).resolves.toEqual({
|
|
sourceType: 'compressed',
|
|
quality: 1080,
|
|
url: `https://${HOST}/vid-auto-2/play_1080p.mp4`,
|
|
});
|
|
});
|
|
|
|
it('honours the requested quality on the fall-through path', async () => {
|
|
stubCdn([
|
|
`https://${HOST}/vid-auto-3/play_480p.mp4`,
|
|
`https://${HOST}/vid-auto-3/play_1080p.mp4`,
|
|
]);
|
|
|
|
const source = await resolveBunnyDownloadSource('vid-auto-3', 480, 'auto');
|
|
|
|
expect(source?.url).toBe(`https://${HOST}/vid-auto-3/play_480p.mp4`);
|
|
});
|
|
});
|
|
|
|
describe('availability probing', () => {
|
|
it('retries with a ranged GET when the CDN refuses HEAD', async () => {
|
|
fetchMock.mockImplementation(async (url: string, init: RequestInit) => {
|
|
if (init.method === 'HEAD') return { ok: false, status: 405 } as unknown as Response;
|
|
return { ok: false, status: 206 } as unknown as Response;
|
|
});
|
|
|
|
const source = await resolveBunnyDownloadSource('vid-probe-1', null, 'original');
|
|
|
|
expect(source?.url).toBe(`https://${HOST}/vid-probe-1/original`);
|
|
expect(fetchMock.mock.calls[1][1]).toMatchObject({
|
|
method: 'GET',
|
|
headers: { Range: 'bytes=0-0' },
|
|
cache: 'no-store',
|
|
});
|
|
});
|
|
|
|
it('accepts a plain 200 on the ranged retry too', async () => {
|
|
fetchMock.mockImplementation(async (_url: string, init: RequestInit) =>
|
|
init.method === 'HEAD'
|
|
? ({ ok: false, status: 405 } as unknown as Response)
|
|
: ({ ok: true, status: 200 } as unknown as Response)
|
|
);
|
|
|
|
const source = await resolveBunnyDownloadSource('vid-probe-2', null, 'original');
|
|
|
|
expect(source).not.toBeNull();
|
|
});
|
|
|
|
it('treats the file as absent when the ranged retry also fails', async () => {
|
|
fetchMock.mockImplementation(async (_url: string, init: RequestInit) =>
|
|
init.method === 'HEAD'
|
|
? ({ ok: false, status: 405 } as unknown as Response)
|
|
: ({ ok: false, status: 403 } as unknown as Response)
|
|
);
|
|
|
|
await expect(resolveBunnyDownloadSource('vid-probe-3', null, 'original')).resolves.toBeNull();
|
|
});
|
|
|
|
it('does not retry a status other than 405', async () => {
|
|
fetchMock.mockResolvedValue({ ok: false, status: 403 } as unknown as Response);
|
|
|
|
await resolveBunnyDownloadSource('vid-probe-4', null, 'original');
|
|
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('treats a rejected probe as absent rather than propagating it', async () => {
|
|
fetchMock.mockRejectedValue(new Error('dns failure'));
|
|
|
|
await expect(resolveBunnyDownloadSource('vid-probe-5', null, 'original')).resolves.toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('the resolution cache', () => {
|
|
it('serves a repeat lookup without touching the CDN again', async () => {
|
|
stubCdn([`https://${HOST}/vid-cache-1/original`]);
|
|
|
|
const first = await resolveBunnyDownloadSource('vid-cache-1', null, 'auto');
|
|
const callsAfterFirst = fetchMock.mock.calls.length;
|
|
const second = await resolveBunnyDownloadSource('vid-cache-1', null, 'auto');
|
|
|
|
expect(second).toEqual(first);
|
|
expect(fetchMock).toHaveBeenCalledTimes(callsAfterFirst);
|
|
});
|
|
|
|
it('caches a negative result so a missing original is not re-probed', async () => {
|
|
stubCdn([]);
|
|
|
|
await resolveBunnyDownloadSource('vid-cache-2', null, 'original');
|
|
const callsAfterFirst = fetchMock.mock.calls.length;
|
|
await expect(resolveBunnyDownloadSource('vid-cache-2', null, 'original')).resolves.toBeNull();
|
|
|
|
expect(fetchMock).toHaveBeenCalledTimes(callsAfterFirst);
|
|
});
|
|
|
|
it('keys the cache on the preference', async () => {
|
|
stubCdn([`https://${HOST}/vid-cache-3/play_720p.mp4`]);
|
|
|
|
await resolveBunnyDownloadSource('vid-cache-3', null, 'original');
|
|
const callsAfterFirst = fetchMock.mock.calls.length;
|
|
await resolveBunnyDownloadSource('vid-cache-3', null, 'compressed');
|
|
|
|
expect(fetchMock.mock.calls.length).toBeGreaterThan(callsAfterFirst);
|
|
});
|
|
|
|
it('keys the cache on the requested quality', async () => {
|
|
stubCdn([
|
|
`https://${HOST}/vid-cache-4/play_720p.mp4`,
|
|
`https://${HOST}/vid-cache-4/play_1080p.mp4`,
|
|
]);
|
|
|
|
const low = await resolveBunnyDownloadSource('vid-cache-4', 720, 'compressed');
|
|
const high = await resolveBunnyDownloadSource('vid-cache-4', 1080, 'compressed');
|
|
|
|
expect(low?.quality).toBe(720);
|
|
expect(high?.quality).toBe(1080);
|
|
});
|
|
|
|
it('re-probes once the sixty second window has passed', async () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date('2026-01-15T00:00:00.000Z'));
|
|
stubCdn([`https://${HOST}/vid-cache-5/original`]);
|
|
|
|
await resolveBunnyDownloadSource('vid-cache-5', null, 'original');
|
|
const callsAfterFirst = fetchMock.mock.calls.length;
|
|
|
|
vi.setSystemTime(new Date('2026-01-15T00:01:00.001Z'));
|
|
await resolveBunnyDownloadSource('vid-cache-5', null, 'original');
|
|
|
|
expect(fetchMock.mock.calls.length).toBeGreaterThan(callsAfterFirst);
|
|
});
|
|
|
|
it('still serves from the cache one millisecond before expiry', async () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date('2026-01-15T00:00:00.000Z'));
|
|
stubCdn([`https://${HOST}/vid-cache-6/original`]);
|
|
|
|
await resolveBunnyDownloadSource('vid-cache-6', null, 'original');
|
|
const callsAfterFirst = fetchMock.mock.calls.length;
|
|
|
|
vi.setSystemTime(new Date('2026-01-15T00:00:59.999Z'));
|
|
await resolveBunnyDownloadSource('vid-cache-6', null, 'original');
|
|
|
|
expect(fetchMock).toHaveBeenCalledTimes(callsAfterFirst);
|
|
});
|
|
});
|
|
|
|
describe('fetchWithTimeout', () => {
|
|
it('passes an abort signal through to fetch', async () => {
|
|
fetchMock.mockResolvedValue(ok());
|
|
|
|
await fetchWithTimeout('https://example.com/a', { method: 'HEAD' });
|
|
|
|
const init = (fetchMock.mock.calls[0] as FetchCall)[1];
|
|
expect(init.method).toBe('HEAD');
|
|
expect(init.signal).toBeInstanceOf(AbortSignal);
|
|
});
|
|
|
|
it('aborts a request that has not answered within eight seconds', async () => {
|
|
vi.useFakeTimers();
|
|
let signal: AbortSignal | undefined;
|
|
// Never settles, so the only thing that can end the request is the timeout.
|
|
fetchMock.mockImplementation((_url: string, init: RequestInit) => {
|
|
signal = init.signal ?? undefined;
|
|
return new Promise(() => {});
|
|
});
|
|
|
|
void fetchWithTimeout('https://example.com/slow', {});
|
|
|
|
await vi.advanceTimersByTimeAsync(7999);
|
|
expect(signal?.aborted).toBe(false);
|
|
|
|
await vi.advanceTimersByTimeAsync(1);
|
|
expect(signal?.aborted).toBe(true);
|
|
});
|
|
|
|
it('does not abort a request that answered in time', async () => {
|
|
vi.useFakeTimers();
|
|
let signal: AbortSignal | undefined;
|
|
fetchMock.mockImplementation(async (_url: string, init: RequestInit) => {
|
|
signal = init.signal ?? undefined;
|
|
return ok();
|
|
});
|
|
|
|
await fetchWithTimeout('https://example.com/fast', {});
|
|
await vi.advanceTimersByTimeAsync(20_000);
|
|
|
|
expect(signal?.aborted).toBe(false);
|
|
});
|
|
});
|