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