chore(login): add unit tests for core business logic (#11083)

# Which Problems Are Solved

- Adds basic unit tests for core business logic
- Adds playwright tests for core flows

# How the Problems Are Solved

- Extends unit tests for session
- Adds unit tests for other core business logic (oidc, idp, auth etc)

# Additional Changes

- N/A

---------

Co-authored-by: Max Peintner <[email protected]>
This commit is contained in:
David Skewis
2025-11-27 16:55:40 +00:00
committed by GitHub
co-authored by Max Peintner
parent 207095f7c1
commit 86cce629c7
12 changed files with 4834 additions and 9 deletions
+320 -7
View File
@@ -20,24 +20,20 @@ import { isSessionValid } from "./session";
import * as verifyHelperModule from "./verify-helper";
import * as zitadelModule from "./zitadel";
// Mock the zitadel client timestampDate function
vi.mock("@zitadel/client", () => ({
timestampDate: vi.fn(),
}));
// Mock the zitadel module
vi.mock("./zitadel", () => ({
listAuthenticationMethodTypes: vi.fn(),
getLoginSettings: vi.fn(),
getUserByID: vi.fn(),
}));
// Mock the verify-helper module
vi.mock("./verify-helper", () => ({
shouldEnforceMFA: vi.fn(),
}));
// Mock environment variables
const originalEnv = process.env;
describe("isSessionValid", () => {
@@ -51,10 +47,16 @@ describe("isSessionValid", () => {
// @ts-ignore - delete is OK for test environment variables
delete process.env.EMAIL_VERIFICATION;
// Setup timestampDate mock to return valid dates
// Setup timestampDate mock to match actual behavior:
// - Returns Invalid Date for malformed timestamps (empty object, invalid seconds)
// - Throws for null/undefined
// - Returns valid Date for proper timestamps
vi.mocked(timestampDate).mockImplementation((timestamp: any) => {
if (!timestamp || !timestamp.seconds) {
return new Date(); // Return current date for invalid timestamps
if (timestamp === null || timestamp === undefined) {
throw new TypeError(`Cannot read properties of ${timestamp} (reading 'seconds')`);
}
if (!timestamp.seconds || typeof timestamp.seconds !== "bigint") {
return new Date("invalid"); // Returns Invalid Date for malformed timestamps
}
return new Date(Number(timestamp.seconds) * 1000);
});
@@ -1148,5 +1150,316 @@ describe("isSessionValid", () => {
expect(result).toBe(true);
});
test("should handle session with null expirationDate", async () => {
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
expirationDate: null,
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [],
} as any);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: false,
forceMfaLocalOnly: false,
} as any);
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
expect(result).toBe(true);
});
test("should handle session expiring exactly now", async () => {
const verifiedTimestamp = createMockTimestamp();
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
// Create timestamp for current moment (0ms offset)
const nowTimestamp = createMockTimestamp(0);
const session = createMockSession({
expirationDate: nowTimestamp,
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [],
} as any);
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
// Session expiring exactly now should be considered expired
expect(result).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith("Session is expired", expect.any(String));
consoleSpy.mockRestore();
});
test("should handle API errors gracefully when fetching login settings", async () => {
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [],
} as any);
// Simulate API error
vi.mocked(zitadelModule.getLoginSettings).mockRejectedValue(new Error("API connection failed"));
// Should throw or handle error appropriately
await expect(isSessionValid({ serviceUrl: mockServiceUrl, session })).rejects.toThrow();
});
test("should handle API errors gracefully when listing auth method types", async () => {
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
},
});
// Simulate API error
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockRejectedValue(new Error("API connection failed"));
// Should throw or handle error appropriately
await expect(isSessionValid({ serviceUrl: mockServiceUrl, session })).rejects.toThrow();
});
test("should handle malformed timestamp in session", async () => {
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
expirationDate: { seconds: "invalid" } as any,
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [],
} as any);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: false,
forceMfaLocalOnly: false,
} as any);
// Malformed timestamps are handled gracefully, returning false for invalid sessions
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
expect(result).toBe(false);
consoleSpy.mockRestore();
});
test("should handle session with all MFA factors verified", async () => {
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
totp: {
verifiedAt: verifiedTimestamp,
},
otpEmail: {
verifiedAt: verifiedTimestamp,
},
otpSms: {
verifiedAt: verifiedTimestamp,
},
webAuthN: {
verifiedAt: verifiedTimestamp,
},
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [
AuthenticationMethodType.TOTP,
AuthenticationMethodType.OTP_EMAIL,
AuthenticationMethodType.OTP_SMS,
AuthenticationMethodType.U2F,
],
} as any);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: true,
forceMfaLocalOnly: false,
} as any);
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
expect(result).toBe(true);
});
test("should handle session with very old verifiedAt timestamps", async () => {
// Timestamp from 10 years ago
const oldTimestamp = {
seconds: BigInt(Math.floor((Date.now() - 10 * 365 * 24 * 60 * 60 * 1000) / 1000)),
};
const futureExpiration = createMockTimestamp();
const session = createMockSession({
expirationDate: futureExpiration,
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: oldTimestamp,
},
password: {
verifiedAt: oldTimestamp,
},
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [],
} as any);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: false,
forceMfaLocalOnly: false,
} as any);
// Old verification timestamps are still valid as long as session hasn't expired
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
expect(result).toBe(true);
});
test("should handle session with future verifiedAt timestamps", async () => {
// Timestamp 1 hour in the future
const futureTimestamp = createMockTimestamp(3600000);
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: futureTimestamp,
},
password: {
verifiedAt: futureTimestamp,
},
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [],
} as any);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: false,
forceMfaLocalOnly: false,
} as any);
// Future timestamps should still be considered valid
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
expect(result).toBe(true);
});
test("should handle concurrent calls to isSessionValid", async () => {
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [],
} as any);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: false,
forceMfaLocalOnly: false,
} as any);
// Make multiple concurrent calls
const promises = Array(5)
.fill(null)
.map(() => isSessionValid({ serviceUrl: mockServiceUrl, session }));
const results = await Promise.all(promises);
// All should return same result
expect(results.every((r) => r === true)).toBe(true);
});
});
});