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>
This commit is contained in:
s.esmaeili
2026-09-22 21:28:17 +03:30
co-authored by Claude Opus 5
parent 460cf276b6
commit 676ec3ed72
4 changed files with 200 additions and 0 deletions
@@ -0,0 +1,76 @@
/**
* 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); });
+63
View File
@@ -0,0 +1,63 @@
/**
* Live end-to-end check of the D20 master OTP against a running API.
* All Persian text is a literal in this file, never a shell argument (D15).
*/
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';
(async () => {
const c = await prisma.customer.findFirst({
where: { isActive: true, isArchived: false }, select: { id: true, mobile: true, fullName: true },
});
if (!c) { console.log('no active customer to test with'); process.exit(1); }
console.log(`Testing against customer ${c.fullName} (${c.mobile})\n`);
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(() => ({})) };
};
console.log('CASE 1 — the master code signs an active customer in');
const a = await verify(c.mobile, MASTER);
chk(a.status === 200 || a.status === 201, `HTTP ${a.status}`);
chk(typeof a.body.token === 'string' && a.body.token.length > 20, 'a JWT came back');
chk(a.body.customer?.mobile === c.mobile, 'it is THIS customer, not another');
console.log('\n the token actually works on a protected customer route');
const me = await fetch(API + '/customer/me', { headers: { Authorization: 'Bearer ' + a.body.token } });
chk(me.status === 200, `GET customer/me -> ${me.status}`);
console.log('\nCASE 1b — Persian digits for the same master code');
const p = await verify(c.mobile, '۷۷۸۸۹۹');
chk(p.status === 200 || p.status === 201, `HTTP ${p.status} — faToEn applied to the code`);
console.log('\nCASE 2 — NEGATIVE: a near-miss code is still rejected');
const bad = await verify(c.mobile, '778898');
chk(bad.status === 401, `HTTP ${bad.status} (expected 401)`);
chk(!bad.body.token, 'no token issued');
console.log('\nCASE 2b — NEGATIVE: an unknown mobile is rejected even with the master code');
const unknown = await verify('09999999999', MASTER);
chk(unknown.status === 401, `HTTP ${unknown.status} (expected 401)`);
console.log('\nCASE 2c — NEGATIVE: an INACTIVE customer is rejected even with the master code');
const inactive = await prisma.customer.findFirst({ where: { isActive: false }, select: { mobile: true } });
if (inactive) {
const r = await verify(inactive.mobile, MASTER);
chk(r.status === 401, `HTTP ${r.status} (expected 401)`);
} else {
console.log(' (skipped — no inactive customer in the DB)');
}
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); });