Files
cal.diy/packages/features/auth/lib/next-auth-options.test.ts
Benny JooandDevin AI ab21c7f805 refactor: Cal.diy (#28903)
* feat: Cal.diy — community-driven MIT-licensed fork of Cal.com

This squashed commit contains all Cal.diy changes applied on top of calcom/cal.com main:

- Rebrand Cal.com to Cal.diy across the entire codebase
- Remove Enterprise Edition (EE) features, license checks, and AGPL restrictions
- Switch license from AGPL-3.0 to MIT
- Remove docs/ directory (migrated to Nextra at cal.diy)
- Remove dead code: org tests, EE tips, platform nav, premium username, SAML/SSO, etc.
- Clean up .env.example for self-hosted Cal.diy
- Update Docker image references to calcom/cal.diy
- Update README, CONTRIBUTING.md, and issue templates for Cal.diy community fork
- Add PR welcome bot for Cal.diy contributors
- Fix API v2 breaking changes oasdiff ignore entries
- Replace Blacksmith CI runners with default GitHub Actions

3893 files changed, 20789 insertions(+), 411020 deletions(-)

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* refactor: remove org-specific /organizations/:orgId endpoints from API v2 atoms controllers (#1701)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: revert Cal.diy Inc to Cal.com, Inc. in license files, copyright notices, and package metadata (#1702)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* rip out org related comments in api v2

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-04-15 09:52:36 -03:00

1290 lines
40 KiB
TypeScript

import { IdentityProvider, UserPermissionRole } from "@calcom/prisma/enums";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ErrorCode } from "./ErrorCode";
// Mock dependencies
vi.mock("@calcom/prisma", () => ({
prisma: {
user: {
update: vi.fn(),
},
},
default: {
user: {
update: vi.fn(),
},
},
}));
const mockFindByEmailAndIncludeProfilesAndPassword = vi.fn();
vi.mock("@calcom/features/users/repositories/UserRepository", () => {
return {
UserRepository: vi.fn().mockImplementation(function () {
return {
findByEmailAndIncludeProfilesAndPassword: mockFindByEmailAndIncludeProfilesAndPassword,
};
}),
};
});
vi.mock("./verifyPassword", () => ({
verifyPassword: vi.fn(),
}));
vi.mock("@calcom/lib/checkRateLimitAndThrowError", () => ({
checkRateLimitAndThrowError: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("@calcom/lib/server/PiiHasher", () => ({
hashEmail: vi.fn((email: string) => `hashed_${email}`),
}));
vi.mock("@calcom/lib/totp", () => ({
totpAuthenticatorCheck: vi.fn(),
}));
vi.mock("@calcom/lib/crypto", () => ({
symmetricDecrypt: vi.fn(),
symmetricEncrypt: vi.fn(),
}));
vi.mock("@calcom/lib/auth/isPasswordValid", () => ({
isPasswordValid: vi.fn(),
}));
vi.mock("@calcom/lib/env", () => ({
isENVDev: false,
}));
vi.mock("@calcom/lib/constants", async (importOriginal) => {
const actual = await importOriginal<typeof import("@calcom/lib/constants")>();
return {
...actual,
IS_TEAM_BILLING_ENABLED: false,
ENABLE_PROFILE_SWITCHER: false,
WEBAPP_URL: "http://localhost:3000",
};
});
vi.mock("@calcom/lib/logger", () => ({
default: {
getSubLogger: vi.fn(() => ({
debug: vi.fn(),
error: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
})),
},
}));
vi.mock("@calcom/lib/safeStringify", () => ({
safeStringify: vi.fn((obj) => JSON.stringify(obj)),
}));
vi.mock("./next-auth-custom-adapter", () => ({
default: vi.fn(() => ({
linkAccount: vi.fn(),
})),
}));
vi.mock("@calcom/i18n/server", () => ({
getTranslation: vi.fn().mockResolvedValue((key: string) => key),
}));
vi.mock("@calcom/features/profile/repositories/ProfileRepository", () => ({
ProfileRepository: {
findAllProfilesForUserIncludingMovedUser: vi.fn(),
findByUpIdWithAuth: vi.fn(),
},
}));
// Additional mocks for signIn/JWT callback tests
const mockCredentialRepoFindFirst = vi.fn();
const mockCredentialRepoFindFirstByUserIdAndType = vi.fn();
const mockCredentialRepoCreate = vi.fn();
const mockBuildCredentialCreateData = vi.fn();
const mockUpdateProfilePhotoMicrosoft = vi.fn();
const mockUpdateProfilePhotoGoogle = vi.fn();
const mockGetIdentityProvider = vi.fn();
const mockWaitUntil = vi.fn();
const mockPrismaUserFindFirst = vi.fn();
const mockPrismaUserCreate = vi.fn();
const mockPrismaUserUpdate = vi.fn();
const mockPrismaTeamFindFirst = vi.fn();
const mockPrismaSelectedCalendarCreate = vi.fn();
const mockLinkAccount = vi.fn();
vi.mock("@calcom/features/credentials/repositories/CredentialRepository", () => ({
CredentialRepository: {
findFirstByAppIdAndUserId: (...args: unknown[]) => mockCredentialRepoFindFirst(...args),
findFirstByUserIdAndType: (...args: unknown[]) => mockCredentialRepoFindFirstByUserIdAndType(...args),
create: (...args: unknown[]) => mockCredentialRepoCreate(...args),
},
}));
vi.mock("@calcom/features/credentials/services/CredentialDataService", () => ({
buildCredentialCreateData: (...args: unknown[]) => mockBuildCredentialCreateData(...args),
}));
vi.mock("@calcom/app-store/_utils/oauth/updateProfilePhotoMicrosoft", () => ({
updateProfilePhotoMicrosoft: (...args: unknown[]) => mockUpdateProfilePhotoMicrosoft(...args),
}));
vi.mock("@calcom/app-store/_utils/oauth/updateProfilePhotoGoogle", () => ({
updateProfilePhotoGoogle: (...args: unknown[]) => mockUpdateProfilePhotoGoogle(...args),
}));
vi.mock("@calcom/features/auth/lib/identityProviders", () => ({
getIdentityProvider: (...args: unknown[]) => mockGetIdentityProvider(...args),
}));
vi.mock("@calcom/features/auth/lib/outlook", () => ({
OUTLOOK_CLIENT_ID: "mock-client-id",
OUTLOOK_CLIENT_SECRET: "mock-client-secret",
OUTLOOK_LOGIN_ENABLED: true,
}));
vi.mock("@vercel/functions", () => ({
waitUntil: (...args: unknown[]) => mockWaitUntil(...args),
}));
vi.mock("@calcom/lib/default-cookies", () => ({
defaultCookies: vi.fn(() => ({})),
}));
vi.mock("@calcom/lib/random", () => ({
randomString: vi.fn(() => "abc123"),
}));
vi.mock("@calcom/lib/slugify", () => ({
default: vi.fn((s: string) => s.toLowerCase().replace(/\s+/g, "-")),
}));
vi.mock("@calcom/app-store/googlecalendar/lib/CalendarService", () => ({
createGoogleCalendarServiceWithGoogleType: vi.fn(),
}));
vi.mock("googleapis-common", () => ({
OAuth2Client: vi.fn(),
}));
vi.mock("@googleapis/calendar", () => ({
calendar_v3: {
Calendar: vi.fn(),
},
}));
vi.mock("next-auth/jwt", () => ({
encode: vi.fn(),
}));
vi.mock("./signJwt", () => ({
default: vi.fn().mockResolvedValue("mock-jwt"),
}));
vi.mock("./dub", () => ({
dub: { track: { lead: vi.fn() } },
}));
vi.mock("../signup/utils/getOrgUsernameFromEmail", () => ({
getOrgUsernameFromEmail: vi.fn((email: string) => email.split("@")[0]),
}));
vi.mock("next-auth/providers/azure-ad", () => ({ default: vi.fn() }));
vi.mock("next-auth/providers/credentials", () => ({ default: vi.fn(() => ({ id: "credentials" })) }));
vi.mock("next-auth/providers/email", () => ({ default: vi.fn() }));
vi.mock("next-auth/providers/google", () => ({ default: vi.fn() }));
describe("CredentialsProvider authorize", () => {
let authorizeCredentials: typeof import("./next-auth-options").authorizeCredentials;
let verifyPassword: any;
beforeEach(async () => {
vi.clearAllMocks();
mockFindByEmailAndIncludeProfilesAndPassword.mockReset();
const verifyPasswordModule = await import("./verifyPassword");
verifyPassword = verifyPasswordModule.verifyPassword;
// Import the exported authorize function directly
const authModule = await import("./next-auth-options");
authorizeCredentials = authModule.authorizeCredentials;
});
const createMockUser = (overrides: Partial<any> = {}) => ({
id: 1,
email: "test@example.com",
name: "Test User",
username: "testuser",
role: UserPermissionRole.USER,
locked: false,
identityProvider: IdentityProvider.CAL,
twoFactorEnabled: false,
twoFactorSecret: null,
backupCodes: null,
password: {
hash: "$2a$10$hashedpassword",
},
allProfiles: [
{
id: 1,
upId: "usr_123",
username: "testuser",
},
],
teams: [],
...overrides,
});
describe("Password validation", () => {
it("should throw error when user has no password hash with CAL identity provider", async () => {
const mockUser = createMockUser({
password: null,
identityProvider: IdentityProvider.CAL,
});
mockFindByEmailAndIncludeProfilesAndPassword.mockResolvedValue(mockUser);
await expect(
authorizeCredentials({
email: "test@example.com",
password: "password123",
} as any)
).rejects.toThrow(ErrorCode.IncorrectEmailPassword);
});
it("should throw error when user has no password hash with Google identity provider", async () => {
const mockUser = createMockUser({
password: null,
identityProvider: IdentityProvider.GOOGLE,
});
mockFindByEmailAndIncludeProfilesAndPassword.mockResolvedValue(mockUser);
await expect(
authorizeCredentials({
email: "test@example.com",
password: "password123",
} as any)
).rejects.toThrow(ErrorCode.IncorrectEmailPassword);
});
it("should throw error when user has no password hash with SAML identity provider", async () => {
const mockUser = createMockUser({
password: null,
identityProvider: IdentityProvider.SAML,
});
mockFindByEmailAndIncludeProfilesAndPassword.mockResolvedValue(mockUser);
await expect(
authorizeCredentials({
email: "test@example.com",
password: "password123",
} as any)
).rejects.toThrow(ErrorCode.IncorrectEmailPassword);
});
it("should throw error when user has no password hash even with TOTP code provided", async () => {
const mockUser = createMockUser({
password: null,
identityProvider: IdentityProvider.CAL,
});
mockFindByEmailAndIncludeProfilesAndPassword.mockResolvedValue(mockUser);
await expect(
authorizeCredentials({
email: "test@example.com",
password: "password123",
totpCode: "123456",
} as any)
).rejects.toThrow(ErrorCode.IncorrectEmailPassword);
});
it("should throw error when user has no password hash with Google identity provider and TOTP code", async () => {
const mockUser = createMockUser({
password: null,
identityProvider: IdentityProvider.GOOGLE,
});
mockFindByEmailAndIncludeProfilesAndPassword.mockResolvedValue(mockUser);
await expect(
authorizeCredentials({
email: "test@example.com",
password: "password123",
totpCode: "123456",
} as any)
).rejects.toThrow(ErrorCode.IncorrectEmailPassword);
});
it("should throw error when user has no password hash with SAML identity provider and TOTP code", async () => {
const mockUser = createMockUser({
password: null,
identityProvider: IdentityProvider.SAML,
});
mockFindByEmailAndIncludeProfilesAndPassword.mockResolvedValue(mockUser);
await expect(
authorizeCredentials({
email: "test@example.com",
password: "password123",
totpCode: "123456",
} as any)
).rejects.toThrow(ErrorCode.IncorrectEmailPassword);
});
});
describe("Inactive admin reason", () => {
it("sets reason to 'both' when password is invalid and 2FA is disabled", async () => {
const { isPasswordValid } = await import("@calcom/lib/auth/isPasswordValid");
vi.mocked(isPasswordValid).mockReturnValue(false);
vi.mocked(verifyPassword).mockResolvedValue(true);
const mockUser = createMockUser({
role: UserPermissionRole.ADMIN,
identityProvider: IdentityProvider.CAL,
twoFactorEnabled: false,
});
mockFindByEmailAndIncludeProfilesAndPassword.mockResolvedValue(mockUser);
const result = await authorizeCredentials({
email: mockUser.email,
password: "password123",
totpCode: "",
backupCode: "",
});
expect(result?.role).toBe("INACTIVE_ADMIN");
expect(result?.inactiveAdminReason).toBe("both");
});
it("sets reason to '2fa' when password is valid and 2FA is disabled", async () => {
const { isPasswordValid } = await import("@calcom/lib/auth/isPasswordValid");
vi.mocked(isPasswordValid).mockReturnValue(true);
vi.mocked(verifyPassword).mockResolvedValue(true);
const mockUser = createMockUser({
role: UserPermissionRole.ADMIN,
identityProvider: IdentityProvider.CAL,
twoFactorEnabled: false,
});
mockFindByEmailAndIncludeProfilesAndPassword.mockResolvedValue(mockUser);
const result = await authorizeCredentials({
email: mockUser.email,
password: "password123",
totpCode: "",
backupCode: "",
});
expect(result?.role).toBe("INACTIVE_ADMIN");
expect(result?.inactiveAdminReason).toBe("2fa");
});
it("sets reason to 'password' when password is invalid and 2FA is enabled", async () => {
const originalKey = process.env.CALENDSO_ENCRYPTION_KEY;
process.env.CALENDSO_ENCRYPTION_KEY = "test";
try {
const { isPasswordValid } = await import("@calcom/lib/auth/isPasswordValid");
vi.mocked(isPasswordValid).mockReturnValue(false);
vi.mocked(verifyPassword).mockResolvedValue(true);
const { symmetricDecrypt } = await import("@calcom/lib/crypto");
vi.mocked(symmetricDecrypt).mockReturnValue("a".repeat(32));
const { totpAuthenticatorCheck } = await import("@calcom/lib/totp");
vi.mocked(totpAuthenticatorCheck).mockReturnValue(true);
const mockUser = createMockUser({
role: UserPermissionRole.ADMIN,
identityProvider: IdentityProvider.CAL,
twoFactorEnabled: true,
twoFactorSecret: "encrypted_secret",
});
mockFindByEmailAndIncludeProfilesAndPassword.mockResolvedValue(mockUser);
const result = await authorizeCredentials({
email: mockUser.email,
password: "password123",
totpCode: "123456",
backupCode: "",
});
expect(result?.role).toBe("INACTIVE_ADMIN");
expect(result?.inactiveAdminReason).toBe("password");
} finally {
process.env.CALENDSO_ENCRYPTION_KEY = originalKey;
}
});
it("does not set inactiveAdminReason when admin requirements are met", async () => {
const originalKey = process.env.CALENDSO_ENCRYPTION_KEY;
process.env.CALENDSO_ENCRYPTION_KEY = "test";
try {
const { isPasswordValid } = await import("@calcom/lib/auth/isPasswordValid");
vi.mocked(isPasswordValid).mockReturnValue(true);
vi.mocked(verifyPassword).mockResolvedValue(true);
const { symmetricDecrypt } = await import("@calcom/lib/crypto");
vi.mocked(symmetricDecrypt).mockReturnValue("a".repeat(32));
const { totpAuthenticatorCheck } = await import("@calcom/lib/totp");
vi.mocked(totpAuthenticatorCheck).mockReturnValue(true);
const mockUser = createMockUser({
role: UserPermissionRole.ADMIN,
identityProvider: IdentityProvider.CAL,
twoFactorEnabled: true,
twoFactorSecret: "encrypted_secret",
});
mockFindByEmailAndIncludeProfilesAndPassword.mockResolvedValue(mockUser);
const result = await authorizeCredentials({
email: mockUser.email,
password: "password123",
totpCode: "123456",
backupCode: "",
});
expect(result?.role).toBe(UserPermissionRole.ADMIN);
expect(result?.inactiveAdminReason).toBeUndefined();
} finally {
process.env.CALENDSO_ENCRYPTION_KEY = originalKey;
}
});
});
});
describe("Azure AD signIn callback", () => {
let getOptions: typeof import("./next-auth-options").getOptions;
let signInCallback: NonNullable<ReturnType<typeof getOptions>["callbacks"]>["signIn"];
beforeEach(async () => {
vi.clearAllMocks();
mockGetIdentityProvider.mockImplementation((provider: string) => {
const map: Record<string, string> = {
"azure-ad": "AZUREAD",
google: "GOOGLE",
saml: "SAML",
"saml-idp": "SAML",
cal: "CAL",
};
return map[provider] ?? null;
});
// Mock prisma methods used in signIn callback
const prismaModule = await import("@calcom/prisma");
const prismaDefault = (prismaModule as any).default;
prismaDefault.user = {
...prismaDefault.user,
findFirst: mockPrismaUserFindFirst,
create: mockPrismaUserCreate,
update: mockPrismaUserUpdate,
};
prismaDefault.team = {
findFirst: mockPrismaTeamFindFirst,
};
prismaDefault.selectedCalendar = {
create: mockPrismaSelectedCalendarCreate,
};
mockPrismaUserFindFirst.mockResolvedValue(null);
mockPrismaUserCreate.mockResolvedValue({ id: 100, email: "new@example.com", twoFactorEnabled: false });
mockPrismaUserUpdate.mockResolvedValue({});
mockPrismaTeamFindFirst.mockResolvedValue(null);
mockUpdateProfilePhotoMicrosoft.mockResolvedValue(undefined);
// Setup mock adapter
const adapterModule = await import("./next-auth-custom-adapter");
(adapterModule.default as any).mockReturnValue({ linkAccount: mockLinkAccount });
mockLinkAccount.mockResolvedValue(undefined);
mockWaitUntil.mockImplementation((p: Promise<any>) => p.catch(() => {}));
const authModule = await import("./next-auth-options");
getOptions = authModule.getOptions;
const options = getOptions({ getDubId: () => undefined, getTrackingData: () => ({}) as any });
signInCallback = options.callbacks!.signIn! as any;
});
describe("Azure AD email verification (xms_edov)", () => {
const baseParams = {
user: { id: "1", email: "user@example.com", name: "User", emailVerified: null },
account: {
provider: "azure-ad",
providerAccountId: "azure-123",
type: "oauth" as const,
},
credentials: undefined,
email: undefined,
};
it("allows login when xms_edov is true (boolean)", async () => {
mockPrismaUserFindFirst.mockResolvedValue({
id: 1,
email: "user@example.com",
accounts: [{ provider: "azure-ad" }],
twoFactorEnabled: false,
identityProvider: "AZUREAD",
});
const result = await signInCallback({
...baseParams,
profile: { email_verified: false, xms_edov: true } as any,
} as any);
expect(result).toBe(true);
});
it("allows login when xms_edov is string 'true'", async () => {
mockPrismaUserFindFirst.mockResolvedValue({
id: 1,
email: "user@example.com",
accounts: [{ provider: "azure-ad" }],
twoFactorEnabled: false,
identityProvider: "AZUREAD",
});
const result = await signInCallback({
...baseParams,
profile: { xms_edov: "true" } as any,
} as any);
expect(result).toBe(true);
});
it("allows login when xms_edov is string '1'", async () => {
mockPrismaUserFindFirst.mockResolvedValue({
id: 1,
email: "user@example.com",
accounts: [{ provider: "azure-ad" }],
twoFactorEnabled: false,
identityProvider: "AZUREAD",
});
const result = await signInCallback({
...baseParams,
profile: { xms_edov: "1" } as any,
} as any);
expect(result).toBe(true);
});
it("allows login when xms_edov is number 1", async () => {
mockPrismaUserFindFirst.mockResolvedValue({
id: 1,
email: "user@example.com",
accounts: [{ provider: "azure-ad" }],
twoFactorEnabled: false,
identityProvider: "AZUREAD",
});
const result = await signInCallback({
...baseParams,
profile: { xms_edov: 1 } as any,
} as any);
expect(result).toBe(true);
});
it("rejects login when xms_edov is false", async () => {
const result = await signInCallback({
...baseParams,
profile: { xms_edov: false } as any,
} as any);
expect(result).toBe("/auth/error?error=unverified-email");
});
it("rejects login when xms_edov is undefined", async () => {
const result = await signInCallback({
...baseParams,
profile: {} as any,
} as any);
expect(result).toBe("/auth/error?error=unverified-email");
});
it("rejects login when xms_edov is 0", async () => {
const result = await signInCallback({
...baseParams,
profile: { xms_edov: 0 } as any,
} as any);
expect(result).toBe("/auth/error?error=unverified-email");
});
it("rejects login when xms_edov is string '0'", async () => {
const result = await signInCallback({
...baseParams,
profile: { xms_edov: "0" } as any,
} as any);
expect(result).toBe("/auth/error?error=unverified-email");
});
it("allows non-AZUREAD with email_verified=true", async () => {
mockPrismaUserFindFirst.mockResolvedValue({
id: 1,
email: "user@example.com",
accounts: [{ provider: "google" }],
twoFactorEnabled: false,
identityProvider: "GOOGLE",
});
const result = await signInCallback({
user: { id: "1", email: "user@example.com", name: "User", emailVerified: null },
account: { provider: "google", providerAccountId: "google-123", type: "oauth" as const },
profile: { email_verified: true } as any,
credentials: undefined,
email: undefined,
} as any);
expect(result).toBe(true);
});
it("rejects non-AZUREAD with email_verified=false", async () => {
const result = await signInCallback({
user: { id: "1", email: "user@example.com", name: "User", emailVerified: null },
account: { provider: "google", providerAccountId: "google-123", type: "oauth" as const },
profile: { email_verified: false } as any,
credentials: undefined,
email: undefined,
} as any);
expect(result).toBe("/auth/error?error=unverified-email");
});
});
describe("Azure AD profile photo on new user (signIn callback)", () => {
const baseUser = { id: "1", email: "newuser@example.com", name: "New User", emailVerified: null };
it("calls updateProfilePhotoMicrosoft for new Azure AD user with access_token", async () => {
// No existing user
mockPrismaUserFindFirst.mockResolvedValue(null);
mockPrismaUserCreate.mockResolvedValue({
id: 100,
email: "newuser@example.com",
twoFactorEnabled: false,
});
await signInCallback({
user: baseUser,
account: {
provider: "azure-ad",
providerAccountId: "azure-new-123",
type: "oauth" as const,
access_token: "new-user-token",
},
profile: { xms_edov: true } as any,
credentials: undefined,
email: undefined,
} as any);
expect(mockUpdateProfilePhotoMicrosoft).toHaveBeenCalledWith("new-user-token", 100);
});
it("skips updateProfilePhotoMicrosoft for new Azure AD user without access_token", async () => {
mockPrismaUserFindFirst.mockResolvedValue(null);
mockPrismaUserCreate.mockResolvedValue({
id: 101,
email: "newuser@example.com",
twoFactorEnabled: false,
});
await signInCallback({
user: baseUser,
account: {
provider: "azure-ad",
providerAccountId: "azure-new-456",
type: "oauth" as const,
},
profile: { xms_edov: true } as any,
credentials: undefined,
email: undefined,
} as any);
expect(mockUpdateProfilePhotoMicrosoft).not.toHaveBeenCalled();
});
it("skips updateProfilePhotoMicrosoft for new Google user", async () => {
mockPrismaUserFindFirst.mockResolvedValue(null);
mockPrismaUserCreate.mockResolvedValue({
id: 102,
email: "newuser@example.com",
twoFactorEnabled: false,
});
await signInCallback({
user: baseUser,
account: {
provider: "google",
providerAccountId: "google-new-789",
type: "oauth" as const,
access_token: "google-token",
},
profile: { email_verified: true } as any,
credentials: undefined,
email: undefined,
} as any);
expect(mockUpdateProfilePhotoMicrosoft).not.toHaveBeenCalled();
});
it("photo update failure does not crash signup", async () => {
mockPrismaUserFindFirst.mockResolvedValue(null);
mockPrismaUserCreate.mockResolvedValue({
id: 103,
email: "newuser@example.com",
twoFactorEnabled: false,
});
mockUpdateProfilePhotoMicrosoft.mockRejectedValue(new Error("Photo update failed"));
const result = await signInCallback({
user: baseUser,
account: {
provider: "azure-ad",
providerAccountId: "azure-fail-123",
type: "oauth" as const,
access_token: "fail-token",
},
profile: { xms_edov: true } as any,
credentials: undefined,
email: undefined,
} as any);
// Should still return something (true or error redirect), not throw
expect(result).toBeDefined();
});
});
describe("Azure AD identity provider conversion (signIn callback)", () => {
it("CAL user with verified email converts to AZUREAD on Azure AD login", async () => {
// No existing user with AZUREAD identity
mockPrismaUserFindFirst
.mockResolvedValueOnce(null) // First call: lookup by identityProvider + providerAccountId
.mockResolvedValueOnce(null) // Legacy lookup
.mockResolvedValueOnce({
// Lookup by email
id: 50,
email: "user@example.com",
emailVerified: new Date(),
identityProvider: "CAL",
password: { hash: "hashed" },
twoFactorEnabled: false,
});
const result = await signInCallback({
user: { id: "1", email: "user@example.com", name: "User", emailVerified: null },
account: { provider: "azure-ad", providerAccountId: "azure-conv-1", type: "oauth" as const },
profile: { xms_edov: true } as any,
credentials: undefined,
email: undefined,
} as any);
expect(mockPrismaUserUpdate).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
identityProvider: "AZUREAD",
identityProviderId: "azure-conv-1",
}),
})
);
expect(result).toBe(true);
});
it("GOOGLE user auto-merges on Azure AD login when email is verified", async () => {
mockPrismaUserFindFirst.mockResolvedValueOnce(null).mockResolvedValueOnce(null).mockResolvedValueOnce({
id: 51,
email: "user@example.com",
emailVerified: new Date(),
identityProvider: "GOOGLE",
password: null,
twoFactorEnabled: false,
});
const result = await signInCallback({
user: { id: "1", email: "user@example.com", name: "User", emailVerified: null },
account: { provider: "azure-ad", providerAccountId: "azure-conv-2", type: "oauth" as const },
profile: { xms_edov: true } as any,
credentials: undefined,
email: undefined,
} as any);
// With isVerified=true and non-CAL provider, auto-merge path is taken (returns true directly)
expect(result).toBe(true);
});
it("AZUREAD user auto-merges on Google login when email is verified", async () => {
mockPrismaUserFindFirst.mockResolvedValueOnce(null).mockResolvedValueOnce(null).mockResolvedValueOnce({
id: 52,
email: "user@example.com",
emailVerified: new Date(),
identityProvider: "AZUREAD",
password: null,
twoFactorEnabled: false,
});
const result = await signInCallback({
user: { id: "1", email: "user@example.com", name: "User", emailVerified: null },
account: { provider: "google", providerAccountId: "google-conv-1", type: "oauth" as const },
profile: { email_verified: true } as any,
credentials: undefined,
email: undefined,
} as any);
// With isVerified=true and non-CAL provider, auto-merge path is taken (returns true directly)
expect(result).toBe(true);
});
it("unverified CAL account blocks Azure AD linking (anti-hijack)", async () => {
mockPrismaUserFindFirst
.mockResolvedValueOnce(null)
.mockResolvedValueOnce(null)
.mockResolvedValueOnce({
id: 53,
email: "user@example.com",
emailVerified: null,
identityProvider: "CAL",
password: { hash: "hashed" },
twoFactorEnabled: false,
});
const result = await signInCallback({
user: { id: "1", email: "user@example.com", name: "User", emailVerified: null },
account: { provider: "azure-ad", providerAccountId: "azure-hijack-1", type: "oauth" as const },
profile: { xms_edov: true } as any,
credentials: undefined,
email: undefined,
} as any);
expect(result).toBe("/auth/error?error=unverified-email");
});
});
});
describe("Azure AD JWT callback", () => {
let getOptions: typeof import("./next-auth-options").getOptions;
let jwtCallback: NonNullable<ReturnType<typeof getOptions>["callbacks"]>["jwt"];
let originalFetch: typeof globalThis.fetch;
beforeEach(async () => {
vi.clearAllMocks();
originalFetch = globalThis.fetch;
mockGetIdentityProvider.mockImplementation((provider: string) => {
const map: Record<string, string> = {
"azure-ad": "AZUREAD",
google: "GOOGLE",
saml: "SAML",
"saml-idp": "SAML",
cal: "CAL",
};
return map[provider] ?? null;
});
mockCredentialRepoCreate.mockResolvedValue({ id: 1 });
mockBuildCredentialCreateData.mockImplementation((data: any) => data);
mockUpdateProfilePhotoMicrosoft.mockResolvedValue(undefined);
// Setup prisma mocks
const prismaModule = await import("@calcom/prisma");
const prismaDefault = (prismaModule as any).default;
prismaDefault.user = {
...prismaDefault.user,
findFirst: mockPrismaUserFindFirst,
};
prismaDefault.selectedCalendar = {
create: mockPrismaSelectedCalendarCreate,
};
prismaDefault.membership = {
findUnique: vi.fn().mockResolvedValue(null),
};
mockPrismaSelectedCalendarCreate.mockResolvedValue({});
const { ProfileRepository } = await import("@calcom/features/profile/repositories/ProfileRepository");
(ProfileRepository.findAllProfilesForUserIncludingMovedUser as any).mockResolvedValue([
{ id: 1, upId: "usr_1" },
]);
(ProfileRepository.findByUpIdWithAuth as any).mockResolvedValue({
id: 1,
username: "testuser",
organization: null,
});
const authModule = await import("./next-auth-options");
getOptions = authModule.getOptions;
const options = getOptions({ getDubId: () => undefined, getTrackingData: () => ({}) as any });
jwtCallback = options.callbacks!.jwt! as any;
});
afterEach(() => {
globalThis.fetch = originalFetch;
});
describe("Azure AD calendar auto-install", () => {
const baseToken = { sub: "1", email: "user@example.com", upId: "usr_1" } as any;
const baseUser = {
id: "1",
email: "user@example.com",
name: "Test",
username: "test",
role: "USER",
locale: "en",
} as any;
it("creates office365-calendar credential on first login with all scopes", async () => {
mockPrismaUserFindFirst.mockResolvedValue({
id: 1,
email: "user@example.com",
name: "Test",
username: "test",
role: "USER",
locale: "en",
avatarUrl: null,
identityProvider: "AZUREAD",
identityProviderId: "azure-123",
teams: [],
movedToProfileId: null,
});
mockCredentialRepoFindFirst.mockResolvedValue(null);
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ value: [{ id: "cal-id-1", isDefaultCalendar: true }] }),
}) as unknown as typeof fetch;
await jwtCallback({
token: baseToken,
user: baseUser,
account: {
provider: "azure-ad",
providerAccountId: "azure-123",
type: "oauth",
access_token: "at-123",
refresh_token: "rt-123",
expires_at: 1700000000,
scope: "User.Read Calendars.Read Calendars.ReadWrite offline_access",
},
trigger: undefined,
session: undefined,
} as any);
expect(mockBuildCredentialCreateData).toHaveBeenCalledWith(
expect.objectContaining({
appId: "office365-calendar",
type: "office365_calendar",
})
);
expect(mockCredentialRepoCreate).toHaveBeenCalled();
});
it("skips creation when office365-calendar credential already exists", async () => {
mockPrismaUserFindFirst.mockResolvedValue({
id: 1,
email: "user@example.com",
name: "Test",
username: "test",
role: "USER",
locale: "en",
avatarUrl: null,
identityProvider: "AZUREAD",
identityProviderId: "azure-123",
teams: [],
movedToProfileId: null,
});
mockCredentialRepoFindFirst.mockResolvedValue({ id: 99 });
await jwtCallback({
token: baseToken,
user: baseUser,
account: {
provider: "azure-ad",
providerAccountId: "azure-123",
type: "oauth",
access_token: "at-123",
scope: "User.Read Calendars.Read Calendars.ReadWrite",
},
trigger: undefined,
session: undefined,
} as any);
expect(mockBuildCredentialCreateData).not.toHaveBeenCalledWith(
expect.objectContaining({ appId: "office365-calendar" })
);
});
it("skips creation when calendar scopes are missing", async () => {
mockPrismaUserFindFirst.mockResolvedValue({
id: 1,
email: "user@example.com",
name: "Test",
username: "test",
role: "USER",
locale: "en",
avatarUrl: null,
identityProvider: "AZUREAD",
identityProviderId: "azure-123",
teams: [],
movedToProfileId: null,
});
mockCredentialRepoFindFirst.mockResolvedValue(null);
await jwtCallback({
token: baseToken,
user: baseUser,
account: {
provider: "azure-ad",
providerAccountId: "azure-123",
type: "oauth",
access_token: "at-123",
scope: "User.Read",
},
trigger: undefined,
session: undefined,
} as any);
expect(mockCredentialRepoCreate).not.toHaveBeenCalled();
});
it("offline_access is excluded from scope check", async () => {
mockPrismaUserFindFirst.mockResolvedValue({
id: 1,
email: "user@example.com",
name: "Test",
username: "test",
role: "USER",
locale: "en",
avatarUrl: null,
identityProvider: "AZUREAD",
identityProviderId: "azure-123",
teams: [],
movedToProfileId: null,
});
mockCredentialRepoFindFirst.mockResolvedValue(null);
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ value: [] }),
}) as unknown as typeof fetch;
// Scope without offline_access — should still pass scope check
await jwtCallback({
token: baseToken,
user: baseUser,
account: {
provider: "azure-ad",
providerAccountId: "azure-123",
type: "oauth",
access_token: "at-123",
scope: "User.Read Calendars.Read Calendars.ReadWrite",
},
trigger: undefined,
session: undefined,
} as any);
expect(mockCredentialRepoCreate).toHaveBeenCalled();
});
it("fetches default calendar and creates selectedCalendar", async () => {
mockPrismaUserFindFirst.mockResolvedValue({
id: 1,
email: "user@example.com",
name: "Test",
username: "test",
role: "USER",
locale: "en",
avatarUrl: null,
identityProvider: "AZUREAD",
identityProviderId: "azure-123",
teams: [],
movedToProfileId: null,
});
mockCredentialRepoFindFirst.mockResolvedValue(null);
mockCredentialRepoCreate.mockResolvedValue({ id: 77 });
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () =>
Promise.resolve({
value: [
{ id: "non-default-cal", isDefaultCalendar: false },
{ id: "default-cal-id", isDefaultCalendar: true },
],
}),
}) as unknown as typeof fetch;
await jwtCallback({
token: baseToken,
user: baseUser,
account: {
provider: "azure-ad",
providerAccountId: "azure-123",
type: "oauth",
access_token: "at-123",
scope: "User.Read Calendars.Read Calendars.ReadWrite",
},
trigger: undefined,
session: undefined,
} as any);
expect(mockPrismaSelectedCalendarCreate).toHaveBeenCalledWith({
data: expect.objectContaining({
integration: "office365_calendar",
externalId: "default-cal-id",
credentialId: 77,
}),
});
});
it("Graph API fetch failure logs error but does not crash", async () => {
mockPrismaUserFindFirst.mockResolvedValue({
id: 1,
email: "user@example.com",
name: "Test",
username: "test",
role: "USER",
locale: "en",
avatarUrl: null,
identityProvider: "AZUREAD",
identityProviderId: "azure-123",
teams: [],
movedToProfileId: null,
});
mockCredentialRepoFindFirst.mockResolvedValue(null);
globalThis.fetch = vi.fn().mockRejectedValue(new Error("Network error")) as unknown as typeof fetch;
// Should not throw
const result = await jwtCallback({
token: baseToken,
user: baseUser,
account: {
provider: "azure-ad",
providerAccountId: "azure-123",
type: "oauth",
access_token: "at-123",
scope: "User.Read Calendars.Read Calendars.ReadWrite",
},
trigger: undefined,
session: undefined,
} as any);
expect(result).toBeDefined();
});
it("calls updateProfilePhotoMicrosoft after calendar install", async () => {
mockPrismaUserFindFirst.mockResolvedValue({
id: 1,
email: "user@example.com",
name: "Test",
username: "test",
role: "USER",
locale: "en",
avatarUrl: null,
identityProvider: "AZUREAD",
identityProviderId: "azure-123",
teams: [],
movedToProfileId: null,
});
mockCredentialRepoFindFirst.mockResolvedValue(null);
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ value: [] }),
}) as unknown as typeof fetch;
await jwtCallback({
token: baseToken,
user: baseUser,
account: {
provider: "azure-ad",
providerAccountId: "azure-123",
type: "oauth",
access_token: "photo-token",
scope: "User.Read Calendars.Read Calendars.ReadWrite",
},
trigger: undefined,
session: undefined,
} as any);
expect(mockUpdateProfilePhotoMicrosoft).toHaveBeenCalledWith("photo-token", 1);
});
it("calls updateProfilePhotoMicrosoft even when calendar already installed (else-if branch)", async () => {
mockPrismaUserFindFirst.mockResolvedValue({
id: 1,
email: "user@example.com",
name: "Test",
username: "test",
role: "USER",
locale: "en",
avatarUrl: null,
identityProvider: "AZUREAD",
identityProviderId: "azure-123",
teams: [],
movedToProfileId: null,
});
// Calendar credential already exists
mockCredentialRepoFindFirst.mockResolvedValue({ id: 99 });
await jwtCallback({
token: baseToken,
user: baseUser,
account: {
provider: "azure-ad",
providerAccountId: "azure-123",
type: "oauth",
access_token: "photo-token-2",
scope: "User.Read Calendars.Read Calendars.ReadWrite",
},
trigger: undefined,
session: undefined,
} as any);
expect(mockUpdateProfilePhotoMicrosoft).toHaveBeenCalledWith("photo-token-2", 1);
});
it("converts expires_at from seconds to milliseconds in credential key", async () => {
mockPrismaUserFindFirst.mockResolvedValue({
id: 1,
email: "user@example.com",
name: "Test",
username: "test",
role: "USER",
locale: "en",
avatarUrl: null,
identityProvider: "AZUREAD",
identityProviderId: "azure-123",
teams: [],
movedToProfileId: null,
});
mockCredentialRepoFindFirst.mockResolvedValue(null);
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ value: [] }),
}) as unknown as typeof fetch;
const expiresAtSeconds = 1700000000;
await jwtCallback({
token: baseToken,
user: baseUser,
account: {
provider: "azure-ad",
providerAccountId: "azure-123",
type: "oauth",
access_token: "at-123",
refresh_token: "rt-123",
expires_at: expiresAtSeconds,
scope: "User.Read Calendars.Read Calendars.ReadWrite",
},
trigger: undefined,
session: undefined,
} as any);
expect(mockBuildCredentialCreateData).toHaveBeenCalledWith(
expect.objectContaining({
key: expect.objectContaining({
expiry_date: expiresAtSeconds * 1000,
}),
})
);
});
});
});