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.
100 lines
4.1 KiB
TypeScript
100 lines
4.1 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import { resolveWorkspacePermissions } from '@/lib/auth';
|
|
|
|
// `@/lib/auth` reaches `@/lib/db`, which opens a pg Pool and registers process
|
|
// signal handlers on import. resolveWorkspacePermissions touches no database.
|
|
vi.mock('@/lib/db', () => ({ db: {}, default: {}, disconnectDb: vi.fn() }));
|
|
|
|
// The workspace half of the permission matrix. Until this file existed the
|
|
// formulas were only ever reached through checkWorkspaceAccess(), which means
|
|
// they were asserted on incidentally by whichever route a suite happened to
|
|
// call. tests/unit/lib/project-access.test.ts does the same job for projects.
|
|
//
|
|
// Every case below states the expected verdict outright rather than deriving it
|
|
// from the inputs, so a change to the formula cannot quietly change the
|
|
// expectation with it.
|
|
|
|
// There is deliberately no separate 'anonymous' actor. resolveWorkspacePermissions
|
|
// receives three booleans, not a user, and an anonymous caller and a signed-in
|
|
// outsider set all three to false, so the two would be byte-identical inputs
|
|
// running under names that imply a distinction this function cannot see. Telling
|
|
// "no session" from "a session with no membership" is checkWorkspaceAccess()'s
|
|
// job: it is the one that resolves a userId to membership rows before calling
|
|
// here, and it is covered against the database in the api suites.
|
|
type Actor = 'outsider' | 'member' | 'admin' | 'owner';
|
|
|
|
function inputsFor(actor: Actor, ownerBillingActive: boolean) {
|
|
return {
|
|
isOwner: actor === 'owner',
|
|
isMember: actor === 'member' || actor === 'admin',
|
|
isAdmin: actor === 'admin',
|
|
ownerBillingActive,
|
|
};
|
|
}
|
|
|
|
describe('resolveWorkspacePermissions, with the owner billing active', () => {
|
|
const cases: Array<{
|
|
actor: Actor;
|
|
hasAccess: boolean;
|
|
canEdit: boolean;
|
|
canDelete: boolean;
|
|
}> = [
|
|
{ actor: 'outsider', hasAccess: false, canEdit: false, canDelete: false },
|
|
{ actor: 'member', hasAccess: true, canEdit: false, canDelete: false },
|
|
{ actor: 'admin', hasAccess: true, canEdit: true, canDelete: false },
|
|
{ actor: 'owner', hasAccess: true, canEdit: true, canDelete: true },
|
|
];
|
|
|
|
for (const { actor, hasAccess, canEdit, canDelete } of cases) {
|
|
it(`grants a ${actor} access=${hasAccess}, edit=${canEdit}, delete=${canDelete}`, () => {
|
|
const result = resolveWorkspacePermissions(inputsFor(actor, true));
|
|
|
|
expect(result.hasAccess).toBe(hasAccess);
|
|
expect(result.canEdit).toBe(canEdit);
|
|
expect(result.canDelete).toBe(canDelete);
|
|
});
|
|
}
|
|
|
|
it('only the owner can delete, an admin cannot', () => {
|
|
// Stated separately because it is the one rule that differs from the
|
|
// project matrix, where a project admin does get canDelete through the
|
|
// workspace-owner branch.
|
|
expect(resolveWorkspacePermissions(inputsFor('admin', true)).canDelete).toBe(false);
|
|
expect(resolveWorkspacePermissions(inputsFor('owner', true)).canDelete).toBe(true);
|
|
});
|
|
|
|
it('reports the membership flags it was handed, unchanged', () => {
|
|
expect(resolveWorkspacePermissions(inputsFor('admin', true))).toEqual({
|
|
isOwner: false,
|
|
isMember: true,
|
|
isAdmin: true,
|
|
hasAccess: true,
|
|
canEdit: true,
|
|
canDelete: false,
|
|
ownerBillingActive: true,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('resolveWorkspacePermissions, with the owner billing lapsed', () => {
|
|
// Billing is the outer gate: it revokes everything, including from the owner
|
|
// of the workspace. A member who kept `hasAccess` here would keep reading a
|
|
// workspace the account no longer pays for.
|
|
for (const actor of ['outsider', 'member', 'admin', 'owner'] as const) {
|
|
it(`refuses a ${actor} everything`, () => {
|
|
const result = resolveWorkspacePermissions(inputsFor(actor, false));
|
|
|
|
expect(result.hasAccess).toBe(false);
|
|
expect(result.canEdit).toBe(false);
|
|
expect(result.canDelete).toBe(false);
|
|
});
|
|
}
|
|
|
|
it('still reports the membership flags, so a caller can tell "lapsed" from "not a member"', () => {
|
|
const result = resolveWorkspacePermissions(inputsFor('owner', false));
|
|
|
|
expect(result.isOwner).toBe(true);
|
|
expect(result.ownerBillingActive).toBe(false);
|
|
});
|
|
});
|