// OTP retrieval helpers (P1 skeleton). // // fetchEmailOTP(email) → reads the latest MailHog mail for `email`, scans body // for a 6-digit code. Full implementation in P2 lib. // fetchSMSOTP(phone) → reads "dev:notification:last:sms:" from Redis // (k6/experimental/redis). Implementation in P3. // // Both poll for up to cfg.otpPollTimeoutMs because Notifier writes happen // asynchronously (Send goroutine + Redis hook). import http from 'k6/http'; import { sleep } from 'k6'; // k6 v2.0+ uses 'k6/x/redis' (the experimental module was promoted). The // default export is a namespace; the constructor is `redis.Client`. import redis from 'k6/x/redis'; import { cfg } from './config.js'; const SIX_DIGITS = /\b(\d{6})\b/; // CSS hex colors (#aabbcc) look like 6-digit OTPs to a naive regex; strip // them before scanning. We also strip quoted-printable soft-line-breaks // (`=\n`) since MailHog returns bodies QP-encoded. const CSS_HEX = /#[0-9a-fA-F]{6}\b/g; const QP_SOFT_BREAK = /=\r?\n/g; function sleepMs(ms) { sleep(ms / 1000); } // extractOTPFromText returns the LAST 6-digit number found (after stripping // CSS hex colors). "Last" because the OTP is typically rendered near the // bottom of the email body, after the header/branding markup. export function extractOTPFromText(text) { if (!text) return ''; const cleaned = String(text).replace(QP_SOFT_BREAK, '').replace(CSS_HEX, ''); const all = cleaned.match(/\b\d{6}\b/g); if (!all || all.length === 0) return ''; return all[all.length - 1]; } // fetchEmailOTP polls MailHog's /api/v2/search?kind=to&query= // until a 6-digit OTP can be parsed out of the most recent message body. // // opts.since (ms epoch, default 0): ignore mails delivered before this ts — // needed when the same address has multiple OTPs in MailHog (e.g. register // then email-verify), so the verify step picks up the new mail rather than // the register one. // opts.limit (default 5): number of latest mails to inspect each poll. export function fetchEmailOTP(email, opts = {}) { const since = opts.since || 0; const limit = opts.limit || 5; const deadline = Date.now() + cfg.otpPollTimeoutMs; const u = `${cfg.mailhogUrl}/api/v2/search?kind=to&query=${encodeURIComponent(email)}&start=0&limit=${limit}`; let last = ''; while (Date.now() < deadline) { const res = http.get(u); if (res.status === 200) { try { const body = JSON.parse(res.body); const items = (body && body.items) || []; for (const item of items) { const created = item.Created ? Date.parse(item.Created) : 0; if (created && created < since) continue; const candidates = [ item.Content && item.Content.Body, item.MIME && item.MIME.Parts && item.MIME.Parts.map((p) => p.Body).join('\n'), ].filter(Boolean); for (const c of candidates) { const code = extractOTPFromText(c); if (code) return code; } } if (items.length > 0) last = `no 6-digit code in ${items.length} items (since=${since})`; } catch (e) { last = `parse-error: ${e}`; } } else { last = `mailhog status ${res.status}`; } sleepMs(cfg.otpPollIntervalMs); } throw new Error(`fetchEmailOTP(${email}) timed out: ${last}`); } // fetchSMSOTP polls Redis key "dev:notification:last:sms:" set by the // mock SMS sender (see internal/model/notification/provider/sms/mock_sender.go). // // opts.prevBody: when supplied, the poll ignores writes whose body equals // prevBody. This is essential when the same phone has two OTPs in a single // test (e.g. register-resend or re-verify); the caller passes the body it // already consumed so the helper waits for the next one. // // Requires k6 v2.0+ which exposes the redis client at k6/x/redis (the // `k6/experimental/redis` module was removed in v2.0). Statically imported // above so this works in default k6 compatibility mode. export async function fetchSMSOTP(phone, opts = {}) { const prev = opts.prevBody || ''; const [host, port] = cfg.redisAddr.split(':'); const client = new redis.Client({ socket: { host: host, port: parseInt(port || '6379', 10) } }); const key = `dev:notification:last:sms:${phone}`; const deadline = Date.now() + cfg.otpPollTimeoutMs; while (Date.now() < deadline) { const body = await client.get(key); if (body && body !== prev) { const code = extractOTPFromText(body); if (code) return { code, body }; } sleepMs(cfg.otpPollIntervalMs); } throw new Error(`fetchSMSOTP(${phone}) timed out`); }