mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-24 16:22:16 +00:00
# Which Problems Are Solved This PR deprecates and replaces the use of `allowUsernamePassword` with `allowLocalAuthentication`. It additionally disables the username on organizations that disallow local authentication. In such a case, only IDPs are shown. # How the Problems Are Solved - replace `allowUsernamePassword` by `allowLocalAuthentication` - disable username form if !allowLocalAuthentication - translate property in console UI
1186 lines
42 KiB
TypeScript
1186 lines
42 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", () => ({
|
|
getServiceConfig: 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", () => ({
|
|
getInstanceHost: vi.fn(),
|
|
getPublicHost: 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 mockGetInstanceHost: any;
|
|
let mockGetPublicHost: any;
|
|
let mockStartIdentityProviderFlow: any;
|
|
let mockGetActiveIdentityProviders: any;
|
|
let mockGetIDPByID: any;
|
|
let mockIdpTypeToSlug: any;
|
|
let mockGetOrgsByDomain: any;
|
|
|
|
beforeEach(async () => {
|
|
vi.clearAllMocks();
|
|
|
|
// Import mocked modules
|
|
const { headers } = await import("next/headers");
|
|
const { create } = await import("@zitadel/client");
|
|
const { getServiceConfig } = await import("../service-url");
|
|
const {
|
|
getLoginSettings,
|
|
searchUsers,
|
|
listAuthenticationMethodTypes,
|
|
listIDPLinks,
|
|
startIdentityProviderFlow,
|
|
getActiveIdentityProviders,
|
|
getOrgsByDomain,
|
|
} = await import("../zitadel");
|
|
const { createSessionAndUpdateCookie } = await import("./cookie");
|
|
const { getInstanceHost, getPublicHost } = await import("./host");
|
|
const { idpTypeToSlug } = await import("../idp");
|
|
|
|
// Setup mocks
|
|
mockHeaders = vi.mocked(headers);
|
|
mockCreate = vi.mocked(create);
|
|
mockGetServiceUrlFromHeaders = vi.mocked(getServiceConfig);
|
|
mockGetLoginSettings = vi.mocked(getLoginSettings);
|
|
mockSearchUsers = vi.mocked(searchUsers);
|
|
mockCreateSessionAndUpdateCookie = vi.mocked(createSessionAndUpdateCookie);
|
|
mockListAuthenticationMethodTypes = vi.mocked(listAuthenticationMethodTypes);
|
|
mockListIDPLinks = vi.mocked(listIDPLinks);
|
|
mockGetInstanceHost = vi.mocked(getInstanceHost);
|
|
mockGetPublicHost = vi.mocked(getPublicHost);
|
|
mockStartIdentityProviderFlow = vi.mocked(startIdentityProviderFlow);
|
|
mockGetActiveIdentityProviders = vi.mocked(getActiveIdentityProviders);
|
|
mockGetIDPByID = vi.mocked(getIDPByID);
|
|
mockIdpTypeToSlug = vi.mocked(idpTypeToSlug);
|
|
mockGetOrgsByDomain = vi.mocked(getOrgsByDomain);
|
|
|
|
// Default mock implementations
|
|
mockHeaders.mockResolvedValue({} as any);
|
|
mockGetServiceUrlFromHeaders.mockReturnValue({ serviceConfig: { baseUrl: "https://api.example.com" } });
|
|
mockGetInstanceHost.mockReturnValue("example.com");
|
|
mockGetPublicHost.mockReturnValue("example.com");
|
|
mockIdpTypeToSlug.mockReturnValue("google");
|
|
mockGetIDPByID.mockResolvedValue({
|
|
id: "idp123",
|
|
name: "Google",
|
|
type: "GOOGLE",
|
|
});
|
|
// Default: org discovery returns empty result
|
|
mockGetOrgsByDomain.mockResolvedValue({ result: [] });
|
|
});
|
|
|
|
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({ allowLocalAuthentication: 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({ allowLocalAuthentication: 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({ allowLocalAuthentication: 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({ allowLocalAuthentication: true });
|
|
mockSearchUsers.mockResolvedValue({ result: [mockUser] });
|
|
mockCreate.mockReturnValue({});
|
|
mockCreateSessionAndUpdateCookie.mockResolvedValue({ session: mockSession, sessionCookie: {} });
|
|
});
|
|
|
|
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({ allowLocalAuthentication: 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({
|
|
serviceConfig: { baseUrl: "https://api.example.com" },
|
|
userId: "user123",
|
|
});
|
|
});
|
|
|
|
test("should return error when password not allowed and no IDP links available", async () => {
|
|
mockGetLoginSettings.mockResolvedValue({ allowLocalAuthentication: false });
|
|
mockListAuthenticationMethodTypes.mockResolvedValue({
|
|
authMethodTypes: [AuthenticationMethodType.PASSWORD],
|
|
});
|
|
mockListIDPLinks.mockResolvedValue({ result: [] });
|
|
mockGetActiveIdentityProviders.mockResolvedValue({ identityProviders: [] });
|
|
|
|
const result = await sendLoginname({
|
|
loginName: "[email protected]",
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
error: "errors.localAuthenticationNotAllowed",
|
|
});
|
|
});
|
|
|
|
test("should redirect to organization IDP when password not allowed, no user IDP links, but organization has active IDP", async () => {
|
|
mockGetLoginSettings.mockResolvedValue({ allowLocalAuthentication: false });
|
|
mockListAuthenticationMethodTypes.mockResolvedValue({
|
|
authMethodTypes: [AuthenticationMethodType.PASSWORD],
|
|
});
|
|
mockListIDPLinks.mockResolvedValue({ result: [] });
|
|
mockGetActiveIdentityProviders.mockResolvedValue({
|
|
identityProviders: [{ id: "org-idp-123", type: 0 }],
|
|
});
|
|
mockIdpTypeToSlug.mockReturnValue("google");
|
|
mockStartIdentityProviderFlow.mockResolvedValue("https://org-idp.example.com/auth");
|
|
|
|
const result = await sendLoginname({
|
|
loginName: "[email protected]",
|
|
});
|
|
|
|
expect(result).toEqual({ redirect: "https://org-idp.example.com/auth" });
|
|
expect(mockGetActiveIdentityProviders).toHaveBeenCalledWith({
|
|
serviceConfig: { baseUrl: "https://api.example.com" },
|
|
orgId: "org123", // User's organization from resourceOwner
|
|
});
|
|
});
|
|
|
|
test("should redirect to passkey when user has only passkey method and it's allowed", async () => {
|
|
mockGetLoginSettings.mockResolvedValue({ passkeysType: PasskeysType.ALLOWED, allowLocalAuthentication: true });
|
|
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, allowLocalAuthentication: true });
|
|
mockListAuthenticationMethodTypes.mockResolvedValue({
|
|
authMethodTypes: [AuthenticationMethodType.PASSKEY],
|
|
});
|
|
|
|
const result = await sendLoginname({
|
|
loginName: "[email protected]",
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
error: "errors.passkeysNotAllowed",
|
|
});
|
|
});
|
|
|
|
test("should return error when passkeys are allowed but allowLocalAuthentication is false", async () => {
|
|
mockGetLoginSettings.mockResolvedValue({
|
|
passkeysType: PasskeysType.ALLOWED,
|
|
allowLocalAuthentication: false,
|
|
});
|
|
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" });
|
|
});
|
|
|
|
test("should NOT create session when ignoreUnknownUsernames is true", async () => {
|
|
mockGetLoginSettings.mockResolvedValue({
|
|
allowLocalAuthentication: true,
|
|
ignoreUnknownUsernames: true,
|
|
});
|
|
mockListAuthenticationMethodTypes.mockResolvedValue({
|
|
authMethodTypes: [AuthenticationMethodType.PASSWORD],
|
|
});
|
|
|
|
const result = await sendLoginname({
|
|
loginName: "[email protected]",
|
|
requestId: "req123",
|
|
});
|
|
|
|
expect(mockCreateSessionAndUpdateCookie).not.toHaveBeenCalled();
|
|
|
|
expect(result).toHaveProperty("redirect");
|
|
expect((result as any).redirect).toMatch(/^\/password\?/);
|
|
expect((result as any).redirect).toContain("loginName=user%40example.com");
|
|
});
|
|
});
|
|
|
|
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 return error when allowLocalAuthentication is false (disabling both password and passkey)", async () => {
|
|
mockGetLoginSettings.mockResolvedValue({ allowLocalAuthentication: false });
|
|
mockListAuthenticationMethodTypes.mockResolvedValue({
|
|
authMethodTypes: [AuthenticationMethodType.PASSWORD, AuthenticationMethodType.PASSKEY],
|
|
});
|
|
|
|
const result = await sendLoginname({
|
|
loginName: "[email protected]",
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
error: "errors.localAuthenticationNotAllowed",
|
|
});
|
|
});
|
|
|
|
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({ allowLocalAuthentication: false });
|
|
mockListAuthenticationMethodTypes.mockResolvedValue({
|
|
authMethodTypes: [AuthenticationMethodType.PASSWORD],
|
|
});
|
|
mockListIDPLinks.mockResolvedValue({ result: [] });
|
|
mockGetActiveIdentityProviders.mockResolvedValue({ identityProviders: [] });
|
|
|
|
const result = await sendLoginname({
|
|
loginName: "[email protected]",
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
error: "errors.localAuthenticationNotAllowed",
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
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,
|
|
allowLocalAuthentication: 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,
|
|
allowLocalAuthentication: 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",
|
|
ignoreUnknownUsernames: true,
|
|
});
|
|
|
|
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,
|
|
allowLocalAuthentication: true,
|
|
});
|
|
|
|
const result = await sendLoginname({
|
|
loginName: "[email protected]",
|
|
});
|
|
|
|
expect(result).toEqual({ error: "errors.userNotFound" });
|
|
});
|
|
|
|
test("should discover organization from domain suffix when user not found without org context", async () => {
|
|
// Mock login settings for instance level (no org context)
|
|
mockGetLoginSettings
|
|
.mockResolvedValueOnce({
|
|
allowRegister: true,
|
|
allowLocalAuthentication: true,
|
|
ignoreUnknownUsernames: false,
|
|
})
|
|
// Mock login settings for discovered org - must include all necessary flags
|
|
.mockResolvedValueOnce({
|
|
allowDomainDiscovery: true,
|
|
allowRegister: true,
|
|
allowLocalAuthentication: true,
|
|
ignoreUnknownUsernames: false,
|
|
});
|
|
|
|
// Mock org discovery to return one org with matching domain
|
|
mockGetOrgsByDomain.mockResolvedValue({
|
|
result: [{ id: "discovered-org-123", name: "Example Org" }],
|
|
});
|
|
|
|
const result = await sendLoginname({
|
|
loginName: "[email protected]",
|
|
requestId: "req123",
|
|
// No organization parameter - this is the key test scenario
|
|
});
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result?.redirect).toMatch(/^\/register\?/);
|
|
expect(result?.redirect).toContain("organization=discovered-org-123");
|
|
expect(result?.redirect).toContain("requestId=req123");
|
|
expect(result?.redirect).toContain("email=user%40example.com");
|
|
|
|
// Verify org discovery was called with correct domain
|
|
expect(mockGetOrgsByDomain).toHaveBeenCalledWith({
|
|
serviceConfig: { baseUrl: "https://api.example.com" },
|
|
domain: "example.com",
|
|
});
|
|
});
|
|
|
|
test("should redirect to IDP with discovered org when user not found and only IDP allowed", async () => {
|
|
// Mock login settings for instance level (no org context)
|
|
mockGetLoginSettings
|
|
.mockResolvedValueOnce({
|
|
allowRegister: true,
|
|
allowLocalAuthentication: false,
|
|
})
|
|
// Mock login settings for discovered org - must include all necessary flags
|
|
.mockResolvedValueOnce({
|
|
allowDomainDiscovery: true,
|
|
allowRegister: true,
|
|
allowLocalAuthentication: false,
|
|
});
|
|
|
|
// Mock org discovery to return one org with matching domain
|
|
mockGetOrgsByDomain.mockResolvedValue({
|
|
result: [{ id: "discovered-org-456", name: "Example Org" }],
|
|
});
|
|
|
|
mockGetActiveIdentityProviders.mockResolvedValue({
|
|
identityProviders: [{ id: "idp123", type: "OIDC" }],
|
|
});
|
|
mockStartIdentityProviderFlow.mockResolvedValue("https://idp.example.com/auth?org=discovered-org-456");
|
|
|
|
const result = await sendLoginname({
|
|
loginName: "[email protected]",
|
|
requestId: "req123",
|
|
// No organization parameter
|
|
});
|
|
|
|
expect(result).toEqual({ redirect: "https://idp.example.com/auth?org=discovered-org-456" });
|
|
|
|
// Verify org discovery was called
|
|
expect(mockGetOrgsByDomain).toHaveBeenCalledWith({
|
|
serviceConfig: { baseUrl: "https://api.example.com" },
|
|
domain: "company.com",
|
|
});
|
|
|
|
// Verify IDP redirect was called with discovered org
|
|
expect(mockGetActiveIdentityProviders).toHaveBeenCalledWith({
|
|
serviceConfig: { baseUrl: "https://api.example.com" },
|
|
orgId: "discovered-org-456",
|
|
});
|
|
});
|
|
|
|
test("should not discover org if domain discovery is disabled", async () => {
|
|
mockGetLoginSettings
|
|
.mockResolvedValueOnce({
|
|
allowRegister: true,
|
|
allowLocalAuthentication: true,
|
|
ignoreUnknownUsernames: false,
|
|
})
|
|
// Mock login settings for org with domain discovery disabled
|
|
.mockResolvedValueOnce({
|
|
allowDomainDiscovery: false,
|
|
});
|
|
|
|
mockGetOrgsByDomain.mockResolvedValue({
|
|
result: [{ id: "10987654321", name: "Example Org" }],
|
|
});
|
|
|
|
const result = await sendLoginname({
|
|
loginName: "[email protected]",
|
|
// No organization parameter
|
|
});
|
|
|
|
// Should return error since discovery is disabled and no org context
|
|
expect(result).toEqual({ error: "errors.userNotFound" });
|
|
});
|
|
|
|
test("should not discover org if multiple orgs match the domain", async () => {
|
|
mockGetLoginSettings.mockResolvedValue({
|
|
allowRegister: true,
|
|
allowLocalAuthentication: true,
|
|
ignoreUnknownUsernames: false,
|
|
});
|
|
|
|
// Mock org discovery to return multiple orgs
|
|
mockGetOrgsByDomain.mockResolvedValue({
|
|
result: [
|
|
{ id: "12345678910", name: "Example Org 1" },
|
|
{ id: "10987654321", name: "Example Org 2" },
|
|
],
|
|
});
|
|
|
|
const result = await sendLoginname({
|
|
loginName: "[email protected]",
|
|
// No organization parameter
|
|
});
|
|
|
|
// Should return error since multiple orgs match
|
|
expect(result).toEqual({ error: "errors.userNotFound" });
|
|
});
|
|
|
|
test("should use provided organization instead of discovering when org context exists", async () => {
|
|
mockGetLoginSettings.mockResolvedValue({
|
|
allowRegister: true,
|
|
allowLocalAuthentication: true,
|
|
ignoreUnknownUsernames: false,
|
|
});
|
|
|
|
const result = await sendLoginname({
|
|
loginName: "[email protected]",
|
|
organization: "123456",
|
|
requestId: "req123",
|
|
});
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result?.redirect).toMatch(/^\/register\?/);
|
|
expect(result?.redirect).toContain("organization=123456");
|
|
|
|
// Verify org discovery was NOT called since org was provided
|
|
expect(mockGetOrgsByDomain).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("should redirect to password when ignoreUnknownUsernames is true, allowRegister is true, but allowLocalAuthentication is false (User not found)", async () => {
|
|
mockSearchUsers.mockResolvedValue({ result: [] });
|
|
mockGetLoginSettings.mockResolvedValue({
|
|
allowRegister: true,
|
|
allowLocalAuthentication: false,
|
|
ignoreUnknownUsernames: true,
|
|
});
|
|
mockGetActiveIdentityProviders.mockResolvedValue({ identityProviders: [] });
|
|
|
|
const result = await sendLoginname({
|
|
loginName: "[email protected]",
|
|
ignoreUnknownUsernames: true,
|
|
});
|
|
|
|
expect(result).not.toEqual({ error: "errors.userNotFound" });
|
|
expect(result).toHaveProperty("redirect");
|
|
expect((result as any).redirect).toMatch(/^\/password\?/);
|
|
});
|
|
|
|
test("should redirect to password when ignoreUnknownUsernames is true, user found but rejected due to disableLoginWithEmail", async () => {
|
|
const mockUser = {
|
|
userId: "user123",
|
|
preferredLoginName: "user",
|
|
details: { resourceOwner: "org123" },
|
|
type: { case: "human", value: { email: { email: "[email protected]" }, phone: { phone: "123456" } } },
|
|
state: UserState.ACTIVE,
|
|
};
|
|
|
|
mockSearchUsers.mockResolvedValue({ result: [mockUser] });
|
|
mockGetLoginSettings.mockResolvedValue({
|
|
disableLoginWithEmail: true,
|
|
ignoreUnknownUsernames: true,
|
|
});
|
|
|
|
const result = await sendLoginname({
|
|
loginName: "[email protected]",
|
|
ignoreUnknownUsernames: true,
|
|
});
|
|
|
|
expect(result).not.toEqual({ error: "errors.userNotFound" });
|
|
expect(result).toHaveProperty("redirect");
|
|
expect((result as any).redirect).toMatch(/^\/password\?/);
|
|
});
|
|
|
|
test("should redirect to password when ignoreUnknownUsernames is true and more than one user found", async () => {
|
|
mockGetLoginSettings.mockResolvedValue({
|
|
ignoreUnknownUsernames: true,
|
|
});
|
|
mockSearchUsers.mockResolvedValue({
|
|
result: [
|
|
{ userId: "user1", preferredLoginName: "[email protected]" },
|
|
{ userId: "user2", preferredLoginName: "[email protected]" },
|
|
],
|
|
});
|
|
|
|
const result = await sendLoginname({
|
|
loginName: "[email protected]",
|
|
ignoreUnknownUsernames: true,
|
|
});
|
|
|
|
expect(result).not.toEqual({ error: "errors.moreThanOneUserFound" });
|
|
expect(result).toHaveProperty("redirect");
|
|
expect((result as any).redirect).toMatch(/^\/password\?/);
|
|
});
|
|
|
|
test("should return generic error when user not active and ignoreUnknownUsernames is true", async () => {
|
|
mockSearchUsers.mockResolvedValue({
|
|
result: [
|
|
{
|
|
userId: "user1",
|
|
state: UserState.ACTIVE,
|
|
preferredLoginName: "user1",
|
|
type: { case: "human", value: { email: { isVerified: true } } },
|
|
},
|
|
],
|
|
});
|
|
mockGetLoginSettings.mockResolvedValue({
|
|
ignoreUnknownUsernames: true,
|
|
allowLocalAuthentication: true,
|
|
});
|
|
mockListAuthenticationMethodTypes.mockResolvedValue({
|
|
authMethodTypes: [AuthenticationMethodType.PASSWORD],
|
|
});
|
|
// Mock createSessionAndUpdateCookie to fail with user not active error
|
|
mockCreateSessionAndUpdateCookie.mockRejectedValue({
|
|
rawMessage: "Errors.User.NotActive (SESSION-Gj4ko)",
|
|
});
|
|
|
|
const result = await sendLoginname({ loginName: "user1", ignoreUnknownUsernames: true });
|
|
|
|
expect(result).toEqual({ redirect: "/password?loginName=user1" });
|
|
// With ignoreUnknownUsernames: true, we skip session creation, so this mock is NOT called
|
|
expect(mockCreateSessionAndUpdateCookie).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("should NOT create session and return redirect when ignoreUnknownUsernames is true and user is valid", async () => {
|
|
mockSearchUsers.mockResolvedValue({
|
|
result: [
|
|
{
|
|
userId: "user1",
|
|
state: UserState.ACTIVE,
|
|
preferredLoginName: "user1",
|
|
type: { case: "human", value: { email: { isVerified: true } } },
|
|
},
|
|
],
|
|
});
|
|
mockGetLoginSettings.mockResolvedValue({
|
|
ignoreUnknownUsernames: true,
|
|
allowLocalAuthentication: true,
|
|
});
|
|
mockListAuthenticationMethodTypes.mockResolvedValue({
|
|
authMethodTypes: [AuthenticationMethodType.PASSWORD],
|
|
});
|
|
|
|
const result = await sendLoginname({ loginName: "user1", ignoreUnknownUsernames: true });
|
|
|
|
expect(result).toEqual({ redirect: "/password?loginName=user1" });
|
|
expect(mockCreateSessionAndUpdateCookie).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("should redirect to password when ignoreUnknownUsernames is true and password not allowed", 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({
|
|
allowLocalAuthentication: false,
|
|
ignoreUnknownUsernames: true,
|
|
});
|
|
mockSearchUsers.mockResolvedValue({ result: [mockUser] });
|
|
mockCreate.mockReturnValue({});
|
|
mockCreateSessionAndUpdateCookie.mockResolvedValue({ session: mockSession, sessionCookie: {} });
|
|
mockListAuthenticationMethodTypes.mockResolvedValue({
|
|
authMethodTypes: [AuthenticationMethodType.PASSWORD],
|
|
});
|
|
mockListIDPLinks.mockResolvedValue({ result: [] });
|
|
mockGetActiveIdentityProviders.mockResolvedValue({ identityProviders: [] });
|
|
|
|
const result = await sendLoginname({
|
|
loginName: "[email protected]",
|
|
ignoreUnknownUsernames: true,
|
|
});
|
|
|
|
expect(result).not.toEqual({ error: "errors.localAuthenticationNotAllowed" });
|
|
expect(result).toHaveProperty("redirect");
|
|
expect((result as any).redirect).toMatch(/^\/password\?/);
|
|
});
|
|
|
|
test("should redirect to password when ignoreUnknownUsernames is true and passkeys not allowed", 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({
|
|
passkeysType: PasskeysType.NOT_ALLOWED,
|
|
ignoreUnknownUsernames: true,
|
|
});
|
|
mockSearchUsers.mockResolvedValue({ result: [mockUser] });
|
|
mockCreate.mockReturnValue({});
|
|
mockCreateSessionAndUpdateCookie.mockResolvedValue({ session: mockSession, sessionCookie: {} });
|
|
mockListAuthenticationMethodTypes.mockResolvedValue({
|
|
authMethodTypes: [AuthenticationMethodType.PASSKEY],
|
|
});
|
|
|
|
const result = await sendLoginname({
|
|
loginName: "[email protected]",
|
|
ignoreUnknownUsernames: true,
|
|
});
|
|
|
|
expect(result).not.toEqual({ error: "errors.passkeysNotAllowed" });
|
|
expect(result).toHaveProperty("redirect");
|
|
expect((result as any).redirect).toMatch(/^\/password\?/);
|
|
});
|
|
});
|
|
|
|
describe("Edge cases", () => {
|
|
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({ allowLocalAuthentication: true });
|
|
mockSearchUsers.mockResolvedValue({ result: [mockUser] });
|
|
mockCreate.mockReturnValue({});
|
|
mockCreateSessionAndUpdateCookie.mockResolvedValue({ session: mockSession, sessionCookie: {} });
|
|
|
|
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({ allowLocalAuthentication: true });
|
|
mockSearchUsers.mockResolvedValue({ result: [mockUser] });
|
|
mockCreate.mockReturnValue({});
|
|
mockCreateSessionAndUpdateCookie.mockResolvedValue({ session: mockSession, sessionCookie: {} });
|
|
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");
|
|
});
|
|
|
|
test("should redirect to password with INPUT loginName when ignoreUnknownUsernames is true, even if user preferredLoginName is different", 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({
|
|
allowLocalAuthentication: true,
|
|
ignoreUnknownUsernames: true,
|
|
});
|
|
// Mock search result returns a user with resolved/different loginName
|
|
mockSearchUsers.mockResolvedValue({ result: [mockUser] });
|
|
mockCreate.mockReturnValue({});
|
|
mockCreateSessionAndUpdateCookie.mockResolvedValue({ session: mockSession, sessionCookie: {} });
|
|
mockListAuthenticationMethodTypes.mockResolvedValue({
|
|
authMethodTypes: [AuthenticationMethodType.PASSWORD],
|
|
});
|
|
|
|
// INPUT login name is just "user"
|
|
const result = await sendLoginname({
|
|
loginName: "user",
|
|
ignoreUnknownUsernames: true,
|
|
});
|
|
|
|
expect(result).toHaveProperty("redirect");
|
|
// Expect redirect to contain input "user" not resolved "[email protected]"
|
|
expect((result as any).redirect).toContain("loginName=user");
|
|
expect((result as any).redirect).not.toContain("loginName=user%40example.com");
|
|
});
|
|
|
|
test("should redirect to passkey with INPUT loginName when ignoreUnknownUsernames is true", 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({
|
|
passkeysType: PasskeysType.ALLOWED,
|
|
allowLocalAuthentication: true,
|
|
ignoreUnknownUsernames: true,
|
|
});
|
|
mockSearchUsers.mockResolvedValue({ result: [mockUser] });
|
|
mockCreate.mockReturnValue({});
|
|
mockCreateSessionAndUpdateCookie.mockResolvedValue({ session: mockSession, sessionCookie: {} });
|
|
mockListAuthenticationMethodTypes.mockResolvedValue({
|
|
authMethodTypes: [AuthenticationMethodType.PASSKEY],
|
|
});
|
|
|
|
const result = await sendLoginname({
|
|
loginName: "user",
|
|
ignoreUnknownUsernames: true,
|
|
});
|
|
|
|
expect(result).toHaveProperty("redirect");
|
|
expect((result as any).redirect).toMatch(/^\/passkey\?/);
|
|
expect((result as any).redirect).toContain("loginName=user");
|
|
expect((result as any).redirect).not.toContain("loginName=user%40example.com");
|
|
});
|
|
|
|
test("should redirect to passkey with INPUT loginName when ignoreUnknownUsernames is true (multi-method)", 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({
|
|
passkeysType: PasskeysType.ALLOWED,
|
|
allowLocalAuthentication: true,
|
|
ignoreUnknownUsernames: true,
|
|
});
|
|
mockSearchUsers.mockResolvedValue({ result: [mockUser] });
|
|
mockCreate.mockReturnValue({});
|
|
mockCreateSessionAndUpdateCookie.mockResolvedValue({ session: mockSession, sessionCookie: {} });
|
|
mockListAuthenticationMethodTypes.mockResolvedValue({
|
|
authMethodTypes: [AuthenticationMethodType.PASSKEY, AuthenticationMethodType.PASSWORD],
|
|
});
|
|
|
|
const result = await sendLoginname({
|
|
loginName: "user",
|
|
ignoreUnknownUsernames: true,
|
|
});
|
|
|
|
expect(result).toHaveProperty("redirect");
|
|
expect((result as any).redirect).toMatch(/^\/passkey\?/);
|
|
expect((result as any).redirect).toContain("loginName=user");
|
|
expect((result as any).redirect).not.toContain("loginName=user%40example.com");
|
|
});
|
|
test("should use CONTEXT settings to HIDE username even if USER settings would show it", async () => {
|
|
const mockUser = {
|
|
userId: "user123",
|
|
preferredLoginName: "[email protected]",
|
|
details: { resourceOwner: "user-org" },
|
|
type: { case: "human", value: { email: { email: "[email protected]" } } },
|
|
state: UserState.ACTIVE,
|
|
};
|
|
|
|
const mockSession = {
|
|
factors: {
|
|
user: {
|
|
id: "user123",
|
|
loginName: "[email protected]",
|
|
organizationId: "user-org",
|
|
},
|
|
},
|
|
};
|
|
|
|
// Mock implementation to return different settings based on organization
|
|
mockGetLoginSettings.mockImplementation(async (args: any) => {
|
|
if (args.organization === "context-org") {
|
|
return { allowLocalAuthentication: true, ignoreUnknownUsernames: true };
|
|
}
|
|
if (args.organization === "user-org") {
|
|
return { allowLocalAuthentication: true, ignoreUnknownUsernames: false };
|
|
}
|
|
return {};
|
|
});
|
|
|
|
mockSearchUsers.mockResolvedValue({ result: [mockUser] });
|
|
mockCreate.mockReturnValue({});
|
|
mockCreateSessionAndUpdateCookie.mockResolvedValue({ session: mockSession, sessionCookie: {} });
|
|
mockListAuthenticationMethodTypes.mockResolvedValue({
|
|
authMethodTypes: [AuthenticationMethodType.PASSWORD],
|
|
});
|
|
|
|
const result = await sendLoginname({
|
|
loginName: "input-name",
|
|
organization: "context-org", // Context has ignore=true
|
|
ignoreUnknownUsernames: true,
|
|
});
|
|
|
|
expect(result).toHaveProperty("redirect");
|
|
// Should result in input-name because context says HIDE
|
|
expect((result as any).redirect).toContain("loginName=input-name");
|
|
expect((result as any).redirect).not.toContain("loginName=user%40example.com");
|
|
});
|
|
|
|
test("should use CONTEXT settings to SHOW username even if USER settings would hide it", async () => {
|
|
const mockUser = {
|
|
userId: "user123",
|
|
preferredLoginName: "[email protected]",
|
|
details: { resourceOwner: "user-org" },
|
|
type: { case: "human", value: { email: { email: "[email protected]" } } },
|
|
state: UserState.ACTIVE,
|
|
};
|
|
|
|
const mockSession = {
|
|
factors: {
|
|
user: {
|
|
id: "user123",
|
|
loginName: "[email protected]",
|
|
organizationId: "user-org",
|
|
},
|
|
},
|
|
};
|
|
|
|
mockGetLoginSettings.mockImplementation(async (args: any) => {
|
|
if (args.organization === "context-org") {
|
|
return { allowLocalAuthentication: true, ignoreUnknownUsernames: false };
|
|
}
|
|
if (args.organization === "user-org") {
|
|
return { allowLocalAuthentication: true, ignoreUnknownUsernames: true };
|
|
}
|
|
return {};
|
|
});
|
|
|
|
mockSearchUsers.mockResolvedValue({ result: [mockUser] });
|
|
mockCreate.mockReturnValue({});
|
|
mockCreateSessionAndUpdateCookie.mockResolvedValue({ session: mockSession, sessionCookie: {} });
|
|
mockListAuthenticationMethodTypes.mockResolvedValue({
|
|
authMethodTypes: [AuthenticationMethodType.PASSWORD],
|
|
});
|
|
|
|
const result = await sendLoginname({
|
|
loginName: "input-name",
|
|
organization: "context-org", // Context has ignore=false
|
|
ignoreUnknownUsernames: false,
|
|
});
|
|
|
|
expect(result).toHaveProperty("redirect");
|
|
// Should result in resolved name because context says SHOW
|
|
expect((result as any).redirect).toContain("loginName=user%40example.com");
|
|
});
|
|
});
|
|
});
|