77 lines
3.2 KiB
JavaScript
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); });
|