diff --git a/apps/web/modules/auth/lib/authOptions.test.ts b/apps/web/modules/auth/lib/authOptions.test.ts index 8010fb8585..7fbae63277 100644 --- a/apps/web/modules/auth/lib/authOptions.test.ts +++ b/apps/web/modules/auth/lib/authOptions.test.ts @@ -8,6 +8,18 @@ import { authOptions } from "./authOptions"; import { mockUser } from "./mock-data"; import { hashPassword } from "./utils"; +// Mock next/headers +vi.mock("next/headers", () => ({ + cookies: () => ({ + get: (name: string) => { + if (name === "next-auth.callback-url") { + return { value: "/" }; + } + return null; + }, + }), +})); + const mockUserId = "cm5yzxcp900000cl78fzocjal"; const mockPassword = randomBytes(12).toString("hex"); const mockHashedPassword = await hashPassword(mockPassword); diff --git a/apps/web/modules/ee/license-check/lib/utils.test.ts b/apps/web/modules/ee/license-check/lib/utils.test.ts new file mode 100644 index 0000000000..9d7456ee1c --- /dev/null +++ b/apps/web/modules/ee/license-check/lib/utils.test.ts @@ -0,0 +1,140 @@ +import { Organization } from "@prisma/client"; +import fetch from "node-fetch"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { + getBiggerUploadFileSizePermission, + getIsContactsEnabled, + getIsMultiOrgEnabled, + getIsSamlSsoEnabled, + getIsSpamProtectionEnabled, + getIsTwoFactorAuthEnabled, + getLicenseFeatures, + getMultiLanguagePermission, + getOrganizationProjectsLimit, + getRemoveBrandingPermission, + getRoleManagementPermission, + getWhiteLabelPermission, + getisSsoEnabled, +} from "./utils"; + +// Mock declarations must be at the top level +vi.mock("@/lib/env", () => ({ + env: { + ENTERPRISE_LICENSE_KEY: "test-license-key", + }, +})); + +vi.mock("@/lib/constants", () => ({ + E2E_TESTING: false, + ENTERPRISE_LICENSE_KEY: "test-license-key", + IS_FORMBRICKS_CLOUD: false, + IS_RECAPTCHA_CONFIGURED: true, + PROJECT_FEATURE_KEYS: { + removeBranding: "remove-branding", + whiteLabel: "white-label", + roleManagement: "role-management", + biggerUploadFileSize: "bigger-upload-file-size", + multiLanguage: "multi-language", + }, +})); + +vi.mock("@/lib/cache", () => ({ + cache: vi.fn((fn) => fn), + revalidateTag: vi.fn(), +})); + +vi.mock("next/server", () => ({ + after: vi.fn(), +})); + +vi.mock("node-fetch", () => ({ + default: vi.fn(), +})); + +describe("License Check Utils", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("Feature Permissions", () => { + const mockOrganization = { + billing: { + plan: "enterprise" as Organization["billing"]["plan"], + limits: { + projects: 3, + }, + }, + } as Organization; + + test("getRemoveBrandingPermission", async () => { + const result = await getRemoveBrandingPermission(mockOrganization.billing.plan); + expect(result).toBe(false); // Default value when no license is active + }); + + test("getWhiteLabelPermission", async () => { + const result = await getWhiteLabelPermission(mockOrganization.billing.plan); + expect(result).toBe(false); // Default value when no license is active + }); + + test("getRoleManagementPermission", async () => { + const result = await getRoleManagementPermission(mockOrganization.billing.plan); + expect(result).toBe(false); // Default value when no license is active + }); + + test("getBiggerUploadFileSizePermission", async () => { + const result = await getBiggerUploadFileSizePermission(mockOrganization.billing.plan); + expect(result).toBe(false); // Default value when no license is active + }); + + test("getMultiLanguagePermission", async () => { + const result = await getMultiLanguagePermission(mockOrganization.billing.plan); + expect(result).toBe(false); // Default value when no license is active + }); + + test("getIsMultiOrgEnabled", async () => { + const result = await getIsMultiOrgEnabled(); + expect(typeof result).toBe("boolean"); + }); + + test("getIsContactsEnabled", async () => { + const result = await getIsContactsEnabled(); + expect(typeof result).toBe("boolean"); + }); + + test("getIsTwoFactorAuthEnabled", async () => { + const result = await getIsTwoFactorAuthEnabled(); + expect(typeof result).toBe("boolean"); + }); + + test("getisSsoEnabled", async () => { + const result = await getisSsoEnabled(); + expect(typeof result).toBe("boolean"); + }); + + test("getIsSamlSsoEnabled", async () => { + const result = await getIsSamlSsoEnabled(); + expect(typeof result).toBe("boolean"); + }); + + test("getIsSpamProtectionEnabled", async () => { + const result = await getIsSpamProtectionEnabled(mockOrganization.billing.plan); + expect(result).toBe(false); // Default value when no license is active + }); + + test("getOrganizationProjectsLimit", async () => { + const result = await getOrganizationProjectsLimit(mockOrganization.billing.limits); + expect(result).toBe(3); // Default value from mock organization + }); + }); + + describe("License Features", () => { + test("getLicenseFeatures returns null when no license is active", async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: false, + } as any); + + const result = await getLicenseFeatures(); + expect(result).toBeNull(); + }); + }); +}); diff --git a/apps/web/modules/ee/sso/lib/providers.test.ts b/apps/web/modules/ee/sso/lib/providers.test.ts new file mode 100644 index 0000000000..eee8a57a45 --- /dev/null +++ b/apps/web/modules/ee/sso/lib/providers.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, test, vi } from "vitest"; +import { getSSOProviders } from "./providers"; + +// Mock environment variables +vi.mock("@/lib/constants", () => ({ + GITHUB_ID: "test-github-id", + GITHUB_SECRET: "test-github-secret", + GOOGLE_CLIENT_ID: "test-google-client-id", + GOOGLE_CLIENT_SECRET: "test-google-client-secret", + AZUREAD_CLIENT_ID: "test-azure-client-id", + AZUREAD_CLIENT_SECRET: "test-azure-client-secret", + AZUREAD_TENANT_ID: "test-azure-tenant-id", + OIDC_CLIENT_ID: "test-oidc-client-id", + OIDC_CLIENT_SECRET: "test-oidc-client-secret", + OIDC_DISPLAY_NAME: "Test OIDC", + OIDC_ISSUER: "https://test-issuer.com", + OIDC_SIGNING_ALGORITHM: "RS256", + WEBAPP_URL: "https://test-app.com", +})); + +describe("SSO Providers", () => { + test("should return all configured providers", () => { + const providers = getSSOProviders(); + expect(providers).toHaveLength(5); // GitHub, Google, Azure AD, OIDC, and SAML + }); + + test("should configure OIDC provider correctly", () => { + const providers = getSSOProviders(); + const oidcProvider = providers[3]; + + expect(oidcProvider.id).toBe("openid"); + expect(oidcProvider.name).toBe("Test OIDC"); + expect((oidcProvider as any).clientId).toBe("test-oidc-client-id"); + expect((oidcProvider as any).clientSecret).toBe("test-oidc-client-secret"); + expect((oidcProvider as any).wellKnown).toBe("https://test-issuer.com/.well-known/openid-configuration"); + expect((oidcProvider as any).client?.id_token_signed_response_alg).toBe("RS256"); + expect(oidcProvider.checks).toContain("pkce"); + expect(oidcProvider.checks).toContain("state"); + }); + + test("should configure SAML provider correctly", () => { + const providers = getSSOProviders(); + const samlProvider = providers[4]; + + expect(samlProvider.id).toBe("saml"); + expect(samlProvider.name).toBe("BoxyHQ SAML"); + expect((samlProvider as any).version).toBe("2.0"); + expect(samlProvider.checks).toContain("pkce"); + expect(samlProvider.checks).toContain("state"); + expect((samlProvider as any).authorization?.url).toBe("https://test-app.com/api/auth/saml/authorize"); + expect(samlProvider.token).toBe("https://test-app.com/api/auth/saml/token"); + expect(samlProvider.userinfo).toBe("https://test-app.com/api/auth/saml/userinfo"); + expect(samlProvider.allowDangerousEmailAccountLinking).toBe(true); + }); +}); diff --git a/apps/web/modules/ui/lib/utils.test.tsx b/apps/web/modules/ui/lib/utils.test.tsx new file mode 100644 index 0000000000..7269fdca00 --- /dev/null +++ b/apps/web/modules/ui/lib/utils.test.tsx @@ -0,0 +1,64 @@ +import { describe, expect, test } from "vitest"; +import { cn } from "./utils"; + +describe("cn (class names utility)", () => { + test("should merge class names correctly", () => { + expect(cn("class1", "class2")).toBe("class1 class2"); + }); + + test("should handle conditional classes", () => { + const isActive = true; + const isDisabled = false; + expect(cn("base", isActive && "active", isDisabled && "disabled")).toBe("base active"); + }); + + test("should handle array of classes", () => { + expect(cn(["class1", "class2"], "class3")).toBe("class1 class2 class3"); + }); + + test("should handle objects of classes", () => { + expect(cn({ active: true, disabled: false })).toBe("active"); + }); + + test("should merge Tailwind classes intelligently", () => { + expect(cn("px-2 py-1", "p-4")).toBe("p-4"); + expect(cn("text-red-500", "text-blue-500")).toBe("text-blue-500"); + }); + + test("should handle undefined and null values", () => { + expect(cn("base", undefined, null, "active")).toBe("base active"); + }); + + test("should handle complex combinations", () => { + const isError = true; + const isLarge = true; + expect( + cn( + "base-class", + { + "text-red-500": isError, + "text-green-500": !isError, + "text-lg": isLarge, + }, + ["p-2", "m-1"], + isError && "border-red-500" + ) + ).toBe("base-class text-red-500 text-lg p-2 m-1 border-red-500"); + }); + + test("should handle Tailwind responsive classes", () => { + const result = cn("sm:p-2 md:p-4", "p-6"); + expect(result).toContain("p-6"); + expect(result).toContain("sm:p-2"); + expect(result).toContain("md:p-4"); + }); + + test("should handle Tailwind state classes", () => { + expect(cn("hover:bg-blue-500", "hover:bg-red-500")).toBe("hover:bg-red-500"); + }); + + test("should handle important classes", () => { + const result = cn("!p-4", "p-2"); + expect(result).toContain("!p-4"); + }); +}); diff --git a/apps/web/vite.config.mts b/apps/web/vite.config.mts index c55aebc052..434cdef0a4 100644 --- a/apps/web/vite.config.mts +++ b/apps/web/vite.config.mts @@ -64,6 +64,7 @@ export default defineConfig({ "modules/ui/components/icons/**", "app/share/**", "lib/shortUrl/**", + "app/[shortUrlId]", "modules/ee/contacts/[contactId]/**", "modules/ee/contacts/components/**", "modules/ee/two-factor-auth/**",