/** * Tests for the calendar boundary. * * The projection itself is exercised against a real Postgres by the seeded * demo book; what is pinned here are the decisions that would otherwise fail * silently — a mistyped `kinds` filter that looks like a quiet quarter, an * authorization gate that mistakes authentication for permission, and the * relationship checks that the nullable foreign keys cannot enforce * themselves. */ import { strict as assert } from 'node:assert'; import { describe, it } from 'node:test'; import type { Database } from '@pig/db'; import { AuthError, type Principal } from '../src/lib/auth'; import { MutationError, executeMutation } from '../src/lib/mutation'; import { calendarReadAllowed, createEntryMutationDefinition, deleteEntryMutationDefinition, entriesQuerySchema, parseKinds, querySchema, requireCalendarWrite, } from '../src/routes/calendar'; function principal(overrides: Partial = {}): Principal { return { userId: '10000000-0000-4000-8000-000000000001', email: 'seller@example.com', name: 'Seller', isPlatformAdmin: false, teams: [{ team: 'demand', role: 'member' }], via: 'jwt', scopes: ['read', 'write'], ...overrides, }; } describe('calendar read boundary', () => { it('requires an explicit read scope rather than treating a token as permission', () => { assert.equal(calendarReadAllowed([]), false); assert.equal(calendarReadAllowed(['write']), false); assert.equal(calendarReadAllowed(['read']), true); }); }); describe('calendar write boundary', () => { it('accepts either pipeline, because a dated item belongs to whoever runs the motion', () => { assert.doesNotThrow(() => requireCalendarWrite(principal({ teams: [{ team: 'demand', role: 'member' }] })), ); assert.doesNotThrow(() => requireCalendarWrite(principal({ teams: [{ team: 'supply', role: 'member' }] })), ); }); it('refuses a read-only credential even when its owner has the role', () => { // The credential's scope caps the person's authority; an agent key issued // for reading must not be able to write because a human somewhere may. assert.throws( () => requireCalendarWrite(principal({ scopes: ['read'] })), (error: unknown) => error instanceof AuthError && error.code === 'insufficient_scope', ); }); it('refuses a member of neither pipeline', () => { assert.throws( () => requireCalendarWrite(principal({ teams: [{ team: 'research', role: 'admin' }] })), (error: unknown) => error instanceof AuthError && error.code === 'insufficient_permission', ); }); }); describe('kinds filter', () => { it('rejects an unknown kind rather than returning nothing', () => { // A typo that silently filters everything out is indistinguishable from a // genuinely empty quarter, which is the worst possible failure for a view // whose whole job is to show what is coming. assert.throws( () => parseKinds('renewal'), (error: unknown) => error instanceof MutationError && error.code === 'invalid_kinds', ); assert.throws(() => parseKinds('obligation_due,expected_clos'), MutationError); }); it('treats absent and empty as no filter at all', () => { assert.equal(parseKinds(undefined), undefined); assert.equal(parseKinds(''), undefined); assert.equal(parseKinds(' , '), undefined); }); it('accepts a spaced list of known kinds', () => { assert.deepEqual(parseKinds('obligation_due, renewal_notice'), [ 'obligation_due', 'renewal_notice', ]); }); }); describe('calendar query validation', () => { it('rejects a malformed account id on both reads, not just one', () => { // Fed straight into `eq()` on a uuid column, `not-a-uuid` came back as a // 500 from Postgres 22P02. The two endpoints take the identical parameter // and must answer it identically. assert.equal(querySchema.safeParse({ accountId: 'not-a-uuid' }).success, false); assert.equal(entriesQuerySchema.safeParse({ accountId: 'not-a-uuid' }).success, false); assert.equal( entriesQuerySchema.safeParse({ accountId: '30000000-0000-4000-8000-000000000003' }) .success, true, ); assert.equal(entriesQuerySchema.safeParse({}).success, true); }); it('rejects a time zone the runtime cannot use rather than silently answering in UTC', () => { // The cache in @pig/core is keyed on this string, so an unvalidated one is // both a wrong answer and a way to make a long-lived process grow. assert.equal(querySchema.safeParse({ timezone: 'Mars/Olympus' }).success, false); assert.equal(querySchema.safeParse({ timezone: 'Europe/London' }).success, true); }); }); /** A transaction stub that records the order of writes, as in capacity-writes. */ function recordingDb(rows: { select?: unknown[]; insertReturns?: unknown[]; deleteReturns?: unknown[]; }) { const events: string[] = []; const tx = { select: () => ({ from: () => ({ where: () => ({ limit: async () => { events.push('select'); return rows.select ?? []; }, }), }), }), insert: () => ({ // A thenable rather than a promise: the audit write is awaited directly // while the entity write goes through `.returning()`, and constructing a // real promise here would record the audit write that never happened. values: (values: Record) => { const record = () => events.push('subject' in values ? 'activity' : 'insert'); return { then: (resolve: (value: unknown) => unknown) => { record(); return Promise.resolve().then(() => resolve(undefined)); }, returning: async () => { record(); return rows.insertReturns ?? []; }, }; }, }), delete: () => ({ where: () => ({ returning: async () => { events.push('delete'); return rows.deleteReturns ?? []; }, }), }), }; const db = { transaction: async (work: (transaction: unknown) => Promise) => { events.push('begin'); const result = await work(tx); events.push('commit'); return result; }, } as unknown as Database; return { db, events }; } describe('calendar entry mutation', () => { const entry = { id: '20000000-0000-4000-8000-000000000002', title: 'Q business review', kind: 'qbr' as const, accountId: null, demandDealId: null, supplyDealId: null, startsAt: new Date('2026-09-03T14:00:00.000Z'), endsAt: new Date('2026-09-03T15:30:00.000Z'), completedAt: null, }; it('writes the entry and its audit event inside one transaction', async () => { const { db, events } = recordingDb({ insertReturns: [entry] }); const created = await executeMutation( db, principal(), async () => ({ title: 'Q business review', kind: 'qbr', startsAt: '2026-09-03T14:00:00.000Z', endsAt: '2026-09-03T15:30:00.000Z', }), createEntryMutationDefinition(), ); assert.equal(created.id, entry.id); assert.deepEqual(events, ['begin', 'insert', 'activity', 'commit']); }); it('defaults the owner to the author, because unassigned work is work nobody does', async () => { let written: Record | undefined; const capturing = { transaction: async (work: (transaction: unknown) => Promise) => work({ insert: () => ({ values: (values: Record) => { written ??= values; return Object.assign(Promise.resolve(), { returning: async () => [entry], }); }, }), }), } as unknown as Database; await executeMutation( capturing, principal(), async () => ({ title: 'Reminder', startsAt: '2026-09-03T14:00:00.000Z' }), createEntryMutationDefinition(), ); assert.equal(written?.ownerUserId, '10000000-0000-4000-8000-000000000001'); assert.equal(written?.createdByUserId, '10000000-0000-4000-8000-000000000001'); }); it('refuses a window that ends before it starts', async () => { const { db } = recordingDb({ insertReturns: [entry] }); await assert.rejects( executeMutation( db, principal(), async () => ({ title: 'Backwards', startsAt: '2026-09-03T16:00:00.000Z', endsAt: '2026-09-03T14:00:00.000Z', }), createEntryMutationDefinition(), ), (error: unknown) => error instanceof MutationError && error.code === 'invalid_window', ); }); it('refuses a deal that belongs to a different account', async () => { // Nothing in the schema can catch this: both columns are independently // nullable foreign keys, so the disagreement is only visible here. const { db } = recordingDb({ select: [{ accountId: '90000000-0000-4000-8000-000000000009' }], }); await assert.rejects( executeMutation( db, principal(), async () => ({ title: 'Mismatch', startsAt: '2026-09-03T14:00:00.000Z', accountId: '30000000-0000-4000-8000-000000000003', demandDealId: '40000000-0000-4000-8000-000000000004', }), createEntryMutationDefinition(), ), (error: unknown) => error instanceof MutationError && error.code === 'relationship_mismatch', ); }); it('reports a stale owner as 404, the way every other reference here does', async () => { // The column is a foreign key with no check in front of it, so assigning // to a user who has been removed produced a 500 from the constraint. It is // an ordinary client mistake and deserves the ordinary answer. const { db } = recordingDb({ select: [], insertReturns: [entry] }); await assert.rejects( executeMutation( db, principal(), async () => ({ title: 'Handover', startsAt: '2026-09-03T14:00:00.000Z', ownerUserId: '50000000-0000-4000-8000-000000000005', }), createEntryMutationDefinition(), ), (error: unknown) => error instanceof MutationError && error.status === 404, ); }); it('does not re-read the author when it defaults the owner to them', async () => { // The request already proved that user exists; a lookup per create to // confirm it would be a query bought with nothing. const { db, events } = recordingDb({ insertReturns: [entry] }); await executeMutation( db, principal(), async () => ({ title: 'Reminder', startsAt: '2026-09-03T14:00:00.000Z' }), createEntryMutationDefinition(), ); assert.deepEqual(events, ['begin', 'insert', 'activity', 'commit']); }); it('reports a missing entry as 404 rather than a silent no-op delete', async () => { const { db } = recordingDb({ deleteReturns: [] }); await assert.rejects( executeMutation( db, principal(), async () => ({}), deleteEntryMutationDefinition(), { id: '20000000-0000-4000-8000-000000000002' }, ), (error: unknown) => error instanceof MutationError && error.status === 404, ); }); });