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 | |
| 8 | import { 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. |
| 12 | function 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. |
| 19 | function isReadMethod(method: string): boolean { |
| 20 | return method === 'GET' || method === 'HEAD' || method === 'OPTIONS'; |
| 21 | } |
| 22 | |
| 23 | describe('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 | |
| 40 | describe('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 | |
| 56 | describe('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 | }); |