Skip to content

Commit b3ae16c

Browse files
committed
test: Add integration tests for Firebase UI authentication methods
1 parent 86eebbc commit b3ae16c

File tree

6 files changed

+436
-11
lines changed

6 files changed

+436
-11
lines changed

packages/firebaseui-core/package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@
2323
"lint": "tsc --noEmit",
2424
"format": "prettier --write \"src/**/*.ts\"",
2525
"clean": "rimraf dist",
26-
"test": "vitest run",
27-
"test:watch": "vitest"
26+
"test": "vitest run tests/unit",
27+
"test:watch": "vitest tests/unit",
28+
"test:integration": "vitest run tests/integration",
29+
"test:integration:watch": "vitest tests/integration",
30+
"test:all": "vitest run"
2831
},
2932
"keywords": [
3033
"firebase",
@@ -43,6 +46,7 @@
4346
},
4447
"devDependencies": {
4548
"@types/jsdom": "^21.1.7",
49+
"firebase": "^11.0.0",
4650
"jsdom": "^26.0.0",
4751
"prettier": "^3.1.1",
4852
"rimraf": "^6.0.1",
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
2+
import { initializeApp } from 'firebase/app';
3+
import { Auth, connectAuthEmulator, getAuth, signOut, deleteUser } from 'firebase/auth';
4+
import { GoogleAuthProvider } from 'firebase/auth';
5+
import {
6+
fuiSignInWithEmailAndPassword,
7+
fuiCreateUserWithEmailAndPassword,
8+
fuiSignInWithEmailLink,
9+
fuiSendSignInLinkToEmail,
10+
fuiSignInAnonymously,
11+
fuiSendPasswordResetEmail,
12+
fuiSignInWithOAuth,
13+
} from '../../src/auth';
14+
15+
describe('Firebase UI Auth Integration', () => {
16+
let auth: Auth;
17+
const testPassword = 'testPassword123!';
18+
let testCount = 0;
19+
20+
const getUniqueEmail = () => `test${Date.now()}-${testCount++}@example.com`;
21+
22+
beforeAll(() => {
23+
const app = initializeApp({
24+
apiKey: 'fake-api-key',
25+
authDomain: 'fake-auth-domain',
26+
projectId: 'fake-project-id',
27+
});
28+
auth = getAuth(app);
29+
connectAuthEmulator(auth, 'http://127.0.0.1:9099', { disableWarnings: true });
30+
});
31+
32+
beforeEach(async () => {
33+
if (auth.currentUser) {
34+
try {
35+
await deleteUser(auth.currentUser);
36+
} catch {}
37+
await signOut(auth);
38+
}
39+
window.localStorage.clear();
40+
testCount = 0;
41+
});
42+
43+
afterEach(async () => {
44+
if (auth.currentUser) {
45+
try {
46+
await deleteUser(auth.currentUser);
47+
} catch {}
48+
await signOut(auth);
49+
}
50+
window.localStorage.clear();
51+
});
52+
53+
describe('Email/Password Authentication', () => {
54+
it('should create a new user and sign in', async () => {
55+
const email = getUniqueEmail();
56+
57+
const createResult = await fuiCreateUserWithEmailAndPassword(auth, email, testPassword);
58+
expect(createResult.user).toBeDefined();
59+
expect(createResult.user.email).toBe(email);
60+
61+
await signOut(auth);
62+
63+
const signInResult = await fuiSignInWithEmailAndPassword(auth, email, testPassword);
64+
expect(signInResult.user).toBeDefined();
65+
expect(signInResult.user.email).toBe(email);
66+
});
67+
68+
it('should fail with invalid credentials', async () => {
69+
const email = getUniqueEmail();
70+
await expect(fuiSignInWithEmailAndPassword(auth, email, 'wrongpassword')).rejects.toThrow();
71+
});
72+
73+
it('should handle password reset email', async () => {
74+
const email = getUniqueEmail();
75+
await fuiCreateUserWithEmailAndPassword(auth, email, testPassword);
76+
await signOut(auth);
77+
78+
await expect(fuiSendPasswordResetEmail(auth, email)).resolves.not.toThrow();
79+
});
80+
});
81+
82+
describe('Anonymous Authentication', () => {
83+
it('should sign in anonymously', async () => {
84+
const result = await fuiSignInAnonymously(auth);
85+
expect(result.user).toBeDefined();
86+
expect(result.user.isAnonymous).toBe(true);
87+
});
88+
89+
it('should upgrade anonymous user to email/password', async () => {
90+
const email = getUniqueEmail();
91+
92+
await fuiSignInAnonymously(auth);
93+
94+
const result = await fuiCreateUserWithEmailAndPassword(auth, email, testPassword, {
95+
enableAutoUpgradeAnonymous: true,
96+
});
97+
98+
expect(result.user).toBeDefined();
99+
expect(result.user.email).toBe(email);
100+
expect(result.user.isAnonymous).toBe(false);
101+
});
102+
});
103+
104+
describe('Email Link Authentication', () => {
105+
it('should initiate email link sign in', async () => {
106+
const email = getUniqueEmail();
107+
await expect(fuiSendSignInLinkToEmail(auth, email)).resolves.not.toThrow();
108+
109+
expect(window.localStorage.getItem('emailForSignIn')).toBe(email);
110+
});
111+
112+
// Note: Full email link sign-in flow can't be tested in integration tests
113+
// as it requires clicking the email link
114+
});
115+
116+
describe('OAuth Authentication', () => {
117+
it.skip('should initiate OAuth sign in (skipped - requires user interaction)', async () => {
118+
const provider = new GoogleAuthProvider();
119+
await fuiSignInWithOAuth(auth, provider);
120+
});
121+
122+
it.skip('should handle OAuth for anonymous upgrade (skipped - requires user interaction)', async () => {
123+
await fuiSignInAnonymously(auth);
124+
const provider = new GoogleAuthProvider();
125+
await fuiSignInWithOAuth(auth, provider, { enableAutoUpgradeAnonymous: true });
126+
});
127+
});
128+
129+
describe('Error Handling', () => {
130+
it('should handle duplicate email registration', async () => {
131+
const email = getUniqueEmail();
132+
133+
await fuiCreateUserWithEmailAndPassword(auth, email, testPassword);
134+
await signOut(auth);
135+
136+
await expect(fuiCreateUserWithEmailAndPassword(auth, email, testPassword)).rejects.toThrow();
137+
});
138+
139+
it('should handle non-existent user sign in', async () => {
140+
const email = getUniqueEmail();
141+
await expect(fuiSignInWithEmailAndPassword(auth, email, 'password')).rejects.toThrow();
142+
});
143+
144+
// Note: Firebase Auth has lenient email validation.
145+
// We test only definitely invalid email formats here.
146+
it('should handle invalid email formats', async () => {
147+
const invalidEmails = [
148+
'invalid', // No @ symbol
149+
'@', // Just @ symbol
150+
'@domain', // No local part
151+
'user@', // No domain
152+
];
153+
154+
for (const email of invalidEmails) {
155+
await expect(fuiCreateUserWithEmailAndPassword(auth, email, testPassword)).rejects.toThrow();
156+
}
157+
});
158+
159+
it('should handle password requirements', async () => {
160+
const email = getUniqueEmail();
161+
const weakPasswords = ['', '123', 'short'];
162+
163+
for (const password of weakPasswords) {
164+
await expect(fuiCreateUserWithEmailAndPassword(auth, email, password)).rejects.toThrow();
165+
}
166+
});
167+
168+
it('should handle multiple anonymous account upgrades', async () => {
169+
const email = getUniqueEmail();
170+
171+
await fuiSignInAnonymously(auth);
172+
const result1 = await fuiCreateUserWithEmailAndPassword(auth, email, testPassword, {
173+
enableAutoUpgradeAnonymous: true,
174+
});
175+
expect(result1.user.isAnonymous).toBe(false);
176+
177+
await signOut(auth);
178+
await fuiSignInAnonymously(auth);
179+
180+
const email2 = getUniqueEmail();
181+
const result2 = await fuiCreateUserWithEmailAndPassword(auth, email2, testPassword, {
182+
enableAutoUpgradeAnonymous: true,
183+
});
184+
expect(result2.user.isAnonymous).toBe(false);
185+
expect(result2.user.uid).not.toBe(result1.user.uid);
186+
});
187+
188+
it('should handle special characters in email', async () => {
189+
const email = `test.name+${Date.now()}@example.com`;
190+
const result = await fuiCreateUserWithEmailAndPassword(auth, email, testPassword);
191+
expect(result.user.email).toBe(email);
192+
});
193+
194+
it('should handle concurrent sign-in attempts', async () => {
195+
const email = getUniqueEmail();
196+
await fuiCreateUserWithEmailAndPassword(auth, email, testPassword);
197+
await signOut(auth);
198+
199+
const attempts = Array(3)
200+
.fill(null)
201+
.map(() => fuiSignInWithEmailAndPassword(auth, email, testPassword));
202+
203+
const results = await Promise.all(attempts);
204+
expect(results.every((result) => result.user.email === email)).toBe(true);
205+
});
206+
});
207+
});

packages/firebaseui-core/src/auth.test.ts renamed to packages/firebaseui-core/tests/unit/auth.test.ts

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/// <reference lib="dom" />
12
import { describe, it, expect, vi, beforeEach } from 'vitest';
23
import {
34
Auth,
@@ -26,8 +27,8 @@ import {
2627
fuiSignInAnonymously,
2728
fuiSignInWithOAuth,
2829
fuiCompleteEmailLinkSignIn,
29-
} from './auth';
30-
import { FirebaseUIError } from './errors';
30+
} from '../../src/auth';
31+
import { FirebaseUIError } from '../../src/errors';
3132

3233
// Mock all Firebase Auth functions
3334
vi.mock('firebase/auth', async () => {
@@ -66,14 +67,10 @@ describe('Firebase UI Auth', () => {
6667

6768
beforeEach(() => {
6869
vi.clearAllMocks();
69-
// Create a fresh mock auth instance for each test
7070
mockAuth = { currentUser: null } as Auth;
71-
// Reset localStorage
7271
window.localStorage.clear();
73-
// Mock EmailAuthProvider
7472
(EmailAuthProvider.credential as any).mockReturnValue(mockCredential);
7573
(EmailAuthProvider.credentialWithLink as any).mockReturnValue(mockCredential);
76-
// Mock PhoneAuthProvider
7774
(PhoneAuthProvider.credential as any).mockReturnValue(mockCredential);
7875
});
7976

@@ -107,6 +104,34 @@ describe('Firebase UI Auth', () => {
107104
FirebaseUIError
108105
);
109106
});
107+
108+
it('should handle network errors', async () => {
109+
const networkError = { name: 'FirebaseError', code: 'auth/network-request-failed' };
110+
(signInWithCredential as any).mockRejectedValue(networkError);
111+
112+
await expect(fuiSignInWithEmailAndPassword(mockAuth, '[email protected]', 'password')).rejects.toThrow();
113+
});
114+
115+
it('should handle invalid email format', async () => {
116+
const invalidEmailError = { name: 'FirebaseError', code: 'auth/invalid-email' };
117+
(signInWithCredential as any).mockRejectedValue(invalidEmailError);
118+
119+
await expect(fuiSignInWithEmailAndPassword(mockAuth, 'invalid-email', 'password')).rejects.toThrow();
120+
});
121+
122+
it('should handle too many requests error', async () => {
123+
const tooManyRequestsError = { name: 'FirebaseError', code: 'auth/too-many-requests' };
124+
(signInWithCredential as any).mockRejectedValue(tooManyRequestsError);
125+
126+
await expect(fuiSignInWithEmailAndPassword(mockAuth, '[email protected]', 'password')).rejects.toThrow();
127+
});
128+
129+
it('should handle user disabled error', async () => {
130+
const userDisabledError = { name: 'FirebaseError', code: 'auth/user-disabled' };
131+
(signInWithCredential as any).mockRejectedValue(userDisabledError);
132+
133+
await expect(fuiSignInWithEmailAndPassword(mockAuth, '[email protected]', 'password')).rejects.toThrow();
134+
});
110135
});
111136

112137
describe('fuiCreateUserWithEmailAndPassword', () => {
@@ -239,6 +264,56 @@ describe('Firebase UI Auth', () => {
239264
expect(signInAnonymously).toHaveBeenCalledWith(mockAuth);
240265
expect(result).toBe(mockUserCredential);
241266
});
267+
268+
it('should handle operation not allowed error', async () => {
269+
const operationNotAllowedError = { name: 'FirebaseError', code: 'auth/operation-not-allowed' };
270+
(signInAnonymously as any).mockRejectedValue(operationNotAllowedError);
271+
272+
await expect(fuiSignInAnonymously(mockAuth)).rejects.toThrow();
273+
});
274+
275+
it('should handle admin restricted operation error', async () => {
276+
const adminRestrictedError = { name: 'FirebaseError', code: 'auth/admin-restricted-operation' };
277+
(signInAnonymously as any).mockRejectedValue(adminRestrictedError);
278+
279+
await expect(fuiSignInAnonymously(mockAuth)).rejects.toThrow();
280+
});
281+
});
282+
283+
describe('Anonymous User Upgrade', () => {
284+
it('should handle upgrade with existing email', async () => {
285+
mockAuth = { currentUser: { isAnonymous: true } } as Auth;
286+
const emailExistsError = { name: 'FirebaseError', code: 'auth/email-already-in-use' };
287+
(linkWithCredential as any).mockRejectedValue(emailExistsError);
288+
289+
await expect(
290+
fuiCreateUserWithEmailAndPassword(mockAuth, '[email protected]', 'password', {
291+
enableAutoUpgradeAnonymous: true,
292+
})
293+
).rejects.toThrow();
294+
});
295+
296+
it('should handle upgrade of non-anonymous user', async () => {
297+
mockAuth = { currentUser: { isAnonymous: false } } as Auth;
298+
299+
const result = await fuiCreateUserWithEmailAndPassword(mockAuth, '[email protected]', 'password', {
300+
enableAutoUpgradeAnonymous: true,
301+
});
302+
303+
expect(createUserWithEmailAndPassword).toHaveBeenCalled();
304+
expect(linkWithCredential).not.toHaveBeenCalled();
305+
});
306+
307+
it('should handle null user during upgrade', async () => {
308+
mockAuth = { currentUser: null } as Auth;
309+
310+
const result = await fuiCreateUserWithEmailAndPassword(mockAuth, '[email protected]', 'password', {
311+
enableAutoUpgradeAnonymous: true,
312+
});
313+
314+
expect(createUserWithEmailAndPassword).toHaveBeenCalled();
315+
expect(linkWithCredential).not.toHaveBeenCalled();
316+
});
242317
});
243318

244319
describe('fuiSignInWithOAuth', () => {

0 commit comments

Comments
 (0)