import { OtpService, normalizeOtpTarget } from './otp.service'; describe('normalizeOtpTarget', () => { it('canonicalises Ethiopian forms to one E.164 key', () => { const forms = ['+251986680099', '251986680099', '0986680099', '+251 98 668 0099']; const keys = forms.map((phone) => normalizeOtpTarget({ phone }).phone); expect(new Set(keys)).toEqual(new Set(['+251986680099'])); }); it('maps local 07… mobile to +2517…', () => { expect(normalizeOtpTarget({ phone: '0712345678' }).phone).toBe('+251712345678'); }); it('passes email targets through untouched', () => { expect(normalizeOtpTarget({ email: 'a@b.com' })).toEqual({ email: 'a@b.com' }); }); it('keeps an already-normalised number stable (idempotent)', () => { const once = normalizeOtpTarget({ phone: '0986680099' }).phone!; expect(normalizeOtpTarget({ phone: once }).phone).toBe(once); }); }); describe('OtpService — send/verify agree across phone formats', () => { // In-memory fake keyed by the exact phone string the service stores under, so // the test proves normalisation makes send and verify collide on one key. function makeService() { const rows = new Map(); const repo = { findByTarget: jest.fn(async (t: { phone?: string; email?: string }) => rows.get(t.email ?? t.phone!) ?? null, ), updateOtp: jest.fn(async (existing: { otp: string }, otp: string) => { existing.otp = otp; }), createOtp: jest.fn(async (t: { phone?: string; email?: string }, otp: string) => { rows.set(t.phone ?? t.email!, { ...t, otp, updatedAt: new Date(0) }); }), deleteOtp: jest.fn(async (row: { phone?: string; email?: string }) => { rows.delete(row.phone ?? row.email!); }), }; const sms = { sendSms: jest.fn().mockResolvedValue(undefined) }; const email = { sendEmail: jest.fn().mockResolvedValue(undefined) }; const service = new OtpService(repo as never, sms as never, email as never); return { service, rows }; } it('verifies a code sent to +251… when verify is called with 09…', async () => { const { service, rows } = makeService(); await service.sendOtp({ phone: '+251986680099' }); const stored = [...rows.values()][0]!.otp; // Fresh TTL: stamp updatedAt to now so the action verifier does not expire it. [...rows.values()][0]!.updatedAt = new Date(); await expect( service.verifyOtpForAction({ phone: '0986680099' }, stored), ).resolves.toEqual({ success: true }); }); });