mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 09:36:08 +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.
125 lines
4.9 KiB
TypeScript
125 lines
4.9 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { resolvePublicBunnyCdnHostname, resolveServerBunnyCdnHostname } from '@/lib/bunny-cdn';
|
|
|
|
beforeEach(() => {
|
|
// The `unit` project loads no env file, so pin both variables rather than
|
|
// inheriting whatever the shell exports.
|
|
vi.stubEnv('BUNNY_CDN_URL', undefined);
|
|
vi.stubEnv('NEXT_PUBLIC_BUNNY_CDN_URL', undefined);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
describe('resolveServerBunnyCdnHostname', () => {
|
|
it('returns null when neither variable is configured', () => {
|
|
expect(resolveServerBunnyCdnHostname()).toBeNull();
|
|
});
|
|
|
|
it('prefers the server variable over the public one', () => {
|
|
vi.stubEnv('BUNNY_CDN_URL', 'https://server.b-cdn.net');
|
|
vi.stubEnv('NEXT_PUBLIC_BUNNY_CDN_URL', 'https://public.b-cdn.net');
|
|
|
|
expect(resolveServerBunnyCdnHostname()).toBe('server.b-cdn.net');
|
|
});
|
|
|
|
it('falls back to the public variable when the server one is unset', () => {
|
|
vi.stubEnv('NEXT_PUBLIC_BUNNY_CDN_URL', 'https://public.b-cdn.net');
|
|
|
|
expect(resolveServerBunnyCdnHostname()).toBe('public.b-cdn.net');
|
|
});
|
|
|
|
it('falls back to the public variable when the server one is empty', () => {
|
|
vi.stubEnv('BUNNY_CDN_URL', '');
|
|
vi.stubEnv('NEXT_PUBLIC_BUNNY_CDN_URL', 'https://public.b-cdn.net');
|
|
|
|
expect(resolveServerBunnyCdnHostname()).toBe('public.b-cdn.net');
|
|
});
|
|
|
|
it.each([
|
|
['a full https url', 'https://cdn.example.b-cdn.net', 'cdn.example.b-cdn.net'],
|
|
['an http url', 'http://cdn.example.b-cdn.net', 'cdn.example.b-cdn.net'],
|
|
['a url with a path', 'https://cdn.example.b-cdn.net/videos', 'cdn.example.b-cdn.net'],
|
|
['a url with a trailing slash', 'https://cdn.example.b-cdn.net/', 'cdn.example.b-cdn.net'],
|
|
['a url with a query string', 'https://cdn.example.b-cdn.net/?a=1', 'cdn.example.b-cdn.net'],
|
|
['a bare hostname', 'cdn.example.b-cdn.net', 'cdn.example.b-cdn.net'],
|
|
['a bare hostname with a trailing slash', 'cdn.example.b-cdn.net/', 'cdn.example.b-cdn.net'],
|
|
[
|
|
'a scheme-less url written with slashes',
|
|
'//cdn.example.b-cdn.net',
|
|
'//cdn.example.b-cdn.net',
|
|
],
|
|
['surrounding whitespace', ' https://cdn.example.b-cdn.net ', 'cdn.example.b-cdn.net'],
|
|
])('reduces %s to the hostname', (_label, configured, expected) => {
|
|
vi.stubEnv('BUNNY_CDN_URL', configured);
|
|
|
|
expect(resolveServerBunnyCdnHostname()).toBe(expected);
|
|
});
|
|
|
|
it('drops the port from a url that carries one', () => {
|
|
// `URL.hostname` excludes the port, unlike `URL.host`. Callers that compare
|
|
// this value against a request hostname get the bare host, which is what the
|
|
// Bunny CDN always serves on.
|
|
vi.stubEnv('BUNNY_CDN_URL', 'https://cdn.example.b-cdn.net:8443/videos');
|
|
|
|
expect(resolveServerBunnyCdnHostname()).toBe('cdn.example.b-cdn.net');
|
|
});
|
|
|
|
it.each([
|
|
['an empty string', ''],
|
|
['whitespace only', ' '],
|
|
])('returns null for %s', (_label, configured) => {
|
|
vi.stubEnv('BUNNY_CDN_URL', configured);
|
|
|
|
expect(resolveServerBunnyCdnHostname()).toBeNull();
|
|
});
|
|
|
|
it('returns null for a bare host:port, which parses as a url with no hostname', () => {
|
|
// `new URL('localhost:9000')` succeeds with protocol `localhost:` and an
|
|
// empty hostname, so it never reaches the string-stripping fallback.
|
|
vi.stubEnv('BUNNY_CDN_URL', 'localhost:9000');
|
|
|
|
expect(resolveServerBunnyCdnHostname()).toBeNull();
|
|
});
|
|
|
|
it('leaves a path attached when the value has no scheme to parse', () => {
|
|
// The fallback only strips a leading scheme and trailing slashes, so a
|
|
// scheme-less value with a path is returned as-is rather than as a hostname.
|
|
vi.stubEnv('BUNNY_CDN_URL', 'cdn.example.b-cdn.net/videos');
|
|
|
|
expect(resolveServerBunnyCdnHostname()).toBe('cdn.example.b-cdn.net/videos');
|
|
});
|
|
|
|
it('never returns a value carrying a scheme', () => {
|
|
vi.stubEnv('BUNNY_CDN_URL', 'https://cdn.example.b-cdn.net');
|
|
|
|
expect(resolveServerBunnyCdnHostname()).not.toContain('://');
|
|
});
|
|
});
|
|
|
|
describe('resolvePublicBunnyCdnHostname', () => {
|
|
it('returns null when the public variable is unset', () => {
|
|
expect(resolvePublicBunnyCdnHostname()).toBeNull();
|
|
});
|
|
|
|
it('reads only the public variable, ignoring the server-only one', () => {
|
|
// This runs in the browser bundle, where BUNNY_CDN_URL is never inlined.
|
|
vi.stubEnv('BUNNY_CDN_URL', 'https://server.b-cdn.net');
|
|
|
|
expect(resolvePublicBunnyCdnHostname()).toBeNull();
|
|
});
|
|
|
|
it('reduces the configured public url to its hostname', () => {
|
|
vi.stubEnv('NEXT_PUBLIC_BUNNY_CDN_URL', 'https://public.b-cdn.net/videos/');
|
|
|
|
expect(resolvePublicBunnyCdnHostname()).toBe('public.b-cdn.net');
|
|
});
|
|
|
|
it('returns the same hostname as the server resolver when only the public url is set', () => {
|
|
vi.stubEnv('NEXT_PUBLIC_BUNNY_CDN_URL', 'https://public.b-cdn.net');
|
|
|
|
expect(resolvePublicBunnyCdnHostname()).toBe(resolveServerBunnyCdnHostname());
|
|
});
|
|
});
|