passwordless.test.ts86 lines · main
| 1 | import { describe, expect, test } from 'bun:test'; |
| 2 | |
| 3 | import { |
| 4 | hashSecret, |
| 5 | isMagicLinkBaseAllowed, |
| 6 | matchPasswordlessSecret, |
| 7 | pickMagicLinkAppOrigin, |
| 8 | sixDigitCode, |
| 9 | } from './passwordless.js'; |
| 10 | |
| 11 | describe('passwordless pure helpers (Phase 3)', () => { |
| 12 | test('pickMagicLinkAppOrigin prefers https production over localhost', () => { |
| 13 | expect( |
| 14 | pickMagicLinkAppOrigin( |
| 15 | ['http://localhost:3000', 'https://pay.mavifinans.sh'], |
| 16 | null, |
| 17 | ), |
| 18 | ).toBe('https://pay.mavifinans.sh'); |
| 19 | }); |
| 20 | |
| 21 | test('pickMagicLinkAppOrigin prefers matching request Origin', () => { |
| 22 | expect( |
| 23 | pickMagicLinkAppOrigin( |
| 24 | ['http://localhost:3000', 'https://pay.mavifinans.sh'], |
| 25 | 'http://localhost:3000', |
| 26 | ), |
| 27 | ).toBe('http://localhost:3000'); |
| 28 | }); |
| 29 | |
| 30 | test('sixDigitCode is 6 digits', () => { |
| 31 | for (let i = 0; i < 20; i++) { |
| 32 | const c = sixDigitCode(); |
| 33 | expect(c).toMatch(/^\d{6}$/); |
| 34 | expect(Number(c)).toBeGreaterThanOrEqual(100000); |
| 35 | expect(Number(c)).toBeLessThanOrEqual(999999); |
| 36 | } |
| 37 | }); |
| 38 | |
| 39 | test('match OTP-only hash', () => { |
| 40 | const otp = '123456'; |
| 41 | const stored = hashSecret(otp); |
| 42 | expect(matchPasswordlessSecret(stored, { userInputCode: otp })).toBe(true); |
| 43 | expect(matchPasswordlessSecret(stored, { userInputCode: '000000' })).toBe( |
| 44 | false, |
| 45 | ); |
| 46 | }); |
| 47 | |
| 48 | test('match link-only hash', () => { |
| 49 | const link = 'abcLinkCodeXYZ'; |
| 50 | const stored = hashSecret(link); |
| 51 | expect(matchPasswordlessSecret(stored, { linkCode: link })).toBe(true); |
| 52 | expect(matchPasswordlessSecret(stored, { linkCode: 'nope' })).toBe(false); |
| 53 | }); |
| 54 | |
| 55 | test('match dual otp:link form', () => { |
| 56 | const otp = '654321'; |
| 57 | const link = 'linkSecret99'; |
| 58 | const stored = `${hashSecret(otp)}:${hashSecret(link)}`; |
| 59 | expect(matchPasswordlessSecret(stored, { userInputCode: otp })).toBe(true); |
| 60 | expect(matchPasswordlessSecret(stored, { linkCode: link })).toBe(true); |
| 61 | expect( |
| 62 | matchPasswordlessSecret(stored, { userInputCode: '111111', linkCode: 'x' }), |
| 63 | ).toBe(false); |
| 64 | }); |
| 65 | |
| 66 | test('empty input fails', () => { |
| 67 | expect(matchPasswordlessSecret(hashSecret('1'), {})).toBe(false); |
| 68 | }); |
| 69 | |
| 70 | test('isMagicLinkBaseAllowed rejects evil origins when allowlist set', () => { |
| 71 | expect( |
| 72 | isMagicLinkBaseAllowed( |
| 73 | 'https://evil.example/phish', |
| 74 | ['https://pay.mavifinans.sh'], |
| 75 | 'https://pay.mavifinans.sh', |
| 76 | ), |
| 77 | ).toBe(false); |
| 78 | expect( |
| 79 | isMagicLinkBaseAllowed( |
| 80 | 'https://pay.mavifinans.sh/auth/verify', |
| 81 | ['https://pay.mavifinans.sh'], |
| 82 | 'https://pay.mavifinans.sh', |
| 83 | ), |
| 84 | ).toBe(true); |
| 85 | }); |
| 86 | }); |