/** * The other half of the D20 contract, run against an API started WITHOUT * OTP_MASTER_CODE: * * the master code must now be rejected; * * the real, freshly-issued code must still work. * * This is the negative case RULE E1 demands: without it, "the master code * works" proves nothing about whether it can be switched off. */ const { PrismaClient } = require('@prisma/client'); const prisma = new PrismaClient(); const API = 'http://localhost:3000/api'; let pass = 0, fail = 0; const chk = (c, m) => { c ? pass++ : fail++; console.log((c ? ' PASS ' : ' FAIL ') + m); }; const MASTER = '778899'; const verify = async (mobile, code) => { const r = await fetch(API + '/customer/auth/verify-otp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: mobile, password: code }), }); return { status: r.status, body: await r.json().catch(() => ({})) }; }; (async () => { const c = await prisma.customer.findFirst({ where: { isActive: true, isArchived: false }, select: { id: true, mobile: true, fullName: true }, }); console.log(`Testing against customer ${c.fullName} (${c.mobile})\n`); console.log('CASE 3 — NEGATIVE: with the env var absent, the master code is REJECTED'); const m = await verify(c.mobile, MASTER); chk(m.status === 401, `HTTP ${m.status} (expected 401)`); chk(!m.body.token, 'no token issued'); console.log('\nCASE 4 — the REAL code still works'); // Clear the throttle from earlier runs so requestOtp is allowed. const removed = await prisma.customerOtp.deleteMany({ where: { mobile: c.mobile } }); console.log(` (cleared ${removed.count} of this test number's own OTP rows to bypass the resend cooldown)`); const req = await fetch(API + '/customer/auth/request-otp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: c.mobile }), }); chk(req.status === 200 || req.status === 201, `request-otp -> HTTP ${req.status}`); // The code is hashed in the DB, so read it from the server log line that // dev mode prints instead of trying to reverse the hash. const fs = require('fs'); const log = fs.readFileSync(process.argv[2], 'utf8'); const codeLine = log .split('\n') .reverse() .find((l) => l.includes('OTP for ' + c.mobile)); const real = codeLine ? (codeLine.match(/:\s*(\d{4})/) || [])[1] : null; if (!real) { console.log(' FAIL could not read the dev OTP from the log; last lines:'); console.log(log.split('\n').slice(-5).join('\n')); fail++; } else { console.log(` (dev log exposed the code: ${real})`); const v = await verify(c.mobile, real); chk(v.status === 200 || v.status === 201, `verify with the REAL code -> HTTP ${v.status}`); chk(typeof v.body.token === 'string', 'a JWT came back'); console.log('\n and it is single-use: replaying the same code fails'); const again = await verify(c.mobile, real); chk(again.status === 401, `replay -> HTTP ${again.status} (expected 401)`); } console.log(`\npassed: ${pass} failed: ${fail}`); await prisma.$disconnect(); process.exit(fail ? 1 : 0); })().catch(async (e) => { console.error(e); await prisma.$disconnect(); process.exit(1); });