Files
tehran_taj/apps/api/scripts/test-master-otp-disabled.cjs
T
s.esmaeiliandClaude Opus 5 676ec3ed72 test(api): live D20 verification + document every env var
Two live scripts against a running API, covering both halves of the master-OTP
contract:
  * with OTP_MASTER_CODE set (8/8): signs in, the JWT works on customer/me,
    Persian digits accepted, and the RULE E1 negatives — near-miss code,
    unknown mobile, inactive customer — all still rejected.
  * without it (6/6): the same code returns 401, no boot banner, a real code
    still logs in, and replaying it fails.

Also confirmed from the boot log that with no SMS credentials the server starts
normally and declines to send instead of erroring (D20).

.env.example gained the auth, SMS and upload variables, names only, no values,
with OTP_MASTER_CODE marked delivery-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 21:28:17 +03:30

77 lines
3.2 KiB
JavaScript

/**
* 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); });