Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 129 additions & 2 deletions packages/core/src/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,11 @@ describe("createUserWithEmailAndPassword", () => {
const password = "password123";

const credential = EmailAuthProvider.credential(email, password);
vi.mocked(hasBehavior).mockReturnValue(true);
vi.mocked(hasBehavior).mockImplementation((ui, behavior) => {
if (behavior === "autoUpgradeAnonymousCredential") return true;
if (behavior === "requireDisplayName") return false;
return false;
});
const mockBehavior = vi.fn().mockResolvedValue({ providerId: "password" } as UserCredential);
vi.mocked(getBehavior).mockReturnValue(mockBehavior);

Expand All @@ -215,7 +219,11 @@ describe("createUserWithEmailAndPassword", () => {
const password = "password123";

const credential = EmailAuthProvider.credential(email, password);
vi.mocked(hasBehavior).mockReturnValue(true);
vi.mocked(hasBehavior).mockImplementation((ui, behavior) => {
if (behavior === "autoUpgradeAnonymousCredential") return true;
if (behavior === "requireDisplayName") return false;
return false;
});
const mockBehavior = vi.fn().mockResolvedValue(undefined);
vi.mocked(getBehavior).mockReturnValue(mockBehavior);

Expand Down Expand Up @@ -249,6 +257,125 @@ describe("createUserWithEmailAndPassword", () => {
expect(handleFirebaseError).toHaveBeenCalledWith(mockUI, error);
expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["idle"]]);
});

it("should call handleFirebaseError when requireDisplayName behavior is enabled but no displayName provided", async () => {
const mockUI = createMockUI();
const email = "test@example.com";
const password = "password123";

vi.mocked(hasBehavior).mockImplementation((_, behavior) => {
if (behavior === "requireDisplayName") return true;
if (behavior === "autoUpgradeAnonymousCredential") return false;
return false;
});

await createUserWithEmailAndPassword(mockUI, email, password);

expect(hasBehavior).toHaveBeenCalledWith(mockUI, "requireDisplayName");
expect(_createUserWithEmailAndPassword).not.toHaveBeenCalled();
expect(handleFirebaseError).toHaveBeenCalled();
});

it("should call requireDisplayName behavior when enabled and displayName provided", async () => {
const mockUI = createMockUI();
const email = "test@example.com";
const password = "password123";
const displayName = "John Doe";

const mockRequireDisplayNameBehavior = vi.fn().mockResolvedValue(undefined);
const mockResult = { providerId: "password", user: { uid: "user123" } } as UserCredential;

vi.mocked(hasBehavior).mockImplementation((_, behavior) => {
if (behavior === "requireDisplayName") return true;
if (behavior === "autoUpgradeAnonymousCredential") return false;
return false;
});
vi.mocked(getBehavior).mockReturnValue(mockRequireDisplayNameBehavior);
vi.mocked(_createUserWithEmailAndPassword).mockResolvedValue(mockResult);

const result = await createUserWithEmailAndPassword(mockUI, email, password, displayName);

expect(hasBehavior).toHaveBeenCalledWith(mockUI, "requireDisplayName");
expect(getBehavior).toHaveBeenCalledWith(mockUI, "requireDisplayName");
expect(mockRequireDisplayNameBehavior).toHaveBeenCalledWith(mockUI, mockResult.user, displayName);
expect(result).toBe(mockResult);
});

it("should call requireDisplayName behavior after autoUpgradeAnonymousCredential when both enabled", async () => {
const mockUI = createMockUI();
const email = "test@example.com";
const password = "password123";
const displayName = "John Doe";

const mockAutoUpgradeBehavior = vi
.fn()
.mockResolvedValue({ providerId: "upgraded", user: { uid: "upgraded-user" } } as UserCredential);
const mockRequireDisplayNameBehavior = vi.fn().mockResolvedValue(undefined);
const credential = EmailAuthProvider.credential(email, password);

vi.mocked(hasBehavior).mockImplementation((_, behavior) => {
if (behavior === "requireDisplayName") return true;
if (behavior === "autoUpgradeAnonymousCredential") return true;
return false;
});

vi.mocked(getBehavior).mockImplementation((_, behavior) => {
if (behavior === "autoUpgradeAnonymousCredential") return mockAutoUpgradeBehavior;
if (behavior === "requireDisplayName") return mockRequireDisplayNameBehavior;
return vi.fn();
});

vi.mocked(EmailAuthProvider.credential).mockReturnValue(credential);

const result = await createUserWithEmailAndPassword(mockUI, email, password, displayName);

expect(hasBehavior).toHaveBeenCalledWith(mockUI, "requireDisplayName");
expect(hasBehavior).toHaveBeenCalledWith(mockUI, "autoUpgradeAnonymousCredential");
expect(mockAutoUpgradeBehavior).toHaveBeenCalledWith(mockUI, credential);
expect(mockRequireDisplayNameBehavior).toHaveBeenCalledWith(mockUI, { uid: "upgraded-user" }, displayName);
expect(result).toEqual({ providerId: "upgraded", user: { uid: "upgraded-user" } });
});

it("should not call requireDisplayName behavior when not enabled", async () => {
const mockUI = createMockUI();
const email = "test@example.com";
const password = "password123";
const displayName = "John Doe";

const mockResult = { providerId: "password", user: { uid: "user123" } } as UserCredential;

vi.mocked(hasBehavior).mockReturnValue(false);
vi.mocked(_createUserWithEmailAndPassword).mockResolvedValue(mockResult);

const result = await createUserWithEmailAndPassword(mockUI, email, password, displayName);

expect(hasBehavior).toHaveBeenCalledWith(mockUI, "requireDisplayName");
expect(getBehavior).not.toHaveBeenCalledWith(mockUI, "requireDisplayName");
expect(result).toBe(mockResult);
});

it("should handle requireDisplayName behavior errors", async () => {
const mockUI = createMockUI();
const email = "test@example.com";
const password = "password123";
const displayName = "John Doe";

const mockRequireDisplayNameBehavior = vi.fn().mockRejectedValue(new Error("Display name update failed"));
const mockResult = { providerId: "password", user: { uid: "user123" } } as UserCredential;

vi.mocked(hasBehavior).mockImplementation((_, behavior) => {
if (behavior === "requireDisplayName") return true;
if (behavior === "autoUpgradeAnonymousCredential") return false;
return false;
});
vi.mocked(getBehavior).mockReturnValue(mockRequireDisplayNameBehavior);
vi.mocked(_createUserWithEmailAndPassword).mockResolvedValue(mockResult);

await createUserWithEmailAndPassword(mockUI, email, password, displayName);

expect(mockRequireDisplayNameBehavior).toHaveBeenCalledWith(mockUI, mockResult.user, displayName);
expect(handleFirebaseError).toHaveBeenCalled();
});
});

