firebase-admin-edge 1.0.25 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -42
- package/dist/auth/auth-error-codes.d.ts +32 -0
- package/dist/auth/auth-error-codes.d.ts.map +1 -1
- package/dist/auth/auth-error-codes.js +32 -0
- package/dist/auth/auth-error-codes.js.map +1 -1
- package/dist/auth/email-action-request.d.ts +40 -0
- package/dist/auth/email-action-request.d.ts.map +1 -0
- package/dist/auth/email-action-request.js +80 -0
- package/dist/auth/email-action-request.js.map +1 -0
- package/dist/auth/email-action-request.test.d.ts +2 -0
- package/dist/auth/email-action-request.test.d.ts.map +1 -0
- package/dist/auth/email-action-request.test.js +128 -0
- package/dist/auth/email-action-request.test.js.map +1 -0
- package/dist/auth/firebase-admin-auth.d.ts +67 -9
- package/dist/auth/firebase-admin-auth.d.ts.map +1 -1
- package/dist/auth/firebase-admin-auth.js +491 -13
- package/dist/auth/firebase-admin-auth.js.map +1 -1
- package/dist/auth/firebase-admin-auth.test.js +1015 -6
- package/dist/auth/firebase-admin-auth.test.js.map +1 -1
- package/dist/auth/firebase-auth-endpoints.d.ts +84 -4
- package/dist/auth/firebase-auth-endpoints.d.ts.map +1 -1
- package/dist/auth/firebase-auth-endpoints.js +150 -8
- package/dist/auth/firebase-auth-endpoints.js.map +1 -1
- package/dist/auth/firebase-auth-endpoints.test.js +335 -3
- package/dist/auth/firebase-auth-endpoints.test.js.map +1 -1
- package/dist/auth/user-batch.d.ts +21 -0
- package/dist/auth/user-batch.d.ts.map +1 -0
- package/dist/auth/user-batch.js +49 -0
- package/dist/auth/user-batch.js.map +1 -0
- package/dist/auth/user-batch.test.d.ts +2 -0
- package/dist/auth/user-batch.test.d.ts.map +1 -0
- package/dist/auth/user-batch.test.js +73 -0
- package/dist/auth/user-batch.test.js.map +1 -0
- package/dist/auth/user-import.d.ts +55 -0
- package/dist/auth/user-import.d.ts.map +1 -0
- package/dist/auth/user-import.js +208 -0
- package/dist/auth/user-import.js.map +1 -0
- package/dist/auth/user-import.test.d.ts +2 -0
- package/dist/auth/user-import.test.d.ts.map +1 -0
- package/dist/auth/user-import.test.js +217 -0
- package/dist/auth/user-import.test.js.map +1 -0
- package/dist/auth/user-record.d.ts +69 -0
- package/dist/auth/user-record.d.ts.map +1 -0
- package/dist/auth/user-record.js +104 -0
- package/dist/auth/user-record.js.map +1 -0
- package/dist/auth/user-record.test.d.ts +2 -0
- package/dist/auth/user-record.test.d.ts.map +1 -0
- package/dist/auth/user-record.test.js +198 -0
- package/dist/auth/user-record.test.js.map +1 -0
- package/dist/auth/user-request.d.ts +98 -0
- package/dist/auth/user-request.d.ts.map +1 -0
- package/dist/auth/user-request.js +298 -0
- package/dist/auth/user-request.js.map +1 -0
- package/dist/auth/user-request.test.d.ts +2 -0
- package/dist/auth/user-request.test.d.ts.map +1 -0
- package/dist/auth/user-request.test.js +327 -0
- package/dist/auth/user-request.test.js.map +1 -0
- package/dist/firebase-edge-server.d.ts.map +1 -1
- package/dist/firebase-edge-server.js +6 -2
- package/dist/firebase-edge-server.js.map +1 -1
- package/dist/firebase-edge-server.test.js +29 -0
- package/dist/firebase-edge-server.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
import { FirebaseAdminAuth } from './firebase-admin-auth.js';
|
|
3
|
-
import { getAccountInfo, createSessionCookie as createSessionCookieEndpoint } from './firebase-auth-endpoints.js';
|
|
3
|
+
import { generateEmailActionLink, deleteAccountsAdmin, importAccountsAdmin, getAccountsInfo, createAccountAdmin, updateAccountAdmin, deleteAccountAdmin, downloadAccount, getAccountInfo, createSessionCookie as createSessionCookieEndpoint } from './firebase-auth-endpoints.js';
|
|
4
4
|
import { getToken } from './google-oauth.js';
|
|
5
5
|
import { signJWTCustomToken, verifyJWT, verifySessionJWT } from './firebase-jwt.js';
|
|
6
6
|
import { FirebaseEdgeError, FirebaseAdminAuthErrorInfo, FirebaseEndpointErrorInfo, JWTErrorInfo } from './errors.js';
|
|
7
7
|
vi.mock('./firebase-auth-endpoints.js', () => ({
|
|
8
|
+
generateEmailActionLink: vi.fn(),
|
|
9
|
+
deleteAccountsAdmin: vi.fn(),
|
|
10
|
+
importAccountsAdmin: vi.fn(),
|
|
11
|
+
getAccountsInfo: vi.fn(),
|
|
12
|
+
createAccountAdmin: vi.fn(),
|
|
13
|
+
updateAccountAdmin: vi.fn(),
|
|
14
|
+
deleteAccountAdmin: vi.fn(),
|
|
15
|
+
downloadAccount: vi.fn(),
|
|
8
16
|
getAccountInfo: vi.fn(),
|
|
9
17
|
createSessionCookie: vi.fn()
|
|
10
18
|
}));
|
|
@@ -76,6 +84,784 @@ describe('FirebaseAdminAuth', () => {
|
|
|
76
84
|
afterEach(() => {
|
|
77
85
|
vi.resetAllMocks();
|
|
78
86
|
});
|
|
87
|
+
describe('getUserByProviderUid', () => {
|
|
88
|
+
it.each([
|
|
89
|
+
[
|
|
90
|
+
'google.com',
|
|
91
|
+
'google-123',
|
|
92
|
+
{
|
|
93
|
+
federatedUserId: [
|
|
94
|
+
{ providerId: 'google.com', rawId: 'google-123' }
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
],
|
|
98
|
+
['email', 'test@example.com', { email: ['test@example.com'] }],
|
|
99
|
+
['phone', '+15551234567', { phoneNumber: ['+15551234567'] }]
|
|
100
|
+
])('looks up %s using the existing batch helper', async (providerId, uid, request) => {
|
|
101
|
+
const fetchFn = vi.fn();
|
|
102
|
+
const admin = new FirebaseAdminAuth(serviceAccountKey, 'tenant', fetchFn);
|
|
103
|
+
mockedGetToken.mockResolvedValue({
|
|
104
|
+
data: mockGoogleTokenResponse,
|
|
105
|
+
error: null
|
|
106
|
+
});
|
|
107
|
+
vi.mocked(getAccountsInfo).mockResolvedValue({
|
|
108
|
+
data: {
|
|
109
|
+
users: [
|
|
110
|
+
{
|
|
111
|
+
...mockUserRecord,
|
|
112
|
+
phoneNumber: '+15551234567',
|
|
113
|
+
providerUserInfo: [
|
|
114
|
+
{
|
|
115
|
+
providerId: 'google.com',
|
|
116
|
+
rawId: 'google-123'
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
},
|
|
122
|
+
error: null
|
|
123
|
+
});
|
|
124
|
+
const result = await admin.getUserByProviderUid(providerId, uid);
|
|
125
|
+
expect(result.error).toBeNull();
|
|
126
|
+
expect(result.data?.uid).toBe('uid-1');
|
|
127
|
+
expect(result.data?.toJSON()).toMatchObject({ uid: 'uid-1' });
|
|
128
|
+
expect(getAccountsInfo).toHaveBeenCalledWith(request, 'test-access-token', 'test-project', 'tenant', fetchFn);
|
|
129
|
+
});
|
|
130
|
+
it('returns user-not-found for an unmatched provider UID', async () => {
|
|
131
|
+
vi.spyOn(auth, 'getUsers').mockResolvedValue({
|
|
132
|
+
data: {
|
|
133
|
+
users: [],
|
|
134
|
+
notFound: [
|
|
135
|
+
{ providerId: 'google.com', providerUid: 'missing' }
|
|
136
|
+
]
|
|
137
|
+
},
|
|
138
|
+
error: null
|
|
139
|
+
});
|
|
140
|
+
expect((await auth.getUserByProviderUid('google.com', 'missing')).error
|
|
141
|
+
?.code).toBe(FirebaseAdminAuthErrorInfo.ADMIN_USER_NOT_FOUND.code);
|
|
142
|
+
});
|
|
143
|
+
it.each([
|
|
144
|
+
['', 'uid'],
|
|
145
|
+
['google.com', ''],
|
|
146
|
+
[null, 'uid'],
|
|
147
|
+
['google.com', null],
|
|
148
|
+
['email', 'invalid'],
|
|
149
|
+
['phone', 'invalid']
|
|
150
|
+
])('rejects invalid identifiers %s/%s before authentication', async (providerId, uid) => {
|
|
151
|
+
expect((await auth.getUserByProviderUid(providerId, uid)).error).toBeInstanceOf(FirebaseEdgeError);
|
|
152
|
+
expect(mockedGetToken).not.toHaveBeenCalled();
|
|
153
|
+
});
|
|
154
|
+
it('preserves lookup failures', async () => {
|
|
155
|
+
const error = new FirebaseEdgeError(FirebaseAdminAuthErrorInfo.ADMIN_USER_LOOKUP_FAILED);
|
|
156
|
+
vi.spyOn(auth, 'getUsers').mockResolvedValue({ data: null, error });
|
|
157
|
+
expect(await auth.getUserByProviderUid('google.com', 'uid')).toEqual({ data: null, error });
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
describe('generateVerifyAndChangeEmailLink', () => {
|
|
161
|
+
it.each([undefined, { url: 'https://example.com/account' }])('generates a link with settings %j', async (settings) => {
|
|
162
|
+
const fetchFn = vi.fn();
|
|
163
|
+
const admin = new FirebaseAdminAuth(serviceAccountKey, 'tenant', fetchFn);
|
|
164
|
+
mockedGetToken.mockResolvedValue({
|
|
165
|
+
data: mockGoogleTokenResponse,
|
|
166
|
+
error: null
|
|
167
|
+
});
|
|
168
|
+
vi.mocked(generateEmailActionLink).mockResolvedValue({
|
|
169
|
+
data: 'https://example.com/action',
|
|
170
|
+
error: null
|
|
171
|
+
});
|
|
172
|
+
expect(await admin.generateVerifyAndChangeEmailLink('old@example.com', 'new@example.com', settings)).toEqual({ data: 'https://example.com/action', error: null });
|
|
173
|
+
expect(generateEmailActionLink).toHaveBeenCalledWith('test-project', {
|
|
174
|
+
requestType: 'VERIFY_AND_CHANGE_EMAIL',
|
|
175
|
+
email: 'old@example.com',
|
|
176
|
+
newEmail: 'new@example.com',
|
|
177
|
+
returnOobLink: true,
|
|
178
|
+
...(settings
|
|
179
|
+
? {
|
|
180
|
+
continueUrl: settings.url,
|
|
181
|
+
canHandleCodeInApp: false
|
|
182
|
+
}
|
|
183
|
+
: {})
|
|
184
|
+
}, 'test-access-token', fetchFn, 'tenant');
|
|
185
|
+
});
|
|
186
|
+
it.each([
|
|
187
|
+
['invalid', 'new@example.com'],
|
|
188
|
+
['old@example.com', 'invalid'],
|
|
189
|
+
['old@example.com', undefined]
|
|
190
|
+
])('rejects invalid addresses %s/%s', async (email, newEmail) => {
|
|
191
|
+
const result = await auth.generateVerifyAndChangeEmailLink(email, newEmail);
|
|
192
|
+
expect(result.error?.code).toBe(FirebaseAdminAuthErrorInfo.ADMIN_API_INVALID_ARGUMENT.code);
|
|
193
|
+
expect(mockedGetToken).not.toHaveBeenCalled();
|
|
194
|
+
});
|
|
195
|
+
it('returns credential errors without requesting a link', async () => {
|
|
196
|
+
mockedGetToken.mockResolvedValue({
|
|
197
|
+
data: null,
|
|
198
|
+
error: new Error('credentials')
|
|
199
|
+
});
|
|
200
|
+
expect((await auth.generateVerifyAndChangeEmailLink('old@example.com', 'new@example.com')).error?.code).toBe(FirebaseAdminAuthErrorInfo.ADMIN_SERVICE_ACCOUNT_TOKEN_FAILED
|
|
201
|
+
.code);
|
|
202
|
+
expect(generateEmailActionLink).not.toHaveBeenCalled();
|
|
203
|
+
});
|
|
204
|
+
it('wraps endpoint failures with their cause', async () => {
|
|
205
|
+
const cause = new Error('email already exists');
|
|
206
|
+
mockedGetToken.mockResolvedValue({
|
|
207
|
+
data: mockGoogleTokenResponse,
|
|
208
|
+
error: null
|
|
209
|
+
});
|
|
210
|
+
vi.mocked(generateEmailActionLink).mockResolvedValue({
|
|
211
|
+
data: null,
|
|
212
|
+
error: cause
|
|
213
|
+
});
|
|
214
|
+
const result = await auth.generateVerifyAndChangeEmailLink('old@example.com', 'new@example.com');
|
|
215
|
+
expect(result.error?.code).toBe(FirebaseAdminAuthErrorInfo.ADMIN_EMAIL_ACTION_LINK_FAILED.code);
|
|
216
|
+
expect(result.error?.cause).toBe(cause);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
describe('setCustomUserClaims', () => {
|
|
220
|
+
beforeEach(() => {
|
|
221
|
+
mockedGetToken.mockResolvedValue({
|
|
222
|
+
data: mockGoogleTokenResponse,
|
|
223
|
+
error: null
|
|
224
|
+
});
|
|
225
|
+
vi.mocked(updateAccountAdmin).mockResolvedValue({
|
|
226
|
+
data: { localId: 'uid' },
|
|
227
|
+
error: null
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
it('uses the existing update helper with tenant and custom fetch', async () => {
|
|
231
|
+
const customFetch = vi.fn();
|
|
232
|
+
const instance = new FirebaseAdminAuth(serviceAccountKey, 'tenant', customFetch);
|
|
233
|
+
expect(await instance.setCustomUserClaims('uid', { role: 'editor' })).toEqual({ data: undefined, error: null });
|
|
234
|
+
expect(updateAccountAdmin).toHaveBeenCalledExactlyOnceWith('test-project', 'uid', { customAttributes: '{"role":"editor"}' }, 'test-access-token', customFetch, 'tenant');
|
|
235
|
+
expect(getAccountInfo).not.toHaveBeenCalled();
|
|
236
|
+
});
|
|
237
|
+
it('clears claims with null', async () => {
|
|
238
|
+
expect((await auth.setCustomUserClaims('uid', null)).error).toBeNull();
|
|
239
|
+
expect(updateAccountAdmin).toHaveBeenCalledWith('test-project', 'uid', { customAttributes: '{}' }, 'test-access-token', undefined, undefined);
|
|
240
|
+
});
|
|
241
|
+
it('validates UID and claims before authentication', async () => {
|
|
242
|
+
expect((await auth.setCustomUserClaims('', {})).error).not.toBeNull();
|
|
243
|
+
expect((await auth.setCustomUserClaims('a'.repeat(129), {})).error).not.toBeNull();
|
|
244
|
+
expect((await auth.setCustomUserClaims('uid', { sub: 'reserved' }))
|
|
245
|
+
.error).not.toBeNull();
|
|
246
|
+
expect(mockedGetToken).not.toHaveBeenCalled();
|
|
247
|
+
expect(updateAccountAdmin).not.toHaveBeenCalled();
|
|
248
|
+
});
|
|
249
|
+
it('uses cached credentials', async () => {
|
|
250
|
+
const cache = {
|
|
251
|
+
getCache: vi.fn().mockResolvedValue(mockGoogleTokenResponse),
|
|
252
|
+
setCache: vi.fn()
|
|
253
|
+
};
|
|
254
|
+
const instance = new FirebaseAdminAuth(serviceAccountKey, undefined, undefined, cache);
|
|
255
|
+
expect((await instance.setCustomUserClaims('uid', {})).error).toBeNull();
|
|
256
|
+
expect(mockedGetToken).not.toHaveBeenCalled();
|
|
257
|
+
});
|
|
258
|
+
it('returns credential failures without updating', async () => {
|
|
259
|
+
const error = new FirebaseEdgeError(FirebaseAdminAuthErrorInfo.ADMIN_SERVICE_ACCOUNT_TOKEN_FAILED);
|
|
260
|
+
mockedGetToken.mockResolvedValue({ data: null, error });
|
|
261
|
+
expect(await auth.setCustomUserClaims('uid', {})).toEqual({
|
|
262
|
+
data: null,
|
|
263
|
+
error
|
|
264
|
+
});
|
|
265
|
+
expect(updateAccountAdmin).not.toHaveBeenCalled();
|
|
266
|
+
});
|
|
267
|
+
it('guards against missing access tokens', async () => {
|
|
268
|
+
mockedGetToken.mockResolvedValue({
|
|
269
|
+
data: {},
|
|
270
|
+
error: null
|
|
271
|
+
});
|
|
272
|
+
expect((await auth.setCustomUserClaims('uid', {})).error?.code).toBe('auth/admin-no-token-returned');
|
|
273
|
+
expect(updateAccountAdmin).not.toHaveBeenCalled();
|
|
274
|
+
});
|
|
275
|
+
it('preserves endpoint errors including user-not-found', async () => {
|
|
276
|
+
const cause = new FirebaseEdgeError(FirebaseEndpointErrorInfo.ENDPOINT_USER_NOT_FOUND);
|
|
277
|
+
vi.mocked(updateAccountAdmin).mockResolvedValue({
|
|
278
|
+
data: null,
|
|
279
|
+
error: cause
|
|
280
|
+
});
|
|
281
|
+
const result = await auth.setCustomUserClaims('missing', {});
|
|
282
|
+
expect(result.data).toBeNull();
|
|
283
|
+
expect(result.error?.code).toBe('auth/admin-set-custom-claims-failed');
|
|
284
|
+
expect(result.error?.cause).toBe(cause);
|
|
285
|
+
});
|
|
286
|
+
it('rejects missing confirmation data', async () => {
|
|
287
|
+
vi.mocked(updateAccountAdmin).mockResolvedValue({
|
|
288
|
+
data: { localId: '' },
|
|
289
|
+
error: null
|
|
290
|
+
});
|
|
291
|
+
expect((await auth.setCustomUserClaims('uid', {})).error?.code).toBe('auth/admin-set-custom-claims-failed');
|
|
292
|
+
});
|
|
293
|
+
it('returns network and credential exceptions instead of rejecting', async () => {
|
|
294
|
+
const cause = new Error('offline');
|
|
295
|
+
vi.mocked(updateAccountAdmin).mockRejectedValue(cause);
|
|
296
|
+
expect((await auth.setCustomUserClaims('uid', {})).error?.cause).toBe(cause);
|
|
297
|
+
mockedGetToken.mockRejectedValue('token failure');
|
|
298
|
+
expect((await auth.setCustomUserClaims('uid', {})).error?.cause).toBeInstanceOf(Error);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
describe('phone lookup and bulk user management', () => {
|
|
302
|
+
beforeEach(() => {
|
|
303
|
+
mockedGetToken.mockResolvedValue({
|
|
304
|
+
data: mockGoogleTokenResponse,
|
|
305
|
+
error: null
|
|
306
|
+
});
|
|
307
|
+
mockedGetAccountInfo.mockResolvedValue({
|
|
308
|
+
data: mockUserRecord,
|
|
309
|
+
error: null
|
|
310
|
+
});
|
|
311
|
+
vi.mocked(deleteAccountsAdmin).mockResolvedValue({
|
|
312
|
+
data: {},
|
|
313
|
+
error: null
|
|
314
|
+
});
|
|
315
|
+
vi.mocked(importAccountsAdmin).mockResolvedValue({
|
|
316
|
+
data: {},
|
|
317
|
+
error: null
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
it('looks up a phone number and returns a converted user record', async () => {
|
|
321
|
+
const customFetch = vi.fn();
|
|
322
|
+
const instance = new FirebaseAdminAuth(serviceAccountKey, 'tenant', customFetch);
|
|
323
|
+
const result = await instance.getUserByPhoneNumber('+15555550100');
|
|
324
|
+
expect(result.error).toBeNull();
|
|
325
|
+
expect(result.data?.uid).toBe('uid-1');
|
|
326
|
+
expect(result.data?.toJSON()).toMatchObject({ uid: 'uid-1' });
|
|
327
|
+
expect(getAccountInfo).toHaveBeenCalledWith({ phoneNumber: '+15555550100' }, 'test-access-token', 'test-project', 'tenant', customFetch);
|
|
328
|
+
});
|
|
329
|
+
it('returns user-not-found when no phone match exists', async () => {
|
|
330
|
+
mockedGetAccountInfo.mockResolvedValue({ data: null, error: null });
|
|
331
|
+
expect((await auth.getUserByPhoneNumber('+15555550100')).error?.code).toBe('auth/admin-user-not-found');
|
|
332
|
+
});
|
|
333
|
+
it.each(['', '555', null, 1])('rejects invalid phone number %s before authentication', async (phone) => {
|
|
334
|
+
expect((await auth.getUserByPhoneNumber(phone)).error).toBeInstanceOf(FirebaseEdgeError);
|
|
335
|
+
expect(mockedGetToken).not.toHaveBeenCalled();
|
|
336
|
+
});
|
|
337
|
+
it('deletes a batch with tenant, custom fetch, counts, and indexed errors', async () => {
|
|
338
|
+
const customFetch = vi.fn();
|
|
339
|
+
const instance = new FirebaseAdminAuth(serviceAccountKey, 'tenant', customFetch);
|
|
340
|
+
vi.mocked(deleteAccountsAdmin).mockResolvedValue({
|
|
341
|
+
data: { errors: [{ index: 1, message: 'failure' }] },
|
|
342
|
+
error: null
|
|
343
|
+
});
|
|
344
|
+
const result = await instance.deleteUsers([
|
|
345
|
+
'one',
|
|
346
|
+
'two',
|
|
347
|
+
'missing'
|
|
348
|
+
]);
|
|
349
|
+
expect(result.error).toBeNull();
|
|
350
|
+
expect(result.data).toMatchObject({
|
|
351
|
+
successCount: 2,
|
|
352
|
+
failureCount: 1,
|
|
353
|
+
errors: [{ index: 1, error: { message: 'failure' } }]
|
|
354
|
+
});
|
|
355
|
+
expect(deleteAccountsAdmin).toHaveBeenCalledWith('test-project', ['one', 'two', 'missing'], 'test-access-token', customFetch, 'tenant');
|
|
356
|
+
});
|
|
357
|
+
it('returns empty delete and import results without network requests', async () => {
|
|
358
|
+
const result = {
|
|
359
|
+
data: { successCount: 0, failureCount: 0, errors: [] },
|
|
360
|
+
error: null
|
|
361
|
+
};
|
|
362
|
+
expect(await auth.deleteUsers([])).toEqual(result);
|
|
363
|
+
expect(await auth.importUsers([])).toEqual(result);
|
|
364
|
+
expect(mockedGetToken).not.toHaveBeenCalled();
|
|
365
|
+
});
|
|
366
|
+
it('rejects invalid delete batches and oversized imports before authentication', async () => {
|
|
367
|
+
expect((await auth.deleteUsers(['one', ''])).error).not.toBeNull();
|
|
368
|
+
expect((await auth.deleteUsers(Array(1001).fill('uid'))).error).not.toBeNull();
|
|
369
|
+
expect((await auth.importUsers(Array(1001).fill({ uid: 'uid' }))).error).not.toBeNull();
|
|
370
|
+
expect(mockedGetToken).not.toHaveBeenCalled();
|
|
371
|
+
});
|
|
372
|
+
it('merges local and server import failures using original indices', async () => {
|
|
373
|
+
const customFetch = vi.fn();
|
|
374
|
+
const instance = new FirebaseAdminAuth(serviceAccountKey, 'tenant', customFetch);
|
|
375
|
+
vi.mocked(importAccountsAdmin).mockResolvedValue({
|
|
376
|
+
data: { error: [{ index: 1, message: 'DUPLICATE_LOCAL_ID' }] },
|
|
377
|
+
error: null
|
|
378
|
+
});
|
|
379
|
+
const result = await instance.importUsers([
|
|
380
|
+
{ uid: '' },
|
|
381
|
+
{ uid: 'one' },
|
|
382
|
+
{ uid: 'two' }
|
|
383
|
+
]);
|
|
384
|
+
expect(result.error).toBeNull();
|
|
385
|
+
expect(result.data).toMatchObject({
|
|
386
|
+
successCount: 1,
|
|
387
|
+
failureCount: 2,
|
|
388
|
+
errors: [{ index: 0 }, { index: 2 }]
|
|
389
|
+
});
|
|
390
|
+
expect(importAccountsAdmin).toHaveBeenCalledWith('test-project', { users: [{ localId: 'one' }, { localId: 'two' }] }, 'test-access-token', customFetch, 'tenant');
|
|
391
|
+
});
|
|
392
|
+
it('returns all-invalid imports locally and rejects missing password settings', async () => {
|
|
393
|
+
expect(await auth.importUsers([{ uid: '' }])).toMatchObject({
|
|
394
|
+
data: { successCount: 0, failureCount: 1 },
|
|
395
|
+
error: null
|
|
396
|
+
});
|
|
397
|
+
expect((await auth.importUsers([
|
|
398
|
+
{ uid: 'uid', passwordHash: new Uint8Array([1]) }
|
|
399
|
+
])).error).not.toBeNull();
|
|
400
|
+
expect(mockedGetToken).not.toHaveBeenCalled();
|
|
401
|
+
expect(importAccountsAdmin).not.toHaveBeenCalled();
|
|
402
|
+
});
|
|
403
|
+
it('sends the selected hashing configuration with password imports', async () => {
|
|
404
|
+
await auth.importUsers([{ uid: 'uid', passwordHash: new Uint8Array([1]) }], { hash: { algorithm: 'BCRYPT' } });
|
|
405
|
+
expect(importAccountsAdmin).toHaveBeenCalledWith('test-project', {
|
|
406
|
+
users: [{ localId: 'uid', passwordHash: 'AQ==' }],
|
|
407
|
+
hashAlgorithm: 'BCRYPT'
|
|
408
|
+
}, 'test-access-token', undefined, undefined);
|
|
409
|
+
});
|
|
410
|
+
it('uses cached credentials for all three methods', async () => {
|
|
411
|
+
const cache = {
|
|
412
|
+
getCache: vi.fn().mockResolvedValue(mockGoogleTokenResponse),
|
|
413
|
+
setCache: vi.fn()
|
|
414
|
+
};
|
|
415
|
+
const instance = new FirebaseAdminAuth(serviceAccountKey, undefined, undefined, cache);
|
|
416
|
+
expect((await instance.getUserByPhoneNumber('+15555550100')).error).toBeNull();
|
|
417
|
+
expect((await instance.deleteUsers(['uid'])).error).toBeNull();
|
|
418
|
+
expect((await instance.importUsers([{ uid: 'uid' }])).error).toBeNull();
|
|
419
|
+
expect(mockedGetToken).not.toHaveBeenCalled();
|
|
420
|
+
});
|
|
421
|
+
const operations = [
|
|
422
|
+
{
|
|
423
|
+
name: 'phone',
|
|
424
|
+
run: () => auth.getUserByPhoneNumber('+15555550100'),
|
|
425
|
+
endpoint: vi.mocked(getAccountInfo)
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
name: 'delete',
|
|
429
|
+
run: () => auth.deleteUsers(['uid']),
|
|
430
|
+
endpoint: vi.mocked(deleteAccountsAdmin)
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
name: 'import',
|
|
434
|
+
run: () => auth.importUsers([{ uid: 'uid' }]),
|
|
435
|
+
endpoint: vi.mocked(importAccountsAdmin)
|
|
436
|
+
}
|
|
437
|
+
];
|
|
438
|
+
describe.each(operations)('$name failures', ({ run, endpoint }) => {
|
|
439
|
+
it('propagates token failures without calling the endpoint', async () => {
|
|
440
|
+
const error = new FirebaseEdgeError(FirebaseAdminAuthErrorInfo.ADMIN_SERVICE_ACCOUNT_TOKEN_FAILED);
|
|
441
|
+
mockedGetToken.mockResolvedValue({ data: null, error });
|
|
442
|
+
expect(await run()).toEqual({ data: null, error });
|
|
443
|
+
expect(endpoint).not.toHaveBeenCalled();
|
|
444
|
+
});
|
|
445
|
+
it('guards against a missing access token', async () => {
|
|
446
|
+
mockedGetToken.mockResolvedValue({
|
|
447
|
+
data: {},
|
|
448
|
+
error: null
|
|
449
|
+
});
|
|
450
|
+
expect((await run()).error?.code).toBe('auth/admin-no-token-returned');
|
|
451
|
+
expect(endpoint).not.toHaveBeenCalled();
|
|
452
|
+
});
|
|
453
|
+
it('returns endpoint errors and network exceptions', async () => {
|
|
454
|
+
const error = new FirebaseEdgeError(FirebaseEndpointErrorInfo.ENDPOINT_PERMISSION_DENIED);
|
|
455
|
+
endpoint.mockResolvedValue({ data: null, error });
|
|
456
|
+
expect(await run()).toMatchObject({
|
|
457
|
+
data: null,
|
|
458
|
+
error: expect.any(FirebaseEdgeError)
|
|
459
|
+
});
|
|
460
|
+
endpoint.mockRejectedValue(new Error('offline'));
|
|
461
|
+
expect(await run()).toMatchObject({
|
|
462
|
+
data: null,
|
|
463
|
+
error: expect.any(FirebaseEdgeError)
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
it('returns errors for corrupt bulk responses instead of misleading counts', async () => {
|
|
468
|
+
vi.mocked(deleteAccountsAdmin).mockResolvedValue({
|
|
469
|
+
data: { errors: [{ index: 5 }] },
|
|
470
|
+
error: null
|
|
471
|
+
});
|
|
472
|
+
vi.mocked(importAccountsAdmin).mockResolvedValue({
|
|
473
|
+
data: { error: [{ index: 5 }] },
|
|
474
|
+
error: null
|
|
475
|
+
});
|
|
476
|
+
expect((await auth.deleteUsers(['uid'])).error).not.toBeNull();
|
|
477
|
+
expect((await auth.importUsers([{ uid: 'uid' }])).error).not.toBeNull();
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
describe('getUsers', () => {
|
|
481
|
+
beforeEach(() => {
|
|
482
|
+
mockedGetToken.mockResolvedValue({
|
|
483
|
+
data: mockGoogleTokenResponse,
|
|
484
|
+
error: null
|
|
485
|
+
});
|
|
486
|
+
vi.mocked(getAccountsInfo).mockResolvedValue({
|
|
487
|
+
data: {},
|
|
488
|
+
error: null
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
it('returns records and unmatched identifiers using one batch request', async () => {
|
|
492
|
+
const customFetch = vi.fn();
|
|
493
|
+
const instance = new FirebaseAdminAuth(serviceAccountKey, 'tenant', customFetch);
|
|
494
|
+
const ids = [{ uid: 'uid' }, { email: 'missing@example.com' }];
|
|
495
|
+
vi.mocked(getAccountsInfo).mockResolvedValue({
|
|
496
|
+
data: { users: [{ localId: 'uid' }] },
|
|
497
|
+
error: null
|
|
498
|
+
});
|
|
499
|
+
const { data, error } = await instance.getUsers(ids);
|
|
500
|
+
expect(error).toBeNull();
|
|
501
|
+
expect(data?.users[0]?.uid).toBe('uid');
|
|
502
|
+
expect(data?.notFound).toEqual([ids[1]]);
|
|
503
|
+
expect(getAccountsInfo).toHaveBeenCalledExactlyOnceWith({ localId: ['uid'], email: ['missing@example.com'] }, 'test-access-token', 'test-project', 'tenant', customFetch);
|
|
504
|
+
});
|
|
505
|
+
it('returns an empty batch without authentication or requests', async () => {
|
|
506
|
+
expect(await auth.getUsers([])).toEqual({
|
|
507
|
+
data: { users: [], notFound: [] },
|
|
508
|
+
error: null
|
|
509
|
+
});
|
|
510
|
+
expect(mockedGetToken).not.toHaveBeenCalled();
|
|
511
|
+
expect(getAccountsInfo).not.toHaveBeenCalled();
|
|
512
|
+
});
|
|
513
|
+
it('rejects invalid inputs before authentication', async () => {
|
|
514
|
+
expect((await auth.getUsers([{ uid: '' }])).error).toBeInstanceOf(FirebaseEdgeError);
|
|
515
|
+
expect((await auth.getUsers(Array(101).fill({ uid: 'uid' }))).error).toBeInstanceOf(FirebaseEdgeError);
|
|
516
|
+
expect(mockedGetToken).not.toHaveBeenCalled();
|
|
517
|
+
expect(getAccountsInfo).not.toHaveBeenCalled();
|
|
518
|
+
});
|
|
519
|
+
it('returns all identifiers in notFound when there are no matches', async () => {
|
|
520
|
+
expect(await auth.getUsers([{ uid: 'missing' }])).toEqual({
|
|
521
|
+
data: { users: [], notFound: [{ uid: 'missing' }] },
|
|
522
|
+
error: null
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
it('uses cached credentials', async () => {
|
|
526
|
+
const cache = {
|
|
527
|
+
getCache: vi.fn().mockResolvedValue(mockGoogleTokenResponse),
|
|
528
|
+
setCache: vi.fn()
|
|
529
|
+
};
|
|
530
|
+
const instance = new FirebaseAdminAuth(serviceAccountKey, undefined, undefined, cache);
|
|
531
|
+
expect((await instance.getUsers([{ uid: 'uid' }])).error).toBeNull();
|
|
532
|
+
expect(mockedGetToken).not.toHaveBeenCalled();
|
|
533
|
+
expect(getAccountsInfo).toHaveBeenCalledTimes(1);
|
|
534
|
+
});
|
|
535
|
+
it('returns credential failures without a lookup', async () => {
|
|
536
|
+
const error = new FirebaseEdgeError(FirebaseAdminAuthErrorInfo.ADMIN_SERVICE_ACCOUNT_TOKEN_FAILED);
|
|
537
|
+
mockedGetToken.mockResolvedValue({ data: null, error });
|
|
538
|
+
expect(await auth.getUsers([{ uid: 'uid' }])).toEqual({
|
|
539
|
+
data: null,
|
|
540
|
+
error
|
|
541
|
+
});
|
|
542
|
+
expect(getAccountsInfo).not.toHaveBeenCalled();
|
|
543
|
+
});
|
|
544
|
+
it('guards against missing access tokens', async () => {
|
|
545
|
+
mockedGetToken.mockResolvedValue({
|
|
546
|
+
data: {},
|
|
547
|
+
error: null
|
|
548
|
+
});
|
|
549
|
+
expect((await auth.getUsers([{ uid: 'uid' }])).error?.code).toBe('auth/admin-no-token-returned');
|
|
550
|
+
expect(getAccountsInfo).not.toHaveBeenCalled();
|
|
551
|
+
});
|
|
552
|
+
it('retains mapped endpoint failures as the cause', async () => {
|
|
553
|
+
const cause = new FirebaseEdgeError(FirebaseEndpointErrorInfo.ENDPOINT_PERMISSION_DENIED);
|
|
554
|
+
vi.mocked(getAccountsInfo).mockResolvedValue({
|
|
555
|
+
data: null,
|
|
556
|
+
error: cause
|
|
557
|
+
});
|
|
558
|
+
const result = await auth.getUsers([{ uid: 'uid' }]);
|
|
559
|
+
expect(result.data).toBeNull();
|
|
560
|
+
expect(result.error?.cause).toBe(cause);
|
|
561
|
+
});
|
|
562
|
+
it.each([null, 'not JSON', { users: [{}] }, { users: 'invalid' }])('returns structured errors for malformed responses %j', async (data) => {
|
|
563
|
+
vi.mocked(getAccountsInfo).mockResolvedValue({
|
|
564
|
+
data,
|
|
565
|
+
error: null
|
|
566
|
+
});
|
|
567
|
+
const result = await auth.getUsers([{ uid: 'uid' }]);
|
|
568
|
+
expect(result.data).toBeNull();
|
|
569
|
+
expect(result.error?.code).toBe('auth/admin-user-lookup-failed');
|
|
570
|
+
});
|
|
571
|
+
it('returns network and token exceptions rather than rejecting', async () => {
|
|
572
|
+
vi.mocked(getAccountsInfo).mockRejectedValue(new Error('offline'));
|
|
573
|
+
expect((await auth.getUsers([{ uid: 'uid' }])).error?.cause).toBeInstanceOf(Error);
|
|
574
|
+
mockedGetToken.mockRejectedValue('token failure');
|
|
575
|
+
expect((await auth.getUsers([{ uid: 'uid' }])).error?.cause).toBeInstanceOf(Error);
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
describe('user management', () => {
|
|
579
|
+
beforeEach(() => {
|
|
580
|
+
mockedGetToken.mockResolvedValue({
|
|
581
|
+
data: mockGoogleTokenResponse,
|
|
582
|
+
error: null
|
|
583
|
+
});
|
|
584
|
+
mockedGetAccountInfo.mockResolvedValue({
|
|
585
|
+
data: mockUserRecord,
|
|
586
|
+
error: null
|
|
587
|
+
});
|
|
588
|
+
vi.mocked(createAccountAdmin).mockResolvedValue({
|
|
589
|
+
data: { localId: 'uid-1' },
|
|
590
|
+
error: null
|
|
591
|
+
});
|
|
592
|
+
vi.mocked(updateAccountAdmin).mockResolvedValue({
|
|
593
|
+
data: { localId: 'uid-1' },
|
|
594
|
+
error: null
|
|
595
|
+
});
|
|
596
|
+
vi.mocked(deleteAccountAdmin).mockResolvedValue({ error: null });
|
|
597
|
+
});
|
|
598
|
+
it('creates a user and reads the full record with the same credentials', async () => {
|
|
599
|
+
const customFetch = vi.fn();
|
|
600
|
+
const instance = new FirebaseAdminAuth(serviceAccountKey, 'tenant', customFetch);
|
|
601
|
+
const result = await instance.createUser({
|
|
602
|
+
email: 'test@example.com',
|
|
603
|
+
photoURL: 'https://example.com/photo',
|
|
604
|
+
disabled: false
|
|
605
|
+
});
|
|
606
|
+
expect(result.error).toBeNull();
|
|
607
|
+
expect(result.data?.uid).toBe('uid-1');
|
|
608
|
+
expect(result.data?.toJSON()).toMatchObject({
|
|
609
|
+
uid: 'uid-1',
|
|
610
|
+
email: 'test@example.com'
|
|
611
|
+
});
|
|
612
|
+
expect(createAccountAdmin).toHaveBeenCalledWith('test-project', {
|
|
613
|
+
email: 'test@example.com',
|
|
614
|
+
photoUrl: 'https://example.com/photo',
|
|
615
|
+
disabled: false
|
|
616
|
+
}, 'test-access-token', customFetch, 'tenant');
|
|
617
|
+
expect(mockedGetAccountInfo).toHaveBeenCalledWith({ uid: 'uid-1' }, 'test-access-token', 'test-project', 'tenant', customFetch);
|
|
618
|
+
expect(mockedGetToken).toHaveBeenCalledTimes(1);
|
|
619
|
+
});
|
|
620
|
+
it('updates through the existing endpoint with translated fields and tenant', async () => {
|
|
621
|
+
const result = await authWithTenant.updateUser('uid-1', {
|
|
622
|
+
displayName: null,
|
|
623
|
+
photoURL: null,
|
|
624
|
+
phoneNumber: null,
|
|
625
|
+
disabled: true
|
|
626
|
+
});
|
|
627
|
+
expect(result.error).toBeNull();
|
|
628
|
+
expect(result.data?.uid).toBe('uid-1');
|
|
629
|
+
expect(updateAccountAdmin).toHaveBeenCalledWith('test-project', 'uid-1', {
|
|
630
|
+
deleteAttribute: ['DISPLAY_NAME', 'PHOTO_URL'],
|
|
631
|
+
deleteProvider: ['phone'],
|
|
632
|
+
disableUser: true
|
|
633
|
+
}, 'test-access-token', undefined, 'test-tenant-id');
|
|
634
|
+
expect(mockedGetAccountInfo).toHaveBeenCalledWith({ uid: 'uid-1' }, 'test-access-token', 'test-project', 'test-tenant-id', undefined);
|
|
635
|
+
});
|
|
636
|
+
it('deletes without a follow-up lookup and returns void data', async () => {
|
|
637
|
+
expect(await authWithTenant.deleteUser('uid-1')).toEqual({
|
|
638
|
+
data: undefined,
|
|
639
|
+
error: null
|
|
640
|
+
});
|
|
641
|
+
expect(deleteAccountAdmin).toHaveBeenCalledWith('test-project', 'uid-1', 'test-access-token', undefined, 'test-tenant-id');
|
|
642
|
+
expect(mockedGetAccountInfo).not.toHaveBeenCalled();
|
|
643
|
+
});
|
|
644
|
+
it.each(['', 'a'.repeat(129), null])('rejects invalid UID %s before authentication', async (uid) => {
|
|
645
|
+
expect((await auth.updateUser(uid, {})).error).toBeInstanceOf(FirebaseEdgeError);
|
|
646
|
+
expect((await auth.deleteUser(uid)).error).toBeInstanceOf(FirebaseEdgeError);
|
|
647
|
+
expect(mockedGetToken).not.toHaveBeenCalled();
|
|
648
|
+
});
|
|
649
|
+
it('rejects invalid create and update properties before authentication', async () => {
|
|
650
|
+
expect((await auth.createUser({ email: 'invalid' })).error).toBeInstanceOf(FirebaseEdgeError);
|
|
651
|
+
expect((await auth.updateUser('uid', { password: 'short' })).error).toBeInstanceOf(FirebaseEdgeError);
|
|
652
|
+
expect(mockedGetToken).not.toHaveBeenCalled();
|
|
653
|
+
});
|
|
654
|
+
const operations = [
|
|
655
|
+
{
|
|
656
|
+
name: 'create',
|
|
657
|
+
run: () => auth.createUser({}),
|
|
658
|
+
endpoint: vi.mocked(createAccountAdmin)
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
name: 'update',
|
|
662
|
+
run: () => auth.updateUser('uid-1', {}),
|
|
663
|
+
endpoint: vi.mocked(updateAccountAdmin)
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
name: 'delete',
|
|
667
|
+
run: () => auth.deleteUser('uid-1'),
|
|
668
|
+
endpoint: vi.mocked(deleteAccountAdmin)
|
|
669
|
+
}
|
|
670
|
+
];
|
|
671
|
+
describe.each(operations)('$name errors', ({ name, run, endpoint }) => {
|
|
672
|
+
it('returns credential errors without a write', async () => {
|
|
673
|
+
const error = new FirebaseEdgeError(FirebaseAdminAuthErrorInfo.ADMIN_SERVICE_ACCOUNT_TOKEN_FAILED);
|
|
674
|
+
mockedGetToken.mockResolvedValue({ data: null, error });
|
|
675
|
+
expect(await run()).toEqual({ data: null, error });
|
|
676
|
+
expect(endpoint).not.toHaveBeenCalled();
|
|
677
|
+
});
|
|
678
|
+
it('guards against missing credentials', async () => {
|
|
679
|
+
mockedGetToken.mockResolvedValue({
|
|
680
|
+
data: {},
|
|
681
|
+
error: null
|
|
682
|
+
});
|
|
683
|
+
expect((await run()).error?.code).toBe('auth/admin-no-token-returned');
|
|
684
|
+
expect(endpoint).not.toHaveBeenCalled();
|
|
685
|
+
});
|
|
686
|
+
it('preserves API errors as the cause without a readback', async () => {
|
|
687
|
+
const cause = new FirebaseEdgeError(FirebaseEndpointErrorInfo.ENDPOINT_USER_NOT_FOUND);
|
|
688
|
+
endpoint.mockResolvedValue({ data: null, error: cause });
|
|
689
|
+
const result = await run();
|
|
690
|
+
expect(result.data).toBeNull();
|
|
691
|
+
expect(result.error?.code).toBe(`auth/admin-${name}-user-failed`);
|
|
692
|
+
expect(result.error?.cause).toBe(cause);
|
|
693
|
+
expect(mockedGetAccountInfo).not.toHaveBeenCalled();
|
|
694
|
+
});
|
|
695
|
+
it('returns transport exceptions instead of rejecting', async () => {
|
|
696
|
+
const cause = new Error('offline');
|
|
697
|
+
endpoint.mockRejectedValue(cause);
|
|
698
|
+
const result = await run();
|
|
699
|
+
expect(result.data).toBeNull();
|
|
700
|
+
expect(result.error?.cause).toBe(cause);
|
|
701
|
+
});
|
|
702
|
+
it('returns unexpected token exceptions', async () => {
|
|
703
|
+
mockedGetToken.mockRejectedValue('token failure');
|
|
704
|
+
const result = await run();
|
|
705
|
+
expect(result.data).toBeNull();
|
|
706
|
+
expect(result.error?.code).toBe(`auth/admin-${name}-user-failed`);
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
describe.each(operations.slice(0, 2))('$name readback', ({ run, endpoint }) => {
|
|
710
|
+
it('requires a UID in the write response', async () => {
|
|
711
|
+
endpoint.mockResolvedValue({
|
|
712
|
+
data: { localId: '' },
|
|
713
|
+
error: null
|
|
714
|
+
});
|
|
715
|
+
expect((await run()).error).toBeInstanceOf(FirebaseEdgeError);
|
|
716
|
+
expect(mockedGetAccountInfo).not.toHaveBeenCalled();
|
|
717
|
+
});
|
|
718
|
+
it('returns lookup errors', async () => {
|
|
719
|
+
const error = new FirebaseEdgeError(FirebaseEndpointErrorInfo.ENDPOINT_USER_NOT_FOUND);
|
|
720
|
+
mockedGetAccountInfo.mockResolvedValue({
|
|
721
|
+
data: null,
|
|
722
|
+
error
|
|
723
|
+
});
|
|
724
|
+
expect(await run()).toEqual({ data: null, error });
|
|
725
|
+
});
|
|
726
|
+
it('handles a missing user in the lookup response', async () => {
|
|
727
|
+
mockedGetAccountInfo.mockResolvedValue({
|
|
728
|
+
data: null,
|
|
729
|
+
error: null
|
|
730
|
+
});
|
|
731
|
+
expect((await run()).error?.code).toBe('auth/admin-user-record-not-found');
|
|
732
|
+
});
|
|
733
|
+
it('returns conversion errors rather than rejecting', async () => {
|
|
734
|
+
mockedGetAccountInfo.mockResolvedValue({
|
|
735
|
+
data: { localId: 'uid', customAttributes: '{' },
|
|
736
|
+
error: null
|
|
737
|
+
});
|
|
738
|
+
expect((await run()).error).toBeInstanceOf(FirebaseEdgeError);
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
});
|
|
742
|
+
describe('listUsers', () => {
|
|
743
|
+
const download = vi.mocked(downloadAccount);
|
|
744
|
+
beforeEach(() => {
|
|
745
|
+
mockedGetToken.mockResolvedValue({
|
|
746
|
+
data: mockGoogleTokenResponse,
|
|
747
|
+
error: null
|
|
748
|
+
});
|
|
749
|
+
download.mockResolvedValue({ data: {}, error: null });
|
|
750
|
+
});
|
|
751
|
+
it('requests the default page and converts the result', async () => {
|
|
752
|
+
download.mockResolvedValue({
|
|
753
|
+
data: { users: [{ localId: 'uid' }], nextPageToken: 'next' },
|
|
754
|
+
error: null
|
|
755
|
+
});
|
|
756
|
+
const { data, error } = await auth.listUsers();
|
|
757
|
+
expect(error).toBeNull();
|
|
758
|
+
expect(data?.pageToken).toBe('next');
|
|
759
|
+
expect(data?.users[0]).toMatchObject({
|
|
760
|
+
uid: 'uid',
|
|
761
|
+
emailVerified: false,
|
|
762
|
+
disabled: false
|
|
763
|
+
});
|
|
764
|
+
expect(download).toHaveBeenCalledExactlyOnceWith('test-access-token', 'test-project', 1000, undefined, undefined, undefined);
|
|
765
|
+
});
|
|
766
|
+
it('forwards pagination, tenant, and custom fetch to the endpoint', async () => {
|
|
767
|
+
const customFetch = vi.fn();
|
|
768
|
+
const instance = new FirebaseAdminAuth(serviceAccountKey, 'tenant', customFetch);
|
|
769
|
+
expect(await instance.listUsers(25, 'opaque+/=')).toEqual({
|
|
770
|
+
data: { users: [] },
|
|
771
|
+
error: null
|
|
772
|
+
});
|
|
773
|
+
expect(download).toHaveBeenCalledExactlyOnceWith('test-access-token', 'test-project', 25, 'opaque+/=', 'tenant', customFetch);
|
|
774
|
+
expect(mockedGetToken).toHaveBeenCalledWith(serviceAccountKey, customFetch);
|
|
775
|
+
});
|
|
776
|
+
it('defaults an undefined page size and omits an absent page token', async () => {
|
|
777
|
+
expect(await auth.listUsers(undefined)).toEqual({
|
|
778
|
+
data: { users: [] },
|
|
779
|
+
error: null
|
|
780
|
+
});
|
|
781
|
+
});
|
|
782
|
+
it('preserves an explicitly returned empty page token', async () => {
|
|
783
|
+
download.mockResolvedValue({
|
|
784
|
+
data: { nextPageToken: '' },
|
|
785
|
+
error: null
|
|
786
|
+
});
|
|
787
|
+
expect(await auth.listUsers()).toEqual({
|
|
788
|
+
data: { users: [], pageToken: '' },
|
|
789
|
+
error: null
|
|
790
|
+
});
|
|
791
|
+
});
|
|
792
|
+
it.each([0, -1, 1001, 1.5, NaN, Infinity, null, '10'])('rejects invalid page size %s before authentication', async (size) => {
|
|
793
|
+
const result = await auth.listUsers(size);
|
|
794
|
+
expect(result.data).toBeNull();
|
|
795
|
+
expect(result.error?.code).toBe('auth/admin-api-invalid-argument');
|
|
796
|
+
expect(mockedGetToken).not.toHaveBeenCalled();
|
|
797
|
+
expect(download).not.toHaveBeenCalled();
|
|
798
|
+
});
|
|
799
|
+
it.each(['', null, 123, {}])('rejects invalid page token %s before authentication', async (pageToken) => {
|
|
800
|
+
const result = await auth.listUsers(25, pageToken);
|
|
801
|
+
expect(result.data).toBeNull();
|
|
802
|
+
expect(result.error?.code).toBe('auth/admin-invalid-page-token');
|
|
803
|
+
expect(mockedGetToken).not.toHaveBeenCalled();
|
|
804
|
+
expect(download).not.toHaveBeenCalled();
|
|
805
|
+
});
|
|
806
|
+
it('returns authentication errors without calling the endpoint', async () => {
|
|
807
|
+
const error = new FirebaseEdgeError(FirebaseAdminAuthErrorInfo.ADMIN_SERVICE_ACCOUNT_TOKEN_FAILED);
|
|
808
|
+
mockedGetToken.mockResolvedValue({ data: null, error });
|
|
809
|
+
expect(await auth.listUsers()).toEqual({ data: null, error });
|
|
810
|
+
expect(download).not.toHaveBeenCalled();
|
|
811
|
+
});
|
|
812
|
+
it('uses cached credentials', async () => {
|
|
813
|
+
const cache = {
|
|
814
|
+
getCache: vi.fn().mockResolvedValue(mockGoogleTokenResponse),
|
|
815
|
+
setCache: vi.fn()
|
|
816
|
+
};
|
|
817
|
+
const instance = new FirebaseAdminAuth(serviceAccountKey, undefined, undefined, cache);
|
|
818
|
+
expect((await instance.listUsers()).error).toBeNull();
|
|
819
|
+
expect(mockedGetToken).not.toHaveBeenCalled();
|
|
820
|
+
expect(cache.getCache).toHaveBeenCalledWith('__cache');
|
|
821
|
+
expect(download).toHaveBeenCalledWith('test-access-token', 'test-project', 1000, undefined, undefined, undefined);
|
|
822
|
+
});
|
|
823
|
+
it('guards against a missing access token', async () => {
|
|
824
|
+
mockedGetToken.mockResolvedValue({
|
|
825
|
+
data: {},
|
|
826
|
+
error: null
|
|
827
|
+
});
|
|
828
|
+
expect((await auth.listUsers()).error?.code).toBe('auth/admin-no-token-returned');
|
|
829
|
+
expect(download).not.toHaveBeenCalled();
|
|
830
|
+
});
|
|
831
|
+
it('retains a mapped endpoint error as its cause', async () => {
|
|
832
|
+
const cause = new FirebaseEdgeError(FirebaseEndpointErrorInfo.ENDPOINT_PERMISSION_DENIED);
|
|
833
|
+
download.mockResolvedValue({ data: null, error: cause });
|
|
834
|
+
const result = await auth.listUsers();
|
|
835
|
+
expect(result.data).toBeNull();
|
|
836
|
+
expect(result.error?.code).toBe('auth/admin-list-users-failed');
|
|
837
|
+
expect(result.error?.cause).toBe(cause);
|
|
838
|
+
});
|
|
839
|
+
it.each([
|
|
840
|
+
null,
|
|
841
|
+
'not JSON',
|
|
842
|
+
{ users: [{ localId: 'uid', customAttributes: '{' }] },
|
|
843
|
+
{ users: [{}] },
|
|
844
|
+
{ users: 'invalid' }
|
|
845
|
+
])('returns an error for invalid endpoint data %j', async (data) => {
|
|
846
|
+
download.mockResolvedValue({ data, error: null });
|
|
847
|
+
const result = await auth.listUsers();
|
|
848
|
+
expect(result.data).toBeNull();
|
|
849
|
+
expect(result.error?.code).toBe('auth/admin-list-users-failed');
|
|
850
|
+
});
|
|
851
|
+
it('returns unexpected endpoint failures rather than rejecting', async () => {
|
|
852
|
+
const cause = new Error('offline');
|
|
853
|
+
download.mockRejectedValue(cause);
|
|
854
|
+
const result = await auth.listUsers();
|
|
855
|
+
expect(result.data).toBeNull();
|
|
856
|
+
expect(result.error?.cause).toBe(cause);
|
|
857
|
+
});
|
|
858
|
+
it('normalizes unexpected authentication failures', async () => {
|
|
859
|
+
mockedGetToken.mockRejectedValue('token failure');
|
|
860
|
+
const result = await auth.listUsers();
|
|
861
|
+
expect(result.data).toBeNull();
|
|
862
|
+
expect(result.error?.cause).toBeInstanceOf(Error);
|
|
863
|
+
});
|
|
864
|
+
});
|
|
79
865
|
describe('getUser', () => {
|
|
80
866
|
it('returns error when getToken returns an error', async () => {
|
|
81
867
|
mockedGetToken.mockResolvedValueOnce({
|
|
@@ -298,13 +1084,186 @@ describe('FirebaseAdminAuth', () => {
|
|
|
298
1084
|
expect(result.error).toBeNull();
|
|
299
1085
|
});
|
|
300
1086
|
});
|
|
1087
|
+
describe.each([
|
|
1088
|
+
['generatePasswordResetLink', 'PASSWORD_RESET'],
|
|
1089
|
+
['generateEmailVerificationLink', 'VERIFY_EMAIL'],
|
|
1090
|
+
['generateSignInWithEmailLink', 'EMAIL_SIGNIN']
|
|
1091
|
+
])('%s', (method, requestType) => {
|
|
1092
|
+
const settings = {
|
|
1093
|
+
url: 'https://example.com/finish',
|
|
1094
|
+
handleCodeInApp: true
|
|
1095
|
+
};
|
|
1096
|
+
it('returns the generated link and forwards credentials, settings, tenant and fetch', async () => {
|
|
1097
|
+
const fetchFn = vi.fn();
|
|
1098
|
+
const admin = new FirebaseAdminAuth(serviceAccountKey, 'tenant', fetchFn);
|
|
1099
|
+
mockedGetToken.mockResolvedValue({
|
|
1100
|
+
data: mockGoogleTokenResponse,
|
|
1101
|
+
error: null
|
|
1102
|
+
});
|
|
1103
|
+
vi.mocked(generateEmailActionLink).mockResolvedValue({
|
|
1104
|
+
data: 'https://example.com/action',
|
|
1105
|
+
error: null
|
|
1106
|
+
});
|
|
1107
|
+
expect(await admin[method]('person@example.com', settings)).toEqual({ data: 'https://example.com/action', error: null });
|
|
1108
|
+
expect(generateEmailActionLink).toHaveBeenCalledWith('test-project', {
|
|
1109
|
+
requestType,
|
|
1110
|
+
email: 'person@example.com',
|
|
1111
|
+
returnOobLink: true,
|
|
1112
|
+
continueUrl: settings.url,
|
|
1113
|
+
canHandleCodeInApp: true
|
|
1114
|
+
}, 'test-access-token', fetchFn, 'tenant');
|
|
1115
|
+
});
|
|
1116
|
+
it('rejects invalid input before obtaining credentials', async () => {
|
|
1117
|
+
expect((await auth[method]('invalid', settings)).error?.code).toBe(FirebaseAdminAuthErrorInfo.ADMIN_API_INVALID_ARGUMENT.code);
|
|
1118
|
+
expect(mockedGetToken).not.toHaveBeenCalled();
|
|
1119
|
+
expect(generateEmailActionLink).not.toHaveBeenCalled();
|
|
1120
|
+
});
|
|
1121
|
+
it('returns authentication errors without calling the endpoint', async () => {
|
|
1122
|
+
mockedGetToken.mockResolvedValue({
|
|
1123
|
+
data: null,
|
|
1124
|
+
error: new Error('credentials')
|
|
1125
|
+
});
|
|
1126
|
+
expect((await auth[method]('person@example.com', settings)).error?.code).toBe(FirebaseAdminAuthErrorInfo.ADMIN_SERVICE_ACCOUNT_TOKEN_FAILED
|
|
1127
|
+
.code);
|
|
1128
|
+
expect(generateEmailActionLink).not.toHaveBeenCalled();
|
|
1129
|
+
});
|
|
1130
|
+
it('wraps endpoint errors and preserves the cause', async () => {
|
|
1131
|
+
const cause = new Error('email not found');
|
|
1132
|
+
mockedGetToken.mockResolvedValue({
|
|
1133
|
+
data: mockGoogleTokenResponse,
|
|
1134
|
+
error: null
|
|
1135
|
+
});
|
|
1136
|
+
vi.mocked(generateEmailActionLink).mockResolvedValue({
|
|
1137
|
+
data: null,
|
|
1138
|
+
error: cause
|
|
1139
|
+
});
|
|
1140
|
+
const result = await auth[method]('person@example.com', settings);
|
|
1141
|
+
expect(result.error?.code).toBe(FirebaseAdminAuthErrorInfo.ADMIN_EMAIL_ACTION_LINK_FAILED.code);
|
|
1142
|
+
expect(result.error?.cause).toBe(cause);
|
|
1143
|
+
});
|
|
1144
|
+
it('returns unexpected failures using the package error convention', async () => {
|
|
1145
|
+
mockedGetToken.mockResolvedValue({
|
|
1146
|
+
data: mockGoogleTokenResponse,
|
|
1147
|
+
error: null
|
|
1148
|
+
});
|
|
1149
|
+
vi.mocked(generateEmailActionLink).mockRejectedValue(new Error('network'));
|
|
1150
|
+
expect((await auth[method]('person@example.com', settings)).error?.code).toBe(FirebaseAdminAuthErrorInfo.ADMIN_EMAIL_ACTION_LINK_FAILED.code);
|
|
1151
|
+
});
|
|
1152
|
+
});
|
|
1153
|
+
it.each([
|
|
1154
|
+
'generatePasswordResetLink',
|
|
1155
|
+
'generateEmailVerificationLink'
|
|
1156
|
+
])('%s allows omitted settings', async (method) => {
|
|
1157
|
+
mockedGetToken.mockResolvedValue({
|
|
1158
|
+
data: mockGoogleTokenResponse,
|
|
1159
|
+
error: null
|
|
1160
|
+
});
|
|
1161
|
+
vi.mocked(generateEmailActionLink).mockResolvedValue({
|
|
1162
|
+
data: 'https://example.com/action',
|
|
1163
|
+
error: null
|
|
1164
|
+
});
|
|
1165
|
+
expect((await auth[method]('person@example.com')).error).toBeNull();
|
|
1166
|
+
expect(generateEmailActionLink).toHaveBeenCalledWith('test-project', expect.not.objectContaining({ continueUrl: expect.anything() }), 'test-access-token', undefined, undefined);
|
|
1167
|
+
});
|
|
1168
|
+
it('requires settings when generating an email sign-in link', async () => {
|
|
1169
|
+
const result =
|
|
1170
|
+
// @ts-expect-error Settings are required for email sign-in.
|
|
1171
|
+
await auth.generateSignInWithEmailLink('person@example.com');
|
|
1172
|
+
expect(result.error?.code).toBe(FirebaseAdminAuthErrorInfo.ADMIN_API_INVALID_ARGUMENT.code);
|
|
1173
|
+
expect(mockedGetToken).not.toHaveBeenCalled();
|
|
1174
|
+
});
|
|
1175
|
+
it.each([
|
|
1176
|
+
'generatePasswordResetLink',
|
|
1177
|
+
'generateEmailVerificationLink',
|
|
1178
|
+
'generateSignInWithEmailLink',
|
|
1179
|
+
'createSessionCookie'
|
|
1180
|
+
])('%s rejects missing access tokens', async (method) => {
|
|
1181
|
+
mockedGetToken.mockResolvedValue({
|
|
1182
|
+
data: { ...mockGoogleTokenResponse, access_token: '' },
|
|
1183
|
+
error: null
|
|
1184
|
+
});
|
|
1185
|
+
const result = method === 'createSessionCookie'
|
|
1186
|
+
? await auth.createSessionCookie('id-token', {
|
|
1187
|
+
expiresIn: 300000
|
|
1188
|
+
})
|
|
1189
|
+
: await auth[method]('person@example.com', {
|
|
1190
|
+
url: 'https://example.com'
|
|
1191
|
+
});
|
|
1192
|
+
expect(result.error?.code).toBe(FirebaseAdminAuthErrorInfo.ADMIN_NO_TOKEN_RETURNED.code);
|
|
1193
|
+
expect(generateEmailActionLink).not.toHaveBeenCalled();
|
|
1194
|
+
expect(mockedCreateSessionCookieEndpoint).not.toHaveBeenCalled();
|
|
1195
|
+
});
|
|
301
1196
|
describe('createSessionCookie', () => {
|
|
1197
|
+
it.each([
|
|
1198
|
+
undefined,
|
|
1199
|
+
null,
|
|
1200
|
+
3600000,
|
|
1201
|
+
{},
|
|
1202
|
+
{ expiresIn: '3600000' },
|
|
1203
|
+
{ expiresIn: NaN },
|
|
1204
|
+
{ expiresIn: Infinity },
|
|
1205
|
+
{ expiresIn: 299999 },
|
|
1206
|
+
{ expiresIn: 1209600001 }
|
|
1207
|
+
])('rejects invalid options %j before authentication', async (options) => {
|
|
1208
|
+
const result = await auth.createSessionCookie('id-token', options);
|
|
1209
|
+
expect(result.error?.code).toBe(FirebaseAdminAuthErrorInfo
|
|
1210
|
+
.ADMIN_SESSION_COOKIE_DURATION_INVALID.code);
|
|
1211
|
+
expect(mockedGetToken).not.toHaveBeenCalled();
|
|
1212
|
+
});
|
|
1213
|
+
it.each(['', null, 1])('rejects invalid ID token %s', async (idToken) => {
|
|
1214
|
+
const result = await auth.createSessionCookie(idToken, { expiresIn: 300000 });
|
|
1215
|
+
expect(result.error?.code).toBe(FirebaseAdminAuthErrorInfo.ADMIN_ID_TOKEN_INVALID.code);
|
|
1216
|
+
expect(mockedGetToken).not.toHaveBeenCalled();
|
|
1217
|
+
});
|
|
1218
|
+
it.each([300000, 1209600000])('accepts boundary lifetime %s and forwards tenant and fetch', async (expiresIn) => {
|
|
1219
|
+
const fetchFn = vi.fn();
|
|
1220
|
+
const admin = new FirebaseAdminAuth(serviceAccountKey, 'tenant', fetchFn);
|
|
1221
|
+
mockedGetToken.mockResolvedValue({
|
|
1222
|
+
data: mockGoogleTokenResponse,
|
|
1223
|
+
error: null
|
|
1224
|
+
});
|
|
1225
|
+
mockedCreateSessionCookieEndpoint.mockResolvedValue({
|
|
1226
|
+
data: 'cookie',
|
|
1227
|
+
error: null
|
|
1228
|
+
});
|
|
1229
|
+
expect(await admin.createSessionCookie('id-token', { expiresIn })).toEqual({ data: 'cookie', error: null });
|
|
1230
|
+
expect(mockedCreateSessionCookieEndpoint).toHaveBeenCalledWith('id-token', 'test-access-token', 'test-project', expiresIn, 'tenant', fetchFn);
|
|
1231
|
+
});
|
|
1232
|
+
it('returns an error for an empty cookie response', async () => {
|
|
1233
|
+
mockedGetToken.mockResolvedValue({
|
|
1234
|
+
data: mockGoogleTokenResponse,
|
|
1235
|
+
error: null
|
|
1236
|
+
});
|
|
1237
|
+
mockedCreateSessionCookieEndpoint.mockResolvedValue({
|
|
1238
|
+
data: null,
|
|
1239
|
+
error: null
|
|
1240
|
+
});
|
|
1241
|
+
const result = await auth.createSessionCookie('id-token', {
|
|
1242
|
+
expiresIn: 300000
|
|
1243
|
+
});
|
|
1244
|
+
expect(result.error?.code).toBe(FirebaseAdminAuthErrorInfo.ADMIN_SESSION_COOKIE_CREATE_FAILED
|
|
1245
|
+
.code);
|
|
1246
|
+
});
|
|
1247
|
+
it('wraps unexpected endpoint failures', async () => {
|
|
1248
|
+
mockedGetToken.mockResolvedValue({
|
|
1249
|
+
data: mockGoogleTokenResponse,
|
|
1250
|
+
error: null
|
|
1251
|
+
});
|
|
1252
|
+
mockedCreateSessionCookieEndpoint.mockRejectedValue(new Error('network'));
|
|
1253
|
+
const result = await auth.createSessionCookie('id-token', {
|
|
1254
|
+
expiresIn: 300000
|
|
1255
|
+
});
|
|
1256
|
+
expect(result.error?.code).toBe(FirebaseAdminAuthErrorInfo.ADMIN_SESSION_COOKIE_CREATE_FAILED
|
|
1257
|
+
.code);
|
|
1258
|
+
});
|
|
302
1259
|
it('returns error when getToken fails', async () => {
|
|
303
1260
|
mockedGetToken.mockResolvedValueOnce({
|
|
304
1261
|
data: null,
|
|
305
1262
|
error: new Error('unauthorized')
|
|
306
1263
|
});
|
|
307
|
-
const result = await auth.createSessionCookie('id-token',
|
|
1264
|
+
const result = await auth.createSessionCookie('id-token', {
|
|
1265
|
+
expiresIn: 3600000
|
|
1266
|
+
});
|
|
308
1267
|
expect(result.data).toBeNull();
|
|
309
1268
|
expect(result.error).toBeInstanceOf(FirebaseEdgeError);
|
|
310
1269
|
expect(result.error.code).toBe(FirebaseAdminAuthErrorInfo.ADMIN_SERVICE_ACCOUNT_TOKEN_FAILED
|
|
@@ -315,7 +1274,9 @@ describe('FirebaseAdminAuth', () => {
|
|
|
315
1274
|
data: null,
|
|
316
1275
|
error: new FirebaseEdgeError(FirebaseAdminAuthErrorInfo.ADMIN_SERVICE_ACCOUNT_TOKEN_FAILED)
|
|
317
1276
|
});
|
|
318
|
-
const result = await auth.createSessionCookie('id-token',
|
|
1277
|
+
const result = await auth.createSessionCookie('id-token', {
|
|
1278
|
+
expiresIn: 3600000
|
|
1279
|
+
});
|
|
319
1280
|
expect(result.data).toBeNull();
|
|
320
1281
|
expect(result.error).toBeInstanceOf(FirebaseEdgeError);
|
|
321
1282
|
expect(result.error.code).toBe(FirebaseAdminAuthErrorInfo.ADMIN_SERVICE_ACCOUNT_TOKEN_FAILED
|
|
@@ -331,8 +1292,10 @@ describe('FirebaseAdminAuth', () => {
|
|
|
331
1292
|
data: null,
|
|
332
1293
|
error: endpointError
|
|
333
1294
|
});
|
|
334
|
-
const result = await auth.createSessionCookie('id-token',
|
|
335
|
-
|
|
1295
|
+
const result = await auth.createSessionCookie('id-token', {
|
|
1296
|
+
expiresIn: 3600000
|
|
1297
|
+
});
|
|
1298
|
+
expect(mockedCreateSessionCookieEndpoint).toHaveBeenCalledWith('id-token', 'test-access-token', 'test-project', 3600000, undefined, undefined);
|
|
336
1299
|
expect(result.data).toBeNull();
|
|
337
1300
|
expect(result.error).toBeInstanceOf(FirebaseEdgeError);
|
|
338
1301
|
expect(result.error.code).toBe(FirebaseAdminAuthErrorInfo.ADMIN_SESSION_COOKIE_CREATE_FAILED
|
|
@@ -348,12 +1311,58 @@ describe('FirebaseAdminAuth', () => {
|
|
|
348
1311
|
data: cookieData,
|
|
349
1312
|
error: null
|
|
350
1313
|
});
|
|
351
|
-
const result = await auth.createSessionCookie('id-token',
|
|
1314
|
+
const result = await auth.createSessionCookie('id-token', {
|
|
1315
|
+
expiresIn: 3600000
|
|
1316
|
+
});
|
|
352
1317
|
expect(result.data).toEqual(cookieData);
|
|
353
1318
|
expect(result.error).toBeNull();
|
|
354
1319
|
});
|
|
355
1320
|
});
|
|
356
1321
|
describe('verifySessionCookie', () => {
|
|
1322
|
+
it('rejects disabled users when revocation checking is enabled', async () => {
|
|
1323
|
+
mockedVerifySessionJWT.mockResolvedValue({
|
|
1324
|
+
data: mockFirebasePayload,
|
|
1325
|
+
error: null
|
|
1326
|
+
});
|
|
1327
|
+
vi.spyOn(auth, 'getUser').mockResolvedValue({
|
|
1328
|
+
data: { ...mockUserRecord, disabled: true },
|
|
1329
|
+
error: null
|
|
1330
|
+
});
|
|
1331
|
+
expect((await auth.verifySessionCookie('cookie', true)).error?.code).toBe(FirebaseAdminAuthErrorInfo.ADMIN_USER_DISABLED.code);
|
|
1332
|
+
});
|
|
1333
|
+
it.each([
|
|
1334
|
+
[
|
|
1335
|
+
'1001',
|
|
1336
|
+
FirebaseAdminAuthErrorInfo.ADMIN_SESSION_COOKIE_REVOKED.code
|
|
1337
|
+
],
|
|
1338
|
+
['1000', undefined],
|
|
1339
|
+
['999', undefined],
|
|
1340
|
+
[undefined, undefined]
|
|
1341
|
+
])('checks auth_time against validSince %s, not issuance time', async (validSince, expectedCode) => {
|
|
1342
|
+
mockedVerifySessionJWT.mockResolvedValue({
|
|
1343
|
+
data: { ...mockFirebasePayload, iat: 1500 },
|
|
1344
|
+
error: null
|
|
1345
|
+
});
|
|
1346
|
+
vi.spyOn(auth, 'getUser').mockResolvedValue({
|
|
1347
|
+
data: { ...mockUserRecord, validSince },
|
|
1348
|
+
error: null
|
|
1349
|
+
});
|
|
1350
|
+
const result = await auth.verifySessionCookie('cookie', true);
|
|
1351
|
+
expect(result.error?.code).toBe(expectedCode);
|
|
1352
|
+
expect(result.data === null).toBe(Boolean(expectedCode));
|
|
1353
|
+
});
|
|
1354
|
+
it('skips the user lookup by default', async () => {
|
|
1355
|
+
mockedVerifySessionJWT.mockResolvedValue({
|
|
1356
|
+
data: mockFirebasePayload,
|
|
1357
|
+
error: null
|
|
1358
|
+
});
|
|
1359
|
+
const lookup = vi.spyOn(auth, 'getUser');
|
|
1360
|
+
expect(await auth.verifySessionCookie('cookie')).toEqual({
|
|
1361
|
+
data: mockFirebasePayload,
|
|
1362
|
+
error: null
|
|
1363
|
+
});
|
|
1364
|
+
expect(lookup).not.toHaveBeenCalled();
|
|
1365
|
+
});
|
|
357
1366
|
it('returns error when verifySessionJWT fails', async () => {
|
|
358
1367
|
mockedVerifySessionJWT.mockResolvedValueOnce({
|
|
359
1368
|
data: null,
|