password-reset.test.ts142 lines · main
1/**
2 * Unit tests for sendPasswordReset + resetPassword.
3 * Fetch is mocked — no network calls made.
4 *
5 * Confirmed Better Auth 1.6.9 endpoints:
6 * POST /request-password-reset { email }
7 * POST /reset-password { token, newPassword }
8 */
9
10import { describe, it, expect } from 'bun:test';
11import { createBrivenAuth } from '../index.js';
12
13const PROJECT_ID = 'p_test123';
14const PUBLIC_KEY = 'pk_briven_test';
15const API_ORIGIN = 'https://api.briven.tech';
16const PREFIX = '/v1/auth-tenant';
17
18function makeClient(fetchMock: (url: string | URL | Request, init?: RequestInit) => Promise<Response>) {
19 return createBrivenAuth({
20 projectId: PROJECT_ID,
21 publicKey: PUBLIC_KEY,
22 fetch: fetchMock as typeof fetch,
23 });
24}
25
26function okResponse(body: unknown, status = 200): Response {
27 return new Response(JSON.stringify(body), {
28 status,
29 headers: { 'content-type': 'application/json' },
30 });
31}
32
33// ---------------------------------------------------------------------------
34// sendPasswordReset
35// ---------------------------------------------------------------------------
36
37describe('sendPasswordReset', () => {
38 it('sends POST to /request-password-reset with email in body', async () => {
39 let capturedUrl = '';
40 let capturedMethod = '';
41 let capturedBody: unknown = null;
42 let capturedHeaders: Record<string, string> = {};
43
44 const auth = makeClient(async (url, init) => {
45 capturedUrl = String(url);
46 capturedMethod = (init?.method ?? 'GET').toUpperCase();
47 capturedBody = JSON.parse((init?.body as string) ?? '{}');
48 capturedHeaders = Object.fromEntries(
49 new Headers(init?.headers as HeadersInit).entries(),
50 );
51 return okResponse({ status: true, message: 'check your inbox' });
52 });
53
54 const result = await auth.sendPasswordReset('jane@example.com');
55
56 expect(capturedUrl).toBe(`${API_ORIGIN}${PREFIX}/request-password-reset`);
57 expect(capturedMethod).toBe('POST');
58 expect(capturedBody).toEqual({ email: 'jane@example.com' });
59 expect(capturedHeaders['x-briven-project-id']).toBe(PROJECT_ID);
60 expect(capturedHeaders['authorization']).toBe(`Bearer ${PUBLIC_KEY}`);
61 expect(result).toEqual({ ok: true });
62 });
63
64 it('returns { ok: true } even when server responds with status: true but no user', async () => {
65 const auth = makeClient(async () => okResponse({ status: true }));
66 expect(await auth.sendPasswordReset('x@x.com')).toEqual({ ok: true });
67 });
68
69 it('returns error frame when fetch throws (network failure)', async () => {
70 const auth = makeClient(async () => {
71 throw new TypeError('Failed to fetch');
72 });
73 const result = await auth.sendPasswordReset('x@x.com');
74 expect(result.ok).toBe(false);
75 if (!result.ok) {
76 expect(result.code).toBe('network_error');
77 expect(typeof result.message).toBe('string');
78 }
79 });
80});
81
82// ---------------------------------------------------------------------------
83// resetPassword
84// ---------------------------------------------------------------------------
85
86describe('resetPassword', () => {
87 it('sends POST to /reset-password with token and newPassword', async () => {
88 let capturedUrl = '';
89 let capturedBody: unknown = null;
90
91 const auth = makeClient(async (url, init) => {
92 capturedUrl = String(url);
93 capturedBody = JSON.parse((init?.body as string) ?? '{}');
94 return okResponse({ status: true });
95 });
96
97 const result = await auth.resetPassword({ token: 'tok_abc123', newPassword: 'n3wP@ss!' });
98
99 expect(capturedUrl).toBe(`${API_ORIGIN}${PREFIX}/reset-password`);
100 expect(capturedBody).toEqual({ token: 'tok_abc123', newPassword: 'n3wP@ss!' });
101 expect(result).toEqual({ ok: true });
102 });
103
104 it('returns { ok: true } when server body has status: true', async () => {
105 const auth = makeClient(async () => okResponse({ status: true }));
106 const result = await auth.resetPassword({ token: 't', newPassword: 'pw12345!' });
107 expect(result).toEqual({ ok: true });
108 });
109
110 it('returns { ok: false } with server message when body lacks status: true', async () => {
111 const auth = makeClient(async () =>
112 okResponse({ code: 'INVALID_TOKEN', message: 'token is expired or invalid' }, 400),
113 );
114 const result = await auth.resetPassword({ token: 'bad', newPassword: 'pw' });
115 expect(result.ok).toBe(false);
116 if (!result.ok) {
117 expect(result.message).toBe('token is expired or invalid');
118 }
119 });
120
121 it('returns { ok: false } with nested error.message', async () => {
122 const auth = makeClient(async () =>
123 okResponse({ error: { code: 'PASSWORD_TOO_SHORT', message: 'password too short' } }, 400),
124 );
125 const result = await auth.resetPassword({ token: 't', newPassword: 'a' });
126 expect(result.ok).toBe(false);
127 if (!result.ok) {
128 expect(result.message).toBe('password too short');
129 }
130 });
131
132 it('returns error frame when fetch throws (network failure)', async () => {
133 const auth = makeClient(async () => {
134 throw new TypeError('Failed to fetch');
135 });
136 const result = await auth.resetPassword({ token: 't', newPassword: 'p' });
137 expect(result.ok).toBe(false);
138 if (!result.ok) {
139 expect(result.code).toBe('network_error');
140 }
141 });
142});