describe("signInWithPhoneNumber", () => {
Expand Down
18 changes: 17 additions & 1 deletion packages/core/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import {
import { FirebaseUIConfiguration } from "./config";
import { handleFirebaseError } from "./errors";
import { hasBehavior, getBehavior } from "./behaviors/index";
import { FirebaseError } from "firebase/app";
import { getTranslation } from "./translations";

async function handlePendingCredential(ui: FirebaseUIConfiguration, user: UserCredential): Promise<UserCredential> {
const pendingCredString = window.sessionStorage.getItem("pendingCred");
Expand Down Expand Up @@ -82,21 +84,35 @@ export async function signInWithEmailAndPassword(
export async function createUserWithEmailAndPassword(
ui: FirebaseUIConfiguration,
email: string,
password: string
password: string,
displayName?: string
): Promise<UserCredential> {
try {
const credential = EmailAuthProvider.credential(email, password);

if (hasBehavior(ui, "requireDisplayName") && !displayName) {
throw new FirebaseError("auth/display-name-required", getTranslation(ui, "errors", "displayNameRequired"));
}

if (hasBehavior(ui, "autoUpgradeAnonymousCredential")) {
const result = await getBehavior(ui, "autoUpgradeAnonymousCredential")(ui, credential);

if (result) {
if (hasBehavior(ui, "requireDisplayName")) {
await getBehavior(ui, "requireDisplayName")(ui, result.user, displayName!);
}

return handlePendingCredential(ui, result);
}
}

ui.setState("pending");
const result = await _createUserWithEmailAndPassword(ui.auth, email, password);

if (hasBehavior(ui, "requireDisplayName")) {
await getBehavior(ui, "requireDisplayName")(ui, result.user, displayName!);
}

return handlePendingCredential(ui, result);
} catch (error) {
handleFirebaseError(ui, error);
Expand Down
35 changes: 34 additions & 1 deletion packages/core/src/behaviors/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,20 @@ import {
getBehavior,
hasBehavior,
recaptchaVerification,
requireDisplayName,
defaultBehaviors,
} from "./index";

// Mock the anonymous-upgrade handlers
vi.mock("./anonymous-upgrade", () => ({
autoUpgradeAnonymousCredentialHandler: vi.fn(),
autoUpgradeAnonymousProviderHandler: vi.fn(),
autoUpgradeAnonymousUserRedirectHandler: vi.fn(),
}));

vi.mock("./require-display-name", () => ({
requireDisplayNameHandler: vi.fn(),
}));

vi.mock("firebase/auth", () => ({
RecaptchaVerifier: vi.fn(),
}));
Expand Down Expand Up @@ -49,13 +53,15 @@ describe("hasBehavior", () => {
autoUpgradeAnonymousCredential: { type: "callable" as const, handler: vi.fn() },
autoUpgradeAnonymousProvider: { type: "callable" as const, handler: vi.fn() },
recaptchaVerification: { type: "callable" as const, handler: vi.fn() },
requireDisplayName: { type: "callable" as const, handler: vi.fn() },
} as any,
});

expect(hasBehavior(mockUI, "autoAnonymousLogin")).toBe(true);
expect(hasBehavior(mockUI, "autoUpgradeAnonymousCredential")).toBe(true);
expect(hasBehavior(mockUI, "autoUpgradeAnonymousProvider")).toBe(true);
expect(hasBehavior(mockUI, "recaptchaVerification")).toBe(true);
expect(hasBehavior(mockUI, "requireDisplayName")).toBe(true);
});
});

Expand Down Expand Up @@ -83,6 +89,7 @@ describe("getBehavior", () => {
autoUpgradeAnonymousCredential: { type: "callable" as const, handler: vi.fn() },
autoUpgradeAnonymousProvider: { type: "callable" as const, handler: vi.fn() },
recaptchaVerification: { type: "callable" as const, handler: vi.fn() },
requireDisplayName: { type: "callable" as const, handler: vi.fn() },
};

const ui = createMockUI({ behaviors: mockBehaviors as any });
Expand All @@ -93,6 +100,7 @@ describe("getBehavior", () => {
);
expect(getBehavior(ui, "autoUpgradeAnonymousProvider")).toBe(mockBehaviors.autoUpgradeAnonymousProvider.handler);
expect(getBehavior(ui, "recaptchaVerification")).toBe(mockBehaviors.recaptchaVerification.handler);
expect(getBehavior(ui, "requireDisplayName")).toBe(mockBehaviors.requireDisplayName.handler);
});
});

