mfa-challenge.test.ts65 lines · main
1import { describe, expect, test } from 'bun:test';
2
3import {
4 consumeMfaChallenge,
5 issueMfaChallenge,
6 parseMfaChallenge,
7} from './mfa-challenge.js';
8
9describe('mfa-challenge', () => {
10 test('issues and parses a valid challenge', () => {
11 const token = issueMfaChallenge({
12 userId: 'beu_test',
13 tenantId: 'tenant_x',
14 });
15 const parsed = parseMfaChallenge(token);
16 expect(parsed.ok).toBe(true);
17 if (parsed.ok) {
18 expect(parsed.userId).toBe('beu_test');
19 expect(parsed.tenantId).toBe('tenant_x');
20 expect(parsed.nonce.length).toBeGreaterThan(8);
21 }
22 });
23
24 test('rejects tampered challenge', () => {
25 const token = issueMfaChallenge({
26 userId: 'beu_test',
27 tenantId: 'tenant_x',
28 });
29 const bad = token.slice(0, -4) + 'xxxx';
30 const parsed = parseMfaChallenge(bad);
31 expect(parsed.ok).toBe(false);
32 });
33
34 test('rejects empty challenge', () => {
35 expect(parseMfaChallenge('').ok).toBe(false);
36 expect(parseMfaChallenge(null).ok).toBe(false);
37 });
38
39 test('consume rejects wrong userId', async () => {
40 const token = issueMfaChallenge({
41 userId: 'beu_a',
42 tenantId: 'tenant_x',
43 });
44 const r = await consumeMfaChallenge(token, 'beu_other');
45 expect(r.ok).toBe(false);
46 if (!r.ok) expect(r.message).toMatch(/match user/i);
47 });
48
49 test('consume accepts matching user (Redis optional)', async () => {
50 const token = issueMfaChallenge({
51 userId: 'beu_consume',
52 tenantId: 'tenant_x',
53 });
54 const r = await consumeMfaChallenge(token, 'beu_consume');
55 expect(r.ok).toBe(true);
56 // Second consume: with Redis NX → already used; without Redis → still ok (fail-open)
57 const r2 = await consumeMfaChallenge(token, 'beu_consume');
58 if (r2.ok) {
59 // no redis — signature still valid within TTL
60 expect(r2.userId).toBe('beu_consume');
61 } else {
62 expect(r2.message).toMatch(/already used|expired|invalid/i);
63 }
64 });
65});