Skip to content

Commit 6fd8ed5

Browse files
committed
test(core): Add error tests
1 parent abb48ce commit 6fd8ed5

File tree

2 files changed

+273
-36
lines changed

2 files changed

+273
-36
lines changed

packages/core/src/errors.test.ts

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest";
2+
import { FirebaseError } from "firebase/app";
3+
import { AuthCredential } from "firebase/auth";
4+
import { FirebaseUIError, handleFirebaseError } from "./errors";
5+
import { createMockUI } from "~/tests/utils";
6+
import { ERROR_CODE_MAP } from "@firebase-ui/translations";
7+
8+
// Mock the translations module
9+
vi.mock("./translations", () => ({
10+
getTranslation: vi.fn(),
11+
}));
12+
13+
import { getTranslation } from "./translations";
14+
15+
let mockSessionStorage: { [key: string]: string };
16+
17+
beforeEach(() => {
18+
vi.clearAllMocks();
19+
20+
// Mock sessionStorage
21+
mockSessionStorage = {};
22+
Object.defineProperty(window, 'sessionStorage', {
23+
value: {
24+
setItem: vi.fn((key: string, value: string) => {
25+
mockSessionStorage[key] = value;
26+
}),
27+
getItem: vi.fn((key: string) => mockSessionStorage[key] || null),
28+
removeItem: vi.fn((key: string) => {
29+
delete mockSessionStorage[key];
30+
}),
31+
clear: vi.fn(() => {
32+
Object.keys(mockSessionStorage).forEach(key => delete mockSessionStorage[key]);
33+
}),
34+
},
35+
writable: true,
36+
});
37+
});
38+
39+
describe("FirebaseUIError", () => {
40+
beforeEach(() => {
41+
vi.clearAllMocks();
42+
});
43+
44+
it("should create a FirebaseUIError with translated message", () => {
45+
const mockUI = createMockUI();
46+
const mockFirebaseError = new FirebaseError("auth/user-not-found", "User not found");
47+
const expectedTranslation = "User not found (translated)";
48+
49+
vi.mocked(getTranslation).mockReturnValue(expectedTranslation);
50+
51+
const error = new FirebaseUIError(mockUI, mockFirebaseError);
52+
53+
expect(error).toBeInstanceOf(FirebaseError);
54+
expect(error.code).toBe("auth/user-not-found");
55+
expect(error.message).toBe(expectedTranslation);
56+
expect(getTranslation).toHaveBeenCalledWith(
57+
mockUI,
58+
"errors",
59+
ERROR_CODE_MAP["auth/user-not-found"]
60+
);
61+
});
62+
63+
it("should handle unknown error codes gracefully", () => {
64+
const mockUI = createMockUI();
65+
const mockFirebaseError = new FirebaseError("auth/unknown-error", "Unknown error");
66+
const expectedTranslation = "Unknown error (translated)";
67+
68+
vi.mocked(getTranslation).mockReturnValue(expectedTranslation);
69+
70+
const error = new FirebaseUIError(mockUI, mockFirebaseError);
71+
72+
expect(error.code).toBe("auth/unknown-error");
73+
expect(error.message).toBe(expectedTranslation);
74+
expect(getTranslation).toHaveBeenCalledWith(
75+
mockUI,
76+
"errors",
77+
ERROR_CODE_MAP["auth/unknown-error" as keyof typeof ERROR_CODE_MAP]
78+
);
79+
});
80+
});
81+
82+
describe("handleFirebaseError", () => {
83+
it("should throw non-Firebase errors as-is", () => {
84+
const mockUI = createMockUI();
85+
const nonFirebaseError = new Error("Regular error");
86+
87+
expect(() => handleFirebaseError(mockUI, nonFirebaseError)).toThrow("Regular error");
88+
});
89+
90+
it("should throw non-Firebase errors with different types", () => {
91+
const mockUI = createMockUI();
92+
const stringError = "String error";
93+
const numberError = 42;
94+
const nullError = null;
95+
const undefinedError = undefined;
96+
97+
expect(() => handleFirebaseError(mockUI, stringError)).toThrow("String error");
98+
expect(() => handleFirebaseError(mockUI, numberError)).toThrow();
99+
expect(() => handleFirebaseError(mockUI, nullError)).toThrow();
100+
expect(() => handleFirebaseError(mockUI, undefinedError)).toThrow();
101+
});
102+
103+
it("should throw FirebaseUIError for Firebase errors", () => {
104+
const mockUI = createMockUI();
105+
const mockFirebaseError = new FirebaseError("auth/user-not-found", "User not found");
106+
const expectedTranslation = "User not found (translated)";
107+
108+
vi.mocked(getTranslation).mockReturnValue(expectedTranslation);
109+
110+
expect(() => handleFirebaseError(mockUI, mockFirebaseError)).toThrow(FirebaseUIError);
111+
112+
try {
113+
handleFirebaseError(mockUI, mockFirebaseError);
114+
} catch (error) {
115+
// Should be an instance of both FirebaseUIError and FirebaseError
116+
expect(error).toBeInstanceOf(FirebaseUIError);
117+
expect(error).toBeInstanceOf(FirebaseError);
118+
expect((error as FirebaseUIError).code).toBe("auth/user-not-found");
119+
expect((error as FirebaseUIError).message).toBe(expectedTranslation);
120+
}
121+
});
122+
123+
it("should store credential in sessionStorage for account-exists-with-different-credential", () => {
124+
const mockUI = createMockUI();
125+
const mockCredential = {
126+
providerId: "google.com",
127+
toJSON: vi.fn().mockReturnValue({ providerId: "google.com", token: "mock-token" })
128+
} as unknown as AuthCredential;
129+
130+
const mockFirebaseError = {
131+
code: "auth/account-exists-with-different-credential",
132+
message: "Account exists with different credential",
133+
credential: mockCredential
134+
} as FirebaseError & { credential: AuthCredential };
135+
136+
const expectedTranslation = "Account exists with different credential (translated)";
137+
vi.mocked(getTranslation).mockReturnValue(expectedTranslation);
138+
139+
expect(() => handleFirebaseError(mockUI, mockFirebaseError)).toThrow(FirebaseUIError);
140+
141+
expect(window.sessionStorage.setItem).toHaveBeenCalledWith(
142+
"pendingCred",
143+
JSON.stringify(mockCredential.toJSON())
144+
);
145+
expect(mockCredential.toJSON).toHaveBeenCalled();
146+
});
147+
148+
it("should not store credential for other error types", () => {
149+
const mockUI = createMockUI();
150+
const mockFirebaseError = new FirebaseError("auth/user-not-found", "User not found");
151+
const expectedTranslation = "User not found (translated)";
152+
153+
vi.mocked(getTranslation).mockReturnValue(expectedTranslation);
154+
155+
expect(() => handleFirebaseError(mockUI, mockFirebaseError)).toThrow(FirebaseUIError);
156+
157+
expect(window.sessionStorage.setItem).not.toHaveBeenCalled();
158+
});
159+
160+
it("should handle account-exists-with-different-credential without credential", () => {
161+
const mockUI = createMockUI();
162+
const mockFirebaseError = {
163+
code: "auth/account-exists-with-different-credential",
164+
message: "Account exists with different credential"
165+
} as FirebaseError;
166+
167+
const expectedTranslation = "Account exists with different credential (translated)";
168+
vi.mocked(getTranslation).mockReturnValue(expectedTranslation);
169+
170+
expect(() => handleFirebaseError(mockUI, mockFirebaseError)).toThrow(FirebaseUIError);
171+
172+
// Should not try to store credential if it doesn't exist
173+
expect(window.sessionStorage.setItem).not.toHaveBeenCalled();
174+
});
175+
});
176+
177+
describe("isFirebaseError utility", () => {
178+
it("should identify FirebaseError objects", () => {
179+
const firebaseError = new FirebaseError("auth/user-not-found", "User not found");
180+
181+
// We can't directly test the private function, but we can test it through handleFirebaseError
182+
const mockUI = createMockUI();
183+
vi.mocked(getTranslation).mockReturnValue("translated message");
184+
185+
expect(() => handleFirebaseError(mockUI, firebaseError)).toThrow(FirebaseUIError);
186+
});
187+
188+
it("should reject non-FirebaseError objects", () => {
189+
const mockUI = createMockUI();
190+
const nonFirebaseError = { code: "test", message: "test" }; // Missing proper structure
191+
192+
expect(() => handleFirebaseError(mockUI, nonFirebaseError)).toThrow();
193+
});
194+
195+
it("should reject objects without code and message", () => {
196+
const mockUI = createMockUI();
197+
const invalidObject = { someProperty: "value" };
198+
199+
expect(() => handleFirebaseError(mockUI, invalidObject)).toThrow();
200+
});
201+
});
202+
203+
describe("errorContainsCredential utility", () => {
204+
it("should identify FirebaseError with credential", () => {
205+
const mockUI = createMockUI();
206+
const mockCredential = {
207+
providerId: "google.com",
208+
toJSON: vi.fn().mockReturnValue({ providerId: "google.com" })
209+
} as unknown as AuthCredential;
210+
211+
const firebaseErrorWithCredential = {
212+
code: "auth/account-exists-with-different-credential",
213+
message: "Account exists with different credential",
214+
credential: mockCredential
215+
} as FirebaseError & { credential: AuthCredential };
216+
217+
vi.mocked(getTranslation).mockReturnValue("translated message");
218+
219+
expect(() => handleFirebaseError(mockUI, firebaseErrorWithCredential)).toThrowError(FirebaseUIError);
220+
221+
// Should have stored the credential
222+
expect(window.sessionStorage.setItem).toHaveBeenCalledWith(
223+
"pendingCred",
224+
JSON.stringify(mockCredential.toJSON())
225+
);
226+
});
227+
228+
it("should handle FirebaseError without credential", () => {
229+
const mockUI = createMockUI();
230+
const firebaseErrorWithoutCredential = {
231+
code: "auth/account-exists-with-different-credential",
232+
message: "Account exists with different credential"
233+
} as FirebaseError;
234+
235+
vi.mocked(getTranslation).mockReturnValue("translated message");
236+
237+
expect(() => handleFirebaseError(mockUI, firebaseErrorWithoutCredential)).toThrowError(FirebaseUIError);
238+
239+
// Should not have stored any credential
240+
expect(window.sessionStorage.setItem).not.toHaveBeenCalled();
241+
});
242+
});
243+