Expand Down Expand Up @@ -211,6 +219,30 @@ describe("recaptchaVerification", () => {
});
});

describe("requireDisplayName", () => {
it("should return behavior with correct structure", () => {
const behavior = requireDisplayName();

expect(behavior).toHaveProperty("requireDisplayName");
expect(behavior.requireDisplayName).toHaveProperty("type", "callable");
expect(behavior.requireDisplayName).toHaveProperty("handler");
expect(typeof behavior.requireDisplayName.handler).toBe("function");
});

it("should call the requireDisplayNameHandler when executed", async () => {
const behavior = requireDisplayName();
const mockUI = createMockUI();
const mockUser = { uid: "test-user-123" } as any;
const displayName = "John Doe";

const { requireDisplayNameHandler } = await import("./require-display-name");

await behavior.requireDisplayName.handler(mockUI, mockUser, displayName);

expect(requireDisplayNameHandler).toHaveBeenCalledWith(mockUI, mockUser, displayName);
});
});

describe("defaultBehaviors", () => {
it("should include recaptchaVerification by default", () => {
expect(defaultBehaviors).toHaveProperty("recaptchaVerification");
Expand All @@ -222,5 +254,6 @@ describe("defaultBehaviors", () => {
expect(defaultBehaviors).not.toHaveProperty("autoAnonymousLogin");
expect(defaultBehaviors).not.toHaveProperty("autoUpgradeAnonymousCredential");
expect(defaultBehaviors).not.toHaveProperty("autoUpgradeAnonymousProvider");
expect(defaultBehaviors).not.toHaveProperty("requireDisplayName");
});
});
8 changes: 8 additions & 0 deletions packages/core/src/behaviors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as autoAnonymousLoginHandlers from "./auto-anonymous-login";
import * as recaptchaHandlers from "./recaptcha";
import * as providerStrategyHandlers from "./provider-strategy";
import * as oneTapSignInHandlers from "./one-tap";
import * as requireDisplayNameHandlers from "./require-display-name";
import {
callableBehavior,
initBehavior,
Expand Down Expand Up @@ -33,6 +34,7 @@ type Registry = {
oneTapSignIn: InitBehavior<
(ui: FirebaseUIConfiguration) => ReturnType<typeof oneTapSignInHandlers.oneTapSignInHandler>
>;
requireDisplayName: CallableBehavior<typeof requireDisplayNameHandlers.requireDisplayNameHandler>;
};

export type Behavior<T extends keyof Registry = keyof Registry> = Pick<Registry, T>;
Expand Down Expand Up @@ -98,6 +100,12 @@ export function oneTapSignIn(options: OneTapSignInOptions): Behavior<"oneTapSign
};
}

export function requireDisplayName(): Behavior<"requireDisplayName"> {
return {
requireDisplayName: callableBehavior(requireDisplayNameHandlers.requireDisplayNameHandler),
};
}

export function hasBehavior<T extends keyof Registry>(ui: FirebaseUIConfiguration, key: T): boolean {
return !!ui.behaviors[key];
}
Expand Down
39 changes: 39 additions & 0 deletions packages/core/src/behaviors/require-display-name.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { User } from "firebase/auth";
import { requireDisplayNameHandler } from "./require-display-name";
import { createMockUI } from "~/tests/utils";

vi.mock("firebase/auth", () => ({
updateProfile: vi.fn(),
}));

import { updateProfile } from "firebase/auth";

beforeEach(() => {
vi.clearAllMocks();
});

describe("requireDisplayNameHandler", () => {
it("should update user profile with display name", async () => {
const mockUser = { uid: "test-user-123" } as User;
const mockUI = createMockUI();
const displayName = "John Doe";

vi.mocked(updateProfile).mockResolvedValue();

await requireDisplayNameHandler(mockUI, mockUser, displayName);

expect(updateProfile).toHaveBeenCalledWith(mockUser, { displayName });
});

it("should handle updateProfile errors", async () => {
const mockUser = { uid: "test-user-123" } as User;
const mockUI = createMockUI();
const displayName = "John Doe";
const mockError = new Error("Profile update failed");

vi.mocked(updateProfile).mockRejectedValue(mockError);

await expect(requireDisplayNameHandler(mockUI, mockUser, displayName)).rejects.toThrow("Profile update failed");
});
});
6 changes: 6 additions & 0 deletions packages/core/src/behaviors/require-display-name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { updateProfile, User } from "firebase/auth";
import { FirebaseUIConfiguration } from "~/config";

export const requireDisplayNameHandler = async (_: FirebaseUIConfiguration, user: User, displayName: string) => {
await updateProfile(user, { displayName });
};
Loading