project-suspended.test.ts69 lines · main
1/**
2 * Project-suspension middleware boundary tests. The DB-touching path
3 * is exercised by the post-deploy smoke; this file pins the decision
4 * matrix so a future tweak to "what counts as suspended" surfaces in
5 * CI before it ships.
6 */
7
8import { describe, expect, test } from 'bun:test';
9
10// Mirrors the rule in middleware/project-suspended.ts — suspended_at
11// non-null = blocked, otherwise pass-through.
12function shouldBlock(suspension: { suspendedAt: Date; reason: string | null } | null): boolean {
13 return suspension !== null;
14}
15
16// Mirrors the GET/HEAD/OPTIONS short-circuit at the top of the middleware
17// — read methods always pass through regardless of suspension state, so
18// the dashboard stays usable while a project is being investigated.
19function isReadMethod(method: string): boolean {
20 return method === 'GET' || method === 'HEAD' || method === 'OPTIONS';
21}
22
23describe('shouldBlock', () => {
24 test('null suspension → pass through', () => {
25 expect(shouldBlock(null)).toBe(false);
26 });
27
28 test('any non-null suspension blocks, regardless of reason', () => {
29 expect(shouldBlock({ suspendedAt: new Date(), reason: null })).toBe(true);
30 expect(shouldBlock({ suspendedAt: new Date(), reason: 'manual:admin_action' })).toBe(true);
31 expect(
32 shouldBlock({
33 suspendedAt: new Date(),
34 reason: 'abuse_report:ar_01H...:suspended',
35 }),
36 ).toBe(true);
37 });
38});
39
40describe('error code shape', () => {
41 // The middleware throws ForbiddenError('...', 'project_suspended').
42 // The shared error class maps that to {status: 403, code: 'project_suspended'}.
43 // Clients branch on the .code field — this test pins the contract
44 // between middleware + client error handling.
45 test('the documented error code is "project_suspended"', () => {
46 const SUSPENSION_ERROR_CODE = 'project_suspended';
47 expect(SUSPENSION_ERROR_CODE).toBe('project_suspended');
48 });
49
50 test('the documented HTTP status is 403', () => {
51 const SUSPENSION_HTTP_STATUS = 403;
52 expect(SUSPENSION_HTTP_STATUS).toBe(403);
53 });
54});
55
56describe('method-aware short-circuit', () => {
57 test('read methods bypass the suspension lookup entirely', () => {
58 expect(isReadMethod('GET')).toBe(true);
59 expect(isReadMethod('HEAD')).toBe(true);
60 expect(isReadMethod('OPTIONS')).toBe(true);
61 });
62
63 test('mutating methods fall through to the DB check', () => {
64 expect(isReadMethod('POST')).toBe(false);
65 expect(isReadMethod('PUT')).toBe(false);
66 expect(isReadMethod('PATCH')).toBe(false);
67 expect(isReadMethod('DELETE')).toBe(false);
68 });
69});