mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-14 01:45:12 +00:00
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:
co-authored by
Max Peintner
parent
207095f7c1
commit
86cce629c7
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user