Files
rm-circle-team-router/qa/profiles-unit.mjs
T
martbost 2f90e1a97f QA pass on the member profile gate: stable button ids, completion opens the dashboard tab, local devCode, 47-test E2E suite
Findings fixed: the gate's buttons had no stable ids (fragile to test and maintain), and finishing the
gate left the member on the pitch tab where the profile card and Messages are not visible, so
completion now opens the Position Dashboard tab. /api/public/profile/email-start returns devCode
outside production so the flow is testable locally, matching the InstantAdPay pattern.

qa/profiles-unit.mjs (28 assertions) and qa/gate-e2e.mjs (47 assertions, real sessions, real UI) with
a README. All pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 17:40:03 -05:00

63 lines
3.7 KiB
JavaScript

// Unit test for RM Circle profiles.js: seeding, username rules, email code flow, coverage.
import { createRequire } from 'node:module';
import fs from 'node:fs';
const require = createRequire('D:/Projects/HighRisk/The RM Circle/promos/package.json');
const DIR = 'D:/tmp/rmc-prof-data';
fs.rmSync(DIR, { recursive: true, force: true }); fs.mkdirSync(DIR, { recursive: true });
// two positions already gave an email for payout alerts: they must seed in, unverified
fs.writeFileSync(DIR + '/member-alerts.json', JSON.stringify({ '21': { email: 'Marty@Example.com', ts: 'x' }, '49': { email: 'b@example.com', ts: 'x' } }));
const profiles = require('D:/Projects/HighRisk/The RM Circle/promos/profiles.js');
const sent = [];
profiles.init({ dataDir: DIR, sendEmail: (to, subj, text) => sent.push({ to, subj, text }) });
const ok = [], bad = [];
const t = (name, cond, extra) => (cond ? ok : bad).push(name + (extra ? ' -> ' + extra : ''));
// seeding
t('seeded 2 alert emails', profiles.coverage().withEmail === 2, JSON.stringify(profiles.coverage()));
t('seeded email is lowercased', (profiles.get(21) || {}).email === 'marty@example.com');
t('seeded email is NOT verified', !(profiles.get(21) || {}).emailVerified);
t('seeded position is incomplete', !profiles.complete(21));
// username rules
t('rejects short username', !!profiles.setUsername(21, 'ab').error);
t('rejects digits-only', !!profiles.setUsername(21, '12345').error);
t('rejects reserved', !!profiles.setUsername(21, 'admin').error);
t('rejects bad chars', !!profiles.setUsername(21, 'marty bostick').error);
t('accepts good username', profiles.setUsername(21, 'MartyB').ok === true);
t('username is lowercased', (profiles.get(21) || {}).username === 'martyb');
t('strips a leading @', profiles.setUsername(49, '@circleguy').ok && profiles.get(49).username === 'circleguy');
t('blocks a taken username', !!profiles.setUsername(49, 'martyb').error);
t('lets the same position keep its own name', profiles.setUsername(21, 'martyb').ok === true);
t('byUsername finds it', (profiles.byUsername('@MartyB') || {}).id === 21);
// email flow
t('rejects a bad address', !!profiles.startEmail(21, 'not-an-email').error);
const s1 = profiles.startEmail(21, 'marty@example.com');
t('sends a code', s1.ok === true && sent.length === 1, JSON.stringify(s1));
t('code email masks the address in the reply', (s1.to || '').includes('***'));
const code = (sent[0].subj.match(/(\d{6})/) || [])[1];
t('subject carries a 6-digit code', !!code);
t('cooldown blocks an instant resend', !!profiles.startEmail(21, 'marty@example.com').error);
t('wrong code refused', !!profiles.verifyEmail(21, '000000').error);
t('right code verifies', profiles.verifyEmail(21, code).ok === true);
t('profile now complete', profiles.complete(21) === true);
t('contactFor reports reachable', profiles.contactFor(21).reachable === true);
t('unverified position is NOT reachable', profiles.contactFor(49).reachable === false);
t('replayed code is refused', !!profiles.verifyEmail(21, code).error);
// one email, several positions (Triple Play)
profiles.setUsername(137, 'martyb2');
const s2 = profiles.startEmail(137, 'marty@example.com');
const code2 = (sent[sent.length - 1].subj.match(/(\d{6})/) || [])[1];
profiles.verifyEmail(137, code2);
t('one email can hold several positions', JSON.stringify(profiles.positionsFor('marty@example.com')) === '[21,137]', JSON.stringify(profiles.positionsFor('marty@example.com')));
// persistence
const fresh = JSON.parse(fs.readFileSync(DIR + '/profiles.json', 'utf8'));
t('written to disk', fresh.byId['21'].emailVerified === true);
t('coverage counts', profiles.coverage().complete === 2, JSON.stringify(profiles.coverage()));
console.log('PASS ' + ok.length);
for (const b of bad) console.log('FAIL ' + b);
process.exit(bad.length ? 1 : 0);