packages/core/src/errors.ts

Lines changed: 30 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -15,51 +15,45 @@
1515
*/
1616

1717
import { ERROR_CODE_MAP, ErrorCode } from "@firebase-ui/translations";
18-
import { getTranslation } from "./translations";
18+
import { FirebaseError } from "firebase/app";
19+
import { AuthCredential } from "firebase/auth";
1920
import { FirebaseUIConfiguration } from "./config";
20-
export class FirebaseUIError extends Error {
21-
code: string;
22-
23-
constructor(error: any, ui: FirebaseUIConfiguration) {
24-
const errorCode: ErrorCode = error?.customData?.message?.match?.(/\(([^)]+)\)/)?.at(1) || error?.code || "unknown";
25-
const translationKey = ERROR_CODE_MAP[errorCode] || "unknownError";
26-
const message = getTranslation(ui, "errors", translationKey);
21+
import { getTranslation } from "./translations";
22+
export class FirebaseUIError extends FirebaseError {
23+
constructor(ui: FirebaseUIConfiguration, error: FirebaseError) {
24+
const message = getTranslation(ui, "errors", ERROR_CODE_MAP[error.code as ErrorCode]);
25+
super(error.code, message);
2726

28-
super(message);
29-
this.name = "FirebaseUIError";
30-
this.code = errorCode;
27+
// Ensures that `instanceof FirebaseUIError` works, alongside `instanceof FirebaseError`
28+
Object.setPrototypeOf(this, FirebaseUIError.prototype);
3129
}
3230
}
3331

3432
export function handleFirebaseError(
3533
ui: FirebaseUIConfiguration,
36-
error: any,
37-
opts?: {
38-
enableHandleExistingCredential?: boolean;
39-
}
34+
error: unknown,
4035
): never {
41-
// TODO(ehesp): Type error as unknown, check instance of FirebaseError
42-
if (error?.code === "auth/account-exists-with-different-credential") {
43-
if (opts?.enableHandleExistingCredential && error.credential) {
44-
window.sessionStorage.setItem("pendingCred", JSON.stringify(error.credential));
45-
} else {
46-
window.sessionStorage.removeItem("pendingCred");
47-
}
48-
49-
throw new FirebaseUIError(
50-
{
51-
code: "auth/account-exists-with-different-credential",
52-
customData: {
53-
email: error.customData?.email,
54-
},
55-
},
56-
ui,
57-
);
36+
// If it's not a Firebase error, then we just throw it and preserve the original error.
37+
if (!isFirebaseError(error)) {
38+
throw error;
5839
}
5940

60-
// TODO: Debug why instanceof FirebaseError is not working
61-
if (error?.name === "FirebaseError") {
62-
throw new FirebaseUIError(error, ui);
41+
// TODO(ehesp): Type error as unknown, check instance of FirebaseError
42+
// TODO(ehesp): Support via behavior
43+
if (error.code === "auth/account-exists-with-different-credential" && errorContainsCredential(error)) {
44+
window.sessionStorage.setItem("pendingCred", JSON.stringify(error.credential.toJSON()));
6345
}
64-
throw new FirebaseUIError({ code: "unknown" }, ui);
46+
47+
throw new FirebaseUIError(ui, error);
48+
}
49+
50+
// Utility to obtain whether something is a FirebaseError
51+
function isFirebaseError(error: unknown): error is FirebaseError {
52+
// Calling instanceof FirebaseError is not working - not sure why yet.
53+
return !!error && typeof error === "object" && "code" in error && "message" in error;
54+
}
55+
56+
// Utility to obtain whether something is a FirebaseError that contains a credential - doesn't seemed to be typed?
57+
function errorContainsCredential(error: FirebaseError): error is FirebaseError & { credential: AuthCredential } {
58+
return 'credential' in error;
6559
}

0 commit comments

Comments
 (0)