Files
zitadel/apps/login/src/lib/server/loginname.test.ts
T
64e14de091 feat(login): translations (#10849)
# Which Problems Are Solved

Replace this example text with a concise list of problems that this PR
solves.
For example:
- password complexity requirements have hardcoded English text
- password, loginname, register and verify components have hardcoded
Engilsh error messages/alerts

# How the Problems Are Solved

Replace this example text with a concise list of changes that this PR
introduces.
For example:
- adds i18n for password complexity requirements
- adds i18n for password, loginname, register and verify components
error messages/alerts

# Additional Changes

- small change in code/styles for icons in PasswordComplexity to make
sure that icons keep size

# Additional Context

N.A

---------

Co-authored-by: Adam Kida <[email protected]>
2025-10-09 11:11:03 +02:00

554 lines
19 KiB
TypeScript

import { describe, expect, test, vi, beforeEach, afterEach } from "vitest";
import { sendLoginname } from "./loginname";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { getIDPByID } from "../zitadel";
// Mock all the dependencies
vi.mock("next/headers", () => ({
headers: vi.fn(),
}));
vi.mock("@zitadel/client", () => ({
create: vi.fn(),
}));
vi.mock("../service-url", () => ({
getServiceUrlFromHeaders: vi.fn(),
}));
vi.mock("../idp", () => ({
idpTypeToIdentityProviderType: vi.fn(),
idpTypeToSlug: vi.fn(),
}));
vi.mock("../zitadel", () => ({
getActiveIdentityProviders: vi.fn(),
getIDPByID: vi.fn(),
getLoginSettings: vi.fn(),
getOrgsByDomain: vi.fn(),
listAuthenticationMethodTypes: vi.fn(),
listIDPLinks: vi.fn(),
searchUsers: vi.fn(),
startIdentityProviderFlow: vi.fn(),
}));
vi.mock("./cookie", () => ({
createSessionAndUpdateCookie: vi.fn(),
}));
vi.mock("./host", () => ({
getOriginalHost: vi.fn(),
}));
// this returns the key itself that can be checked not the translated value
vi.mock("next-intl/server", () => ({
getTranslations: vi.fn(() => (key: string) => key),
}));
describe("sendLoginname", () => {
// Mock modules
let mockHeaders: any;
let mockCreate: any;
let mockGetServiceUrlFromHeaders: any;
let mockGetLoginSettings: any;
let mockSearchUsers: any;
let mockCreateSessionAndUpdateCookie: any;
let mockListAuthenticationMethodTypes: any;
let mockListIDPLinks: any;
let mockGetOriginalHost: any;
let mockStartIdentityProviderFlow: any;
let mockGetActiveIdentityProviders: any;
let mockGetIDPByID: any;
let mockIdpTypeToSlug: any;
beforeEach(async () => {
vi.clearAllMocks();
// Import mocked modules
const { headers } = await import("next/headers");
const { create } = await import("@zitadel/client");
const { getServiceUrlFromHeaders } = await import("../service-url");
const {
getLoginSettings,
searchUsers,
listAuthenticationMethodTypes,
listIDPLinks,
startIdentityProviderFlow,
getActiveIdentityProviders,
} = await import("../zitadel");
const { createSessionAndUpdateCookie } = await import("./cookie");
const { getOriginalHost } = await import("./host");
const { idpTypeToSlug } = await import("../idp");
// Setup mocks
mockHeaders = vi.mocked(headers);
mockCreate = vi.mocked(create);
mockGetServiceUrlFromHeaders = vi.mocked(getServiceUrlFromHeaders);
mockGetLoginSettings = vi.mocked(getLoginSettings);
mockSearchUsers = vi.mocked(searchUsers);
mockCreateSessionAndUpdateCookie = vi.mocked(createSessionAndUpdateCookie);
mockListAuthenticationMethodTypes = vi.mocked(listAuthenticationMethodTypes);
mockListIDPLinks = vi.mocked(listIDPLinks);
mockGetOriginalHost = vi.mocked(getOriginalHost);
mockStartIdentityProviderFlow = vi.mocked(startIdentityProviderFlow);
mockGetActiveIdentityProviders = vi.mocked(getActiveIdentityProviders);
mockGetIDPByID = vi.mocked(getIDPByID);
mockIdpTypeToSlug = vi.mocked(idpTypeToSlug);
// Default mock implementations
mockHeaders.mockResolvedValue({} as any);
mockGetServiceUrlFromHeaders.mockReturnValue({ serviceUrl: "https://api.example.com" });
mockGetOriginalHost.mockResolvedValue("example.com");
mockIdpTypeToSlug.mockReturnValue("google");
mockGetIDPByID.mockResolvedValue({
id: "idp123",
name: "Google",
type: "GOOGLE",
});
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("Error cases", () => {
test("should return error when login settings cannot be retrieved", async () => {
mockGetLoginSettings.mockResolvedValue(null);
const result = await sendLoginname({
loginName: "[email protected]",
});
expect(result).toEqual({ error: "errors.couldNotGetLoginSettings" });
});
test("should return error when user search fails", async () => {
mockGetLoginSettings.mockResolvedValue({ allowUsernamePassword: true });
mockSearchUsers.mockResolvedValue({ error: "Search failed" });
const result = await sendLoginname({
loginName: "[email protected]",
});
expect(result).toEqual({ error: "Search failed" });
});
test("should return error when search result has no result field", async () => {
mockGetLoginSettings.mockResolvedValue({ allowUsernamePassword: true });
mockSearchUsers.mockResolvedValue({});
const result = await sendLoginname({
loginName: "[email protected]",
});
expect(result).toEqual({ error: "errors.couldNotSearchUsers" });
});
test("should return error when more than one user found", async () => {
mockGetLoginSettings.mockResolvedValue({ allowUsernamePassword: true });
mockSearchUsers.mockResolvedValue({
result: [
{ userId: "user1", preferredLoginName: "[email protected]" },
{ userId: "user2", preferredLoginName: "[email protected]" },
],
});
const result = await sendLoginname({
loginName: "[email protected]",
});
expect(result).toEqual({ error: "errors.moreThanOneUserFound" });
});
});
describe("Single user found - authentication method handling", () => {
const mockUser = {
userId: "user123",
preferredLoginName: "[email protected]",
details: { resourceOwner: "org123" },
type: { case: "human", value: { email: { email: "[email protected]" } } },
state: UserState.ACTIVE,
};
const mockSession = {
factors: {
user: {
id: "user123",
loginName: "[email protected]",
organizationId: "org123",
},
},
};
beforeEach(() => {
mockGetLoginSettings.mockResolvedValue({ allowUsernamePassword: true });
mockSearchUsers.mockResolvedValue({ result: [mockUser] });
mockCreate.mockReturnValue({});
mockCreateSessionAndUpdateCookie.mockResolvedValue(mockSession);
});
test("should redirect to verify when user has no authentication methods", async () => {
mockListAuthenticationMethodTypes.mockResolvedValue({ authMethodTypes: [] });
const result = await sendLoginname({
loginName: "[email protected]",
requestId: "req123",
});
expect(result).toHaveProperty("redirect");
expect((result as any).redirect).toMatch(/^\/verify\?/);
expect((result as any).redirect).toContain("loginName=user%40example.com");
expect((result as any).redirect).toContain("send=true");
expect((result as any).redirect).toContain("invite=true");
expect((result as any).redirect).toContain("requestId=req123");
});
describe("Single authentication method", () => {
test("should redirect to password when user has only password method and it's allowed", async () => {
mockListAuthenticationMethodTypes.mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD],
});
const result = await sendLoginname({
loginName: "[email protected]",
requestId: "req123",
});
expect(result).toHaveProperty("redirect");
expect((result as any).redirect).toMatch(/^\/password\?/);
expect((result as any).redirect).toContain("loginName=user%40example.com");
expect((result as any).redirect).toContain("requestId=req123");
});
test("should attempt IDP redirect when password is not allowed but user has IDP links", async () => {
mockGetLoginSettings.mockResolvedValue({ allowUsernamePassword: false });
mockListAuthenticationMethodTypes.mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD],
});
mockListIDPLinks.mockResolvedValue({
result: [{ idpId: "idp123" }],
});
mockStartIdentityProviderFlow.mockResolvedValue("https://idp.example.com/auth");
const result = await sendLoginname({
loginName: "[email protected]",
});
expect(result).toEqual({ redirect: "https://idp.example.com/auth" });
expect(mockListIDPLinks).toHaveBeenCalledWith({
serviceUrl: "https://api.example.com",
userId: "user123",
});
});
test("should return error when password not allowed and no IDP links available", async () => {
mockGetLoginSettings.mockResolvedValue({ allowUsernamePassword: false });
mockListAuthenticationMethodTypes.mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD],
});
mockListIDPLinks.mockResolvedValue({ result: [] });
const result = await sendLoginname({
loginName: "[email protected]",
});
expect(result).toEqual({
error: "errors.usernamePasswordNotAllowed",
});
});
test("should redirect to passkey when user has only passkey method and it's allowed", async () => {
mockGetLoginSettings.mockResolvedValue({ passkeysType: PasskeysType.ALLOWED });
mockListAuthenticationMethodTypes.mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSKEY],
});
const result = await sendLoginname({
loginName: "[email protected]",
requestId: "req123",
});
expect(result).toHaveProperty("redirect");
expect((result as any).redirect).toMatch(/^\/passkey\?/);
expect((result as any).redirect).toContain("loginName=user%40example.com");
expect((result as any).redirect).toContain("requestId=req123");
});
test("should return error when passkeys are not allowed", async () => {
mockGetLoginSettings.mockResolvedValue({ passkeysType: PasskeysType.NOT_ALLOWED });
mockListAuthenticationMethodTypes.mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSKEY],
});
const result = await sendLoginname({
loginName: "[email protected]",
});
expect(result).toEqual({
error: "errors.passkeysNotAllowed",
});
});
test("should redirect to IDP when user has only IDP method", async () => {
mockListAuthenticationMethodTypes.mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.IDP],
});
mockListIDPLinks.mockResolvedValue({
result: [{ idpId: "idp123" }],
});
mockStartIdentityProviderFlow.mockResolvedValue("https://idp.example.com/auth");
const result = await sendLoginname({
loginName: "[email protected]",
});
expect(result).toEqual({ redirect: "https://idp.example.com/auth" });
});
});
describe("Multiple authentication methods", () => {
test("should prefer passkey when multiple methods available", async () => {
mockListAuthenticationMethodTypes.mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD, AuthenticationMethodType.PASSKEY],
});
const result = await sendLoginname({
loginName: "[email protected]",
});
expect(result).toHaveProperty("redirect");
expect((result as any).redirect).toMatch(/^\/passkey\?/);
expect((result as any).redirect).toContain("altPassword=true"); // password is allowed
});
test("should not show password alternative when password is not allowed", async () => {
mockGetLoginSettings.mockResolvedValue({ allowUsernamePassword: false });
mockListAuthenticationMethodTypes.mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD, AuthenticationMethodType.PASSKEY],
});
const result = await sendLoginname({
loginName: "[email protected]",
});
expect(result).toBeDefined();
expect(result?.redirect).toMatch(/^\/passkey\?/);
expect(result?.redirect).toContain("altPassword=false"); // password is not allowed
});
test("should redirect to IDP when no passkey but IDP available", async () => {
mockListAuthenticationMethodTypes.mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD, AuthenticationMethodType.IDP],
});
mockListIDPLinks.mockResolvedValue({
result: [{ idpId: "idp123" }],
});
mockStartIdentityProviderFlow.mockResolvedValue("https://idp.example.com/auth");
const result = await sendLoginname({
loginName: "[email protected]",
});
expect(result).toEqual({ redirect: "https://idp.example.com/auth" });
});
test("should redirect to password when no passkey or IDP, only password available and allowed", async () => {
mockListAuthenticationMethodTypes.mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD],
});
const result = await sendLoginname({
loginName: "[email protected]",
});
expect(result).toBeDefined();
expect(result?.redirect).toMatch(/^\/password\?/);
});
test("should return error when password is only method in multi-method scenario but not allowed", async () => {
mockGetLoginSettings.mockResolvedValue({ allowUsernamePassword: false });
mockListAuthenticationMethodTypes.mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD],
});
mockListIDPLinks.mockResolvedValue({ result: [] });
const result = await sendLoginname({
loginName: "[email protected]",
});
expect(result).toEqual({
error: "errors.usernamePasswordNotAllowed",
});
});
});
});
describe("User not found scenarios", () => {
beforeEach(() => {
mockSearchUsers.mockResolvedValue({ result: [] });
});
test("should redirect to single IDP when register allowed but password not allowed", async () => {
mockGetLoginSettings.mockResolvedValue({
allowRegister: true,
allowUsernamePassword: false,
});
mockGetActiveIdentityProviders.mockResolvedValue({
identityProviders: [{ id: "idp123", type: "OIDC" }],
});
mockStartIdentityProviderFlow.mockResolvedValue("https://idp.example.com/auth");
const result = await sendLoginname({
loginName: "[email protected]",
});
expect(result).toEqual({ redirect: "https://idp.example.com/auth" });
});
test("should redirect to register when both register and password allowed", async () => {
mockGetLoginSettings.mockResolvedValue({
allowRegister: true,
allowUsernamePassword: true,
ignoreUnknownUsernames: false,
});
const result = await sendLoginname({
loginName: "[email protected]",
organization: "org123",
requestId: "req123",
});
expect(result).toBeDefined();
expect(result?.redirect).toMatch(/^\/register\?/);
expect(result?.redirect).toContain("organization=org123");
expect(result?.redirect).toContain("requestId=req123");
expect(result?.redirect).toContain("email=user%40example.com");
});
test("should redirect to password when ignoreUnknownUsernames is true", async () => {
mockGetLoginSettings.mockResolvedValue({
ignoreUnknownUsernames: true,
});
const result = await sendLoginname({
loginName: "[email protected]",
requestId: "req123",
organization: "org123",
});
expect(result).toBeDefined();
expect(result?.redirect).toMatch(/^\/password\?/);
expect(result?.redirect).toContain("loginName=user%40example.com");
expect(result?.redirect).toContain("requestId=req123");
expect(result?.redirect).toContain("organization=org123");
});
test("should return error when user not found and no registration allowed", async () => {
mockGetLoginSettings.mockResolvedValue({
allowRegister: false,
allowUsernamePassword: true,
});
const result = await sendLoginname({
loginName: "[email protected]",
});
expect(result).toEqual({ error: "errors.userNotFound" });
});
});
describe("Edge cases", () => {
test("should handle session creation failure", async () => {
const mockUser = {
userId: "user123",
preferredLoginName: "[email protected]",
details: { resourceOwner: "org123" },
type: { case: "human", value: { email: { email: "[email protected]" } } },
state: UserState.ACTIVE,
};
mockGetLoginSettings.mockResolvedValue({ allowUsernamePassword: true });
mockSearchUsers.mockResolvedValue({ result: [mockUser] });
mockCreate.mockReturnValue({});
mockCreateSessionAndUpdateCookie.mockResolvedValue({ factors: {} }); // No user in session
const result = await sendLoginname({
loginName: "[email protected]",
});
expect(result).toEqual({ error: "errors.couldNotCreateSession" });
});
test("should handle initial user state", async () => {
const mockUser = {
userId: "user123",
preferredLoginName: "[email protected]",
details: { resourceOwner: "org123" },
type: { case: "human", value: { email: { email: "[email protected]" } } },
state: UserState.INITIAL,
};
const mockSession = {
factors: {
user: {
id: "user123",
loginName: "[email protected]",
organizationId: "org123",
},
},
};
mockGetLoginSettings.mockResolvedValue({ allowUsernamePassword: true });
mockSearchUsers.mockResolvedValue({ result: [mockUser] });
mockCreate.mockReturnValue({});
mockCreateSessionAndUpdateCookie.mockResolvedValue(mockSession);
const result = await sendLoginname({
loginName: "[email protected]",
});
expect(result).toEqual({ error: "errors.initialUserNotSupported" });
});
test("should handle organization parameter in all redirects", async () => {
const mockUser = {
userId: "user123",
preferredLoginName: "[email protected]",
details: { resourceOwner: "org123" },
type: { case: "human", value: { email: { email: "[email protected]" } } },
state: UserState.ACTIVE,
};
const mockSession = {
factors: {
user: {
id: "user123",
loginName: "[email protected]",
organizationId: "org123",
},
},
};
mockGetLoginSettings.mockResolvedValue({ allowUsernamePassword: true });
mockSearchUsers.mockResolvedValue({ result: [mockUser] });
mockCreate.mockReturnValue({});
mockCreateSessionAndUpdateCookie.mockResolvedValue(mockSession);
mockListAuthenticationMethodTypes.mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD],
});
const result = await sendLoginname({
loginName: "[email protected]",
organization: "custom-org",
requestId: "req123",
});
expect(result).toBeDefined();
expect(result?.redirect).toContain("organization=custom-org");
expect(result?.redirect).toContain("requestId=req123");
});
});
});