/** * Auth smoke test — verifies hashSecret/verifySecret and authenticateCredential. * Run: pnpm tsx scripts/auth-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 { hashSecret, verifySecret, prisma } from '../packages/db/src/index.js'; import { authenticateCredential } from '../packages/api/src/index.js'; let passed = 0; let failed = 0; function ok(label: string) { console.log(` ✓ ${label}`); passed++; } function fail(label: string, detail?: unknown) { console.error(` ✗ ${label}`, detail ?? ''); failed++; } async function main() { // ── 1. Crypto unit tests (no DB) ───────────────────────────────────────────── console.log('\n[1] Crypto unit tests'); const hash = await hashSecret('1234'); if (await verifySecret('1234', hash)) ok('verifySecret correct input → true'); else fail('verifySecret correct input → true'); if (!(await verifySecret('9999', hash))) ok('verifySecret wrong input → false'); else fail('verifySecret wrong input → false'); if (!(await verifySecret('x', null))) ok('verifySecret null stored → false'); else fail('verifySecret null stored → false'); if (!(await verifySecret('x', 'malformed'))) ok('verifySecret malformed stored → false'); else fail('verifySecret malformed stored → false'); // ── 2. authenticateCredential — success cases ───────────────────────────────── console.log('\n[2] authenticateCredential — success cases'); const op1 = await authenticateCredential({ email: 'op1@demo.local', secret: '1111', allowedRoles: ['OPERATOR'], }); if (op1 && op1.role === 'OPERATOR') ok('op1 + correct PIN → user returned'); else fail('op1 + correct PIN → user returned', op1); const admin = await authenticateCredential({ email: 'admin@demo.local', secret: 'admin1234', allowedRoles: ['ADMIN', 'SUPERVISOR'], }); if (admin && admin.role === 'ADMIN') ok('admin + correct password → user returned'); else fail('admin + correct password → user returned', admin); // ── 3. authenticateCredential — failure cases ───────────────────────────────── console.log('\n[3] authenticateCredential — failure cases'); const wrongPin = await authenticateCredential({ email: 'op2@demo.local', secret: '0000', allowedRoles: ['OPERATOR'], }); if (!wrongPin) ok('op2 + wrong PIN → null'); else fail('op2 + wrong PIN → null'); const wrongRole = await authenticateCredential({ email: 'admin@demo.local', secret: 'admin1234', allowedRoles: ['OPERATOR'], }); if (!wrongRole) ok('admin trying to log in as OPERATOR → null'); else fail('admin trying to log in as OPERATOR → null'); const noUser = await authenticateCredential({ email: 'ghost@demo.local', secret: '1111', allowedRoles: ['OPERATOR'], }); if (!noUser) ok('unknown email → null'); else fail('unknown email → null'); // ── 4. Lockout after 5 wrong PINs ───────────────────────────────────────────── console.log('\n[4] Lockout after 5 failed attempts (op3@demo.local)'); // Reset first in case a prior run left the account locked await prisma.user.updateMany({ where: { email: 'op3@demo.local' }, data: { failedAttempts: 0, lockedUntil: null }, }); for (let i = 1; i <= 5; i++) { await authenticateCredential({ email: 'op3@demo.local', secret: '0000', allowedRoles: ['OPERATOR'] }); } const afterLockout = await authenticateCredential({ email: 'op3@demo.local', secret: '3333', // correct PIN allowedRoles: ['OPERATOR'], }); if (!afterLockout) ok('6th attempt (correct PIN) still blocked by lockout → null'); else fail('6th attempt (correct PIN) still blocked by lockout → null'); // Reset op3 so the DB isn't left in a broken state await prisma.user.updateMany({ where: { email: 'op3@demo.local' }, data: { failedAttempts: 0, lockedUntil: null }, }); ok('op3 lockout reset — DB clean'); // ── Summary ─────────────────────────────────────────────────────────────────── await prisma.$disconnect(); console.log(`\nResults: ${passed} passed, ${failed} failed`); if (failed > 0) process.exit(1); } main().catch((err) => { console.error(err); process.exit(1); });