mcp-auth-bridge.test.ts154 lines · main
1import { describe, expect, test } from 'bun:test';
2
3import {
4 AUTH_BRIDGE_TOOLS,
5 AUTH_GUIDANCE,
6 matchAuthGuidance,
7 sanitizeAuthConfig,
8 tokeniseQuestion,
9} from './mcp-auth-bridge.js';
10import { DEFAULT_AUTH_CONFIG, type AuthConfig } from './tenant-config-store.js';
11
12describe('mcp auth bridge — pure helpers', () => {
13 /* ── sanitizeAuthConfig ────────────────────────────────────────────── */
14
15 test('sanitizeAuthConfig replaces OAuth clientId values with a boolean', () => {
16 const cfg: AuthConfig = {
17 ...DEFAULT_AUTH_CONFIG,
18 providers: {
19 ...DEFAULT_AUTH_CONFIG.providers,
20 google: { enabled: true, clientId: 'g-123-secret-ish.apps.example' },
21 github: { enabled: false, clientId: null },
22 },
23 };
24 const out = sanitizeAuthConfig(cfg);
25 expect(out.providers.google).toEqual({ enabled: true, clientIdSet: true });
26 expect(out.providers.github).toEqual({ enabled: false, clientIdSet: false });
27 expect(JSON.stringify(out)).not.toContain('g-123-secret-ish');
28 });
29
30 test('sanitizeAuthConfig keeps non-clientId provider settings and branding', () => {
31 const out = sanitizeAuthConfig(DEFAULT_AUTH_CONFIG);
32 expect(out.providers.magicLink).toHaveProperty('enabled');
33 expect(out.providers.magicLink).toHaveProperty('expiryMinutes');
34 expect(out.branding).toHaveProperty('senderName');
35 expect(out.branding).toHaveProperty('senderDomain');
36 });
37
38 /* ── matchAuthGuidance ─────────────────────────────────────────────── */
39
40 test('fallback-sender question surfaces the fallback guidance first', () => {
41 const matches = matchAuthGuidance('why do my emails still come from noreply@briven.tech?');
42 expect(matches.length).toBeGreaterThan(0);
43 expect(matches[0]!.id).toBe('emails-from-fallback');
44 });
45
46 test('sender-domain question finds sender-domain guidance', () => {
47 const ids = matchAuthGuidance('how do I set a custom sender domain for my emails').map(
48 (m) => m.id,
49 );
50 expect(ids).toContain('sender-domain-setup');
51 });
52
53 test('2FA / backup code question hits two-factor guidance', () => {
54 const ids = matchAuthGuidance('lost phone how do backup recovery codes work for 2fa').map(
55 (m) => m.id,
56 );
57 expect(ids).toContain('two-factor-backup-codes');
58 });
59
60 test('e2e testing token question hits testing-tokens guidance', () => {
61 const ids = matchAuthGuidance('how do I use a testing token for playwright e2e ci').map(
62 (m) => m.id,
63 );
64 expect(ids).toContain('testing-tokens-e2e');
65 });
66
67 test('scaffold question hits scaffold-setup guidance', () => {
68 const ids = matchAuthGuidance('briven auth scaffold next middleware setup').map((m) => m.id);
69 expect(ids).toContain('scaffold-setup');
70 });
71
72 test('passkey 404 / Face ID question hits passkey-webauthn guidance', () => {
73 const ids = matchAuthGuidance(
74 'passkey generate authenticate options returns 404 is Face ID waiting on platform',
75 ).map((m) => m.id);
76 expect(ids).toContain('passkey-webauthn');
77 });
78
79 test('magic link HTTP 500 empty body hits magic-otp-500 guidance', () => {
80 const ids = matchAuthGuidance(
81 'POST magic link returns HTTP 500 empty body what is broken',
82 ).map((m) => m.id);
83 expect(ids).toContain('magic-otp-500');
84 });
85
86 test('OAuth toggle without secrets hits providers-config', () => {
87 const ids = matchAuthGuidance(
88 'google provider enabled but oauth needs client id and secret',
89 ).map((m) => m.id);
90 expect(ids).toContain('providers-config');
91 });
92
93 test('passkey guidance teaches briven-engine webauthn FDI paths', () => {
94 const entry = AUTH_GUIDANCE.find((e) => e.id === 'passkey-webauthn')!;
95 const a = entry.answer.toLowerCase();
96 expect(a).toContain('/v1/auth-core/fdi/webauthn');
97 expect(a).toContain('signin/options');
98 expect(a).toContain('device');
99 expect(a).toContain('410');
100 });
101
102 test('OTP and magic guidance cite briven-engine FDI not auth-tenant', () => {
103 const otp = AUTH_GUIDANCE.find((e) => e.id === 'email-otp-integration')!;
104 const magic = AUTH_GUIDANCE.find((e) => e.id === 'magic-link-integration')!;
105 expect(otp.answer).toContain('/v1/auth-core/fdi/signinup/code');
106 expect(magic.answer).toContain('/v1/auth-core/fdi/signinup/code');
107 expect(otp.answer.toLowerCase()).toContain('410');
108 expect(magic.answer.toLowerCase()).toContain('410');
109 // Guidance may mention auth-tenant only as retired
110 for (const e of AUTH_GUIDANCE) {
111 if (e.answer.includes('/v1/auth-tenant/')) {
112 expect(e.answer.toLowerCase()).toMatch(/retir|410|gone|do not/);
113 }
114 }
115 });
116
117 test('unrelated question returns no matches', () => {
118 expect(matchAuthGuidance('kubernetes ingress annotations')).toEqual([]);
119 });
120
121 test('every guidance entry carries apply-steps and a docs citation', () => {
122 for (const entry of AUTH_GUIDANCE) {
123 expect(entry.applyInYourProject.length).toBeGreaterThan(0);
124 expect(entry.docs).toStartWith('https://docs.briven.tech/');
125 }
126 });
127
128 test('guidance prose never names the email vendor', () => {
129 const blob = JSON.stringify(AUTH_GUIDANCE).toLowerCase();
130 expect(blob).not.toContain('mittera');
131 });
132
133 /* ── misc ──────────────────────────────────────────────────────────── */
134
135 test('tokeniseQuestion lowercases and drops single characters', () => {
136 expect(tokeniseQuestion('Why DO x emails FAIL?')).toEqual(['why', 'do', 'emails', 'fail']);
137 });
138
139 test('AUTH_BRIDGE_TOOLS lists exactly the three read tools', () => {
140 expect([...AUTH_BRIDGE_TOOLS].sort()).toEqual([
141 'auth_config_get',
142 'auth_docs_ask',
143 'sender_domain_status',
144 ]);
145 });
146
147 test('AUTH_BRIDGE_WRITE_TOOLS lists passwordless enable + mint key', async () => {
148 const { AUTH_BRIDGE_WRITE_TOOLS } = await import('./mcp-auth-bridge.js');
149 expect([...AUTH_BRIDGE_WRITE_TOOLS].sort()).toEqual([
150 'auth_enable_passwordless',
151 'auth_mint_public_key',
152 ]);
153 });
154});