/** * Report smoke test — exercises the real maintenanceRequest.report procedure * (aggregation + Zod validation + role) via a tRPC caller, the same pattern as * maintenance-smoke.ts. * Run: pnpm tsx scripts/report-smoke.ts * Requires: Docker Postgres running + pnpm db:seed already done. */ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { config as loadEnv } from 'dotenv'; const here = path.dirname(fileURLToPath(import.meta.url)); loadEnv({ path: path.resolve(here, '../.env') }); import { prisma } from '../packages/db/src/index.js'; import { appRouter, createTRPCContext } from '../packages/api/src/index.js'; import { createCallerFactory } from '../packages/api/src/trpc.js'; let passed = 0; let failed = 0; function ok(label: string) { console.log(` ✓ ${label}`); passed++; } function fail(label: string, detail?: string) { console.error(` ✗ ${label}${detail ? ` — ${detail}` : ''}`); failed++; } function assert(condition: boolean, label: string, detail?: string) { if (condition) ok(label); else fail(label, detail); } async function makeCaller(email: string) { const user = await prisma.user.findFirst({ where: { email } }); if (!user) throw new Error(`User ${email} not found — run pnpm db:seed`); const ctx = await createTRPCContext({ user: { id: user.id, email: user.email, role: user.role as 'ADMIN' | 'OPERATOR', tenantId: user.tenantId, }, headers: new Headers(), }); return createCallerFactory(appRouter)(ctx); } async function main() { console.log('\nReport smoke — running assertions against the real procedure…\n'); const admin = await makeCaller('admin@demo.local'); // Wide window covering all seeded "today" requests. const from = new Date(); from.setHours(0, 0, 0, 0); const to = new Date(); const report = await admin.maintenanceRequest.report({ from, to }); // 1. Totals — seed creates 3 RESOLVED + 1 CLAIMED + 2 OPEN = 6 assert(report.totals.created >= 6, `created ≥6 (got ${report.totals.created})`); assert(report.totals.open >= 2, `open ≥2 (got ${report.totals.open})`); assert(report.totals.claimed >= 1, `claimed ≥1 (got ${report.totals.claimed})`); assert(report.totals.resolved >= 3, `resolved ≥3 (got ${report.totals.resolved})`); assert( report.totals.open + report.totals.claimed + report.totals.resolved === report.totals.created, 'totals add up to created', `${report.totals.open}+${report.totals.claimed}+${report.totals.resolved} != ${report.totals.created}`, ); // 2. responseMs — only rows with claimedAt (3 resolved + 1 claimed = 4) assert(report.responseMs.count >= 4, `responseMs.count ≥4 (got ${report.responseMs.count})`); assert( report.responseMs.avg !== null && report.responseMs.avg > 0, `responseMs.avg positive (${report.responseMs.avg})`, ); assert( report.responseMs.max !== null && report.responseMs.max >= report.responseMs.avg!, `responseMs.max ≥ avg (${report.responseMs.max} ≥ ${report.responseMs.avg})`, ); // 3. resolutionMs — only resolved rows (3) assert(report.resolutionMs.count >= 3, `resolutionMs.count ≥3 (got ${report.resolutionMs.count})`); assert( report.resolutionMs.avg !== null && report.resolutionMs.avg > 0, `resolutionMs.avg positive (${report.resolutionMs.avg})`, ); // 4. resolution takes longer than response on average (it includes it) if (report.responseMs.avg !== null && report.resolutionMs.avg !== null) { assert( report.resolutionMs.avg > report.responseMs.avg, `resolutionMs.avg (${report.resolutionMs.avg}) > responseMs.avg (${report.responseMs.avg})`, ); } // 5. byWorkstation — counts add up, sorted desc const wTotal = report.byWorkstation.reduce((s, w) => s + w.count, 0); assert(wTotal === report.totals.created, 'byWorkstation counts add up to created'); const wSortedDesc = report.byWorkstation.every( (w, i) => i === 0 || report.byWorkstation[i - 1]!.count >= w.count, ); assert(wSortedDesc, 'byWorkstation sorted by count desc'); // 6. byArea — counts add up const aTotal = report.byArea.reduce((s, a) => s + a.count, 0); assert(aTotal === report.totals.created, 'byArea counts add up to created'); // 7. stillOpen = OPEN + CLAIMED, never RESOLVED assert( report.stillOpen.length === report.totals.open + report.totals.claimed, `stillOpen length == OPEN+CLAIMED (${report.stillOpen.length} == ${report.totals.open + report.totals.claimed})`, ); assert( report.stillOpen.every((r) => r.status === 'OPEN' || r.status === 'CLAIMED'), 'stillOpen contains no RESOLVED rows', ); // 8. Validation: to <= from → BAD_REQUEST try { await admin.maintenanceRequest.report({ from: to, to: from }); fail('to <= from → BAD_REQUEST', 'no error thrown'); } catch (err: unknown) { const code = (err as { code?: string }).code; assert(code === 'BAD_REQUEST', 'to <= from → BAD_REQUEST', `got code=${code}`); } // 9. Validation: window > 31 days → BAD_REQUEST try { const farFrom = new Date(to.getTime() - 40 * 24 * 60 * 60 * 1000); await admin.maintenanceRequest.report({ from: farFrom, to }); fail('window > 31d → BAD_REQUEST', 'no error thrown'); } catch (err: unknown) { const code = (err as { code?: string }).code; assert(code === 'BAD_REQUEST', 'window > 31d → BAD_REQUEST', `got code=${code}`); } // 10. Future window → empty report const fFrom = new Date(to.getTime() + 24 * 60 * 60 * 1000); const fTo = new Date(to.getTime() + 48 * 60 * 60 * 1000); const empty = await admin.maintenanceRequest.report({ from: fFrom, to: fTo }); assert(empty.totals.created === 0, 'future window → 0 created'); assert(empty.responseMs.avg === null, 'future window → responseMs.avg null'); assert(empty.stillOpen.length === 0, 'future window → stillOpen empty'); // 11. Role: an OPERATOR may not call report (requireRole) const op = await makeCaller('op1@demo.local'); try { await op.maintenanceRequest.report({ from, to }); fail('operator → FORBIDDEN', 'no error thrown'); } catch (err: unknown) { const code = (err as { code?: string }).code; assert(code === 'FORBIDDEN', 'operator → FORBIDDEN', `got code=${code}`); } console.log(`\n${passed} passed, ${failed} failed.\n`); if (failed > 0) process.exit(1); } main() .catch((err) => { console.error('Report smoke failed:', err); process.exit(1); }) .finally(() => prisma.$disconnect());