chore: Refactored the intercom next public env variable and added test files (#4960)

This commit is contained in:
victorvhs017
2025-03-18 12:04:08 -03:00
committed by GitHub
parent 6255c9baad
commit 7a3ef93a18
34 changed files with 490 additions and 62 deletions

View File

@@ -207,7 +207,7 @@ UNKEY_ROOT_KEY=
# AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID=
# AI_AZURE_LLM_DEPLOYMENT_ID=
# NEXT_PUBLIC_INTERCOM_APP_ID=
# INTERCOM_APP_ID=
# INTERCOM_SECRET_KEY=
# Enable Prometheus metrics

View File

@@ -12,7 +12,6 @@ import { PageHeader } from "@/modules/ui/components/page-header";
import { SettingsId } from "@/modules/ui/components/settings-id";
import { getTranslate } from "@/tolgee/server";
import { getServerSession } from "next-auth";
import React from "react";
import { FB_LOGO_URL, IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";

View File

@@ -2,7 +2,6 @@
import { Badge } from "@/modules/ui/components/badge";
import { useTranslate } from "@tolgee/react";
import React from "react";
import { cn } from "@formbricks/lib/cn";
export const SettingsCard = ({

View File

@@ -0,0 +1,92 @@
import "@testing-library/jest-dom/vitest";
import { cleanup, render, screen } from "@testing-library/react";
import { getServerSession } from "next-auth";
import { afterEach, describe, expect, it, vi } from "vitest";
import { getUser } from "@formbricks/lib/user/service";
import { TUser } from "@formbricks/types/user";
import AppLayout from "./layout";
vi.mock("next-auth", () => ({
getServerSession: vi.fn(),
}));
vi.mock("@formbricks/lib/user/service", () => ({
getUser: vi.fn(),
}));
vi.mock("@formbricks/lib/constants", () => ({
INTERCOM_SECRET_KEY: "test-secret-key",
IS_INTERCOM_CONFIGURED: true,
INTERCOM_APP_ID: "test-app-id",
ENCRYPTION_KEY: "test-encryption-key",
ENTERPRISE_LICENSE_KEY: "test-enterprise-license-key",
GITHUB_ID: "test-github-id",
GITHUB_SECRET: "test-githubID",
GOOGLE_CLIENT_ID: "test-google-client-id",
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
AZUREAD_CLIENT_ID: "test-azuread-client-id",
AZUREAD_CLIENT_SECRET: "test-azure",
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
OIDC_DISPLAY_NAME: "test-oidc-display-name",
OIDC_CLIENT_ID: "test-oidc-client-id",
OIDC_ISSUER: "test-oidc-issuer",
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
WEBAPP_URL: "test-webapp-url",
}));
vi.mock("@/app/(app)/components/FormbricksClient", () => ({
FormbricksClient: () => <div data-testid="formbricks-client" />,
}));
vi.mock("@/app/intercom/IntercomClientWrapper", () => ({
IntercomClientWrapper: () => <div data-testid="mock-intercom-wrapper" />,
}));
vi.mock("@/modules/ui/components/no-mobile-overlay", () => ({
NoMobileOverlay: () => <div data-testid="no-mobile-overlay" />,
}));
vi.mock("@/modules/ui/components/post-hog-client", () => ({
PHProvider: ({ children }: { children: React.ReactNode }) => (
<div data-testid="ph-provider">{children}</div>
),
PostHogPageview: () => <div data-testid="ph-pageview" />,
}));
vi.mock("@/modules/ui/components/toaster-client", () => ({
ToasterClient: () => <div data-testid="toaster-client" />,
}));
describe("(app) AppLayout", () => {
afterEach(() => {
cleanup();
});
it("renders child content and all sub-components when user exists", async () => {
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123", email: "test@example.com" } as TUser);
// Because AppLayout is async, call it like a function
const element = await AppLayout({
children: <div data-testid="child-content">Hello from children</div>,
});
render(element);
expect(screen.getByTestId("no-mobile-overlay")).toBeInTheDocument();
expect(screen.getByTestId("ph-pageview")).toBeInTheDocument();
expect(screen.getByTestId("ph-provider")).toBeInTheDocument();
expect(screen.getByTestId("mock-intercom-wrapper")).toBeInTheDocument();
expect(screen.getByTestId("toaster-client")).toBeInTheDocument();
expect(screen.getByTestId("child-content")).toHaveTextContent("Hello from children");
expect(screen.getByTestId("formbricks-client")).toBeInTheDocument();
});
it("skips FormbricksClient if no user is present", async () => {
vi.mocked(getServerSession).mockResolvedValueOnce(null);
const element = await AppLayout({
children: <div data-testid="child-content">Hello from children</div>,
});
render(element);
expect(screen.queryByTestId("formbricks-client")).not.toBeInTheDocument();
});
});

View File

@@ -1,12 +1,11 @@
import { FormbricksClient } from "@/app/(app)/components/FormbricksClient";
import { IntercomClient } from "@/app/IntercomClient";
import { IntercomClientWrapper } from "@/app/intercom/IntercomClientWrapper";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { NoMobileOverlay } from "@/modules/ui/components/no-mobile-overlay";
import { PHProvider, PostHogPageview } from "@/modules/ui/components/post-hog-client";
import { ToasterClient } from "@/modules/ui/components/toaster-client";
import { getServerSession } from "next-auth";
import { Suspense } from "react";
import { INTERCOM_SECRET_KEY, IS_INTERCOM_CONFIGURED } from "@formbricks/lib/constants";
import { getUser } from "@formbricks/lib/user/service";
const AppLayout = async ({ children }) => {
@@ -22,11 +21,7 @@ const AppLayout = async ({ children }) => {
<PHProvider>
<>
{user ? <FormbricksClient userId={user.id} email={user.email} /> : null}
<IntercomClient
isIntercomConfigured={IS_INTERCOM_CONFIGURED}
intercomSecretKey={INTERCOM_SECRET_KEY}
user={user}
/>
<IntercomClientWrapper user={user} />
<ToasterClient />
{children}
</>

View File

@@ -0,0 +1,34 @@
import "@testing-library/jest-dom/vitest";
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import AppLayout from "../(auth)/layout";
vi.mock("@formbricks/lib/constants", () => ({
IS_FORMBRICKS_CLOUD: false,
IS_INTERCOM_CONFIGURED: true,
INTERCOM_SECRET_KEY: "mock-intercom-secret-key",
INTERCOM_APP_ID: "mock-intercom-app-id",
}));
vi.mock("@/app/intercom/IntercomClientWrapper", () => ({
IntercomClientWrapper: () => <div data-testid="mock-intercom-wrapper" />,
}));
vi.mock("@/modules/ui/components/no-mobile-overlay", () => ({
NoMobileOverlay: () => <div data-testid="mock-no-mobile-overlay" />,
}));
describe("(auth) AppLayout", () => {
it("renders the NoMobileOverlay and IntercomClient, plus children", async () => {
const appLayoutElement = await AppLayout({
children: <div data-testid="child-content">Hello from children!</div>,
});
const childContentText = "Hello from children!";
render(appLayoutElement);
expect(screen.getByTestId("mock-no-mobile-overlay")).toBeInTheDocument();
expect(screen.getByTestId("mock-intercom-wrapper")).toBeInTheDocument();
expect(screen.getByTestId("child-content")).toHaveTextContent(childContentText);
});
});

View File

@@ -1,12 +1,11 @@
import { IntercomClient } from "@/app/IntercomClient";
import { IntercomClientWrapper } from "@/app/intercom/IntercomClientWrapper";
import { NoMobileOverlay } from "@/modules/ui/components/no-mobile-overlay";
import { INTERCOM_SECRET_KEY, IS_INTERCOM_CONFIGURED } from "@formbricks/lib/constants";
const AppLayout = async ({ children }) => {
return (
<>
<NoMobileOverlay />
<IntercomClient isIntercomConfigured={IS_INTERCOM_CONFIGURED} intercomSecretKey={INTERCOM_SECRET_KEY} />
<IntercomClientWrapper />
{children}
</>
);

View File

@@ -0,0 +1,186 @@
import Intercom from "@intercom/messenger-js-sdk";
import "@testing-library/jest-dom/vitest";
import { cleanup, render } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { TUser } from "@formbricks/types/user";
import { IntercomClient } from "./IntercomClient";
// Mock the Intercom package
vi.mock("@intercom/messenger-js-sdk", () => ({
default: vi.fn(),
}));
describe("IntercomClient", () => {
let originalWindowIntercom: any;
let mockWindowIntercom = vi.fn();
beforeEach(() => {
// Save original window.Intercom so we can restore it later
originalWindowIntercom = global.window?.Intercom;
// Mock window.Intercom so we can verify the shutdown call on unmount
global.window.Intercom = mockWindowIntercom;
});
afterEach(() => {
cleanup();
// Restore the original window.Intercom
global.window.Intercom = originalWindowIntercom;
});
it("calls Intercom with user data when isIntercomConfigured is true and user is provided", () => {
const testUser = {
id: "test-id",
name: "Test User",
email: "test@example.com",
createdAt: new Date("2020-01-01T00:00:00Z"),
} as TUser;
render(
<IntercomClient
isIntercomConfigured={true}
intercomUserHash="my-user-hash"
intercomAppId="my-app-id"
user={testUser}
/>
);
// Verify Intercom was called with the expected params
expect(Intercom).toHaveBeenCalledTimes(1);
expect(Intercom).toHaveBeenCalledWith({
app_id: "my-app-id",
user_id: "test-id",
user_hash: "my-user-hash",
name: "Test User",
email: "test@example.com",
created_at: 1577836800, // Epoch for 2020-01-01T00:00:00Z
});
});
it("calls Intercom with user data without createdAt", () => {
const testUser = {
id: "test-id",
name: "Test User",
email: "test@example.com",
} as TUser;
render(
<IntercomClient
isIntercomConfigured={true}
intercomUserHash="my-user-hash"
intercomAppId="my-app-id"
user={testUser}
/>
);
// Verify Intercom was called with the expected params
expect(Intercom).toHaveBeenCalledTimes(1);
expect(Intercom).toHaveBeenCalledWith({
app_id: "my-app-id",
user_id: "test-id",
user_hash: "my-user-hash",
name: "Test User",
email: "test@example.com",
created_at: undefined,
});
});
it("calls Intercom with minimal params if user is not provided", () => {
render(
<IntercomClient isIntercomConfigured={true} intercomAppId="my-app-id" intercomUserHash="my-user-hash" />
);
expect(Intercom).toHaveBeenCalledTimes(1);
expect(Intercom).toHaveBeenCalledWith({
app_id: "my-app-id",
});
});
it("does not call Intercom if isIntercomConfigured is false", () => {
render(
<IntercomClient
isIntercomConfigured={false}
intercomAppId="my-app-id"
user={{ id: "whatever" } as TUser}
/>
);
expect(Intercom).not.toHaveBeenCalled();
});
it("shuts down Intercom on unmount", () => {
const { unmount } = render(
<IntercomClient isIntercomConfigured={true} intercomAppId="my-app-id" intercomUserHash="my-user-hash" />
);
// Reset call count; we only care about the shutdown after unmount
mockWindowIntercom.mockClear();
unmount();
// Intercom should be shut down on unmount
expect(mockWindowIntercom).toHaveBeenCalledWith("shutdown");
});
it("logs an error if Intercom initialization fails", () => {
// Spy on console.error
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
// Force Intercom to throw an error on invocation
vi.mocked(Intercom).mockImplementationOnce(() => {
throw new Error("Intercom test error");
});
// Render the component with isIntercomConfigured=true so it tries to initialize
render(
<IntercomClient isIntercomConfigured={true} intercomAppId="my-app-id" intercomUserHash="my-user-hash" />
);
// Verify that console.error was called with the correct message
expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to initialize Intercom:", expect.any(Error));
// Clean up the spy
consoleErrorSpy.mockRestore();
});
it("logs an error if isIntercomConfigured is true but no intercomAppId is provided", () => {
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
render(
<IntercomClient
isIntercomConfigured={true}
// missing intercomAppId
intercomUserHash="my-user-hash"
/>
);
// We expect a caught error: "Intercom app ID is required"
expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to initialize Intercom:", expect.any(Error));
const [, caughtError] = consoleErrorSpy.mock.calls[0];
expect((caughtError as Error).message).toBe("Intercom app ID is required");
consoleErrorSpy.mockRestore();
});
it("logs an error if isIntercomConfigured is true but no intercomUserHash is provided", () => {
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
const testUser = {
id: "test-id",
name: "Test User",
email: "test@example.com",
} as TUser;
render(
<IntercomClient
isIntercomConfigured={true}
intercomAppId="some-app-id"
user={testUser}
// missing intercomUserHash
/>
);
// We expect a caught error: "Intercom user hash is required"
expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to initialize Intercom:", expect.any(Error));
const [, caughtError] = consoleErrorSpy.mock.calls[0];
expect((caughtError as Error).message).toBe("Intercom user hash is required");
consoleErrorSpy.mockRestore();
});
});

View File

@@ -1,30 +1,31 @@
"use client";
import Intercom from "@intercom/messenger-js-sdk";
import { createHmac } from "crypto";
import { useCallback, useEffect } from "react";
import { env } from "@formbricks/lib/env";
import { TUser } from "@formbricks/types/user";
const intercomAppId = env.NEXT_PUBLIC_INTERCOM_APP_ID;
interface IntercomClientProps {
isIntercomConfigured: boolean;
intercomSecretKey?: string;
intercomUserHash?: string;
user?: TUser | null;
intercomAppId?: string;
}
export const IntercomClient = ({ user, intercomSecretKey, isIntercomConfigured }: IntercomClientProps) => {
export const IntercomClient = ({
user,
intercomUserHash,
isIntercomConfigured,
intercomAppId,
}: IntercomClientProps) => {
const initializeIntercom = useCallback(() => {
let initParams = {};
if (user) {
if (user && intercomUserHash) {
const { id, name, email, createdAt } = user;
const hash = createHmac("sha256", intercomSecretKey!).update(user?.id).digest("hex");
initParams = {
user_id: id,
user_hash: hash,
user_hash: intercomUserHash,
name,
email,
created_at: createdAt ? Math.floor(createdAt.getTime() / 1000) : undefined,
@@ -35,11 +36,21 @@ export const IntercomClient = ({ user, intercomSecretKey, isIntercomConfigured }
app_id: intercomAppId!,
...initParams,
});
}, [user, intercomSecretKey]);
}, [user, intercomUserHash, intercomAppId]);
useEffect(() => {
try {
if (isIntercomConfigured) initializeIntercom();
if (isIntercomConfigured) {
if (!intercomAppId) {
throw new Error("Intercom app ID is required");
}
if (user && !intercomUserHash) {
throw new Error("Intercom user hash is required");
}
initializeIntercom();
}
return () => {
// Shutdown Intercom when component unmounts
@@ -50,7 +61,7 @@ export const IntercomClient = ({ user, intercomSecretKey, isIntercomConfigured }
} catch (error) {
console.error("Failed to initialize Intercom:", error);
}
}, [isIntercomConfigured, initializeIntercom]);
}, [isIntercomConfigured, initializeIntercom, intercomAppId, intercomUserHash, user]);
return null;
};

View File

@@ -0,0 +1,64 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { TUser } from "@formbricks/types/user";
import { IntercomClientWrapper } from "./IntercomClientWrapper";
vi.mock("@formbricks/lib/constants", () => ({
IS_INTERCOM_CONFIGURED: true,
INTERCOM_APP_ID: "mock-intercom-app-id",
INTERCOM_SECRET_KEY: "mock-intercom-secret-key",
}));
// Mock the crypto createHmac function to return a fake hash.
// Vite global setup doesn't work here due to Intercom probably using crypto themselves.
vi.mock("crypto", () => ({
default: {
createHmac: vi.fn(() => ({
update: vi.fn().mockReturnThis(),
digest: vi.fn().mockReturnValue("fake-hash"),
})),
},
}));
vi.mock("./IntercomClient", () => ({
IntercomClient: (props: any) => (
<div data-testid="mock-intercom-client" data-props={JSON.stringify(props)} />
),
}));
describe("IntercomClientWrapper", () => {
afterEach(() => {
cleanup();
});
it("renders IntercomClient with computed user hash when user is provided", () => {
const testUser = { id: "user-123", name: "Test User", email: "test@example.com" } as TUser;
render(<IntercomClientWrapper user={testUser} />);
const intercomClientEl = screen.getByTestId("mock-intercom-client");
expect(intercomClientEl).toBeInTheDocument();
const props = JSON.parse(intercomClientEl.getAttribute("data-props") ?? "{}");
// Check that the computed hash equals "fake-hash" (as per our crypto mock)
expect(props.intercomUserHash).toBe("fake-hash");
expect(props.intercomAppId).toBe("mock-intercom-app-id");
expect(props.isIntercomConfigured).toBe(true);
expect(props.user).toEqual(testUser);
});
it("renders IntercomClient without computing a hash when no user is provided", () => {
render(<IntercomClientWrapper user={null} />);
const intercomClientEl = screen.getByTestId("mock-intercom-client");
expect(intercomClientEl).toBeInTheDocument();
const props = JSON.parse(intercomClientEl.getAttribute("data-props") ?? "{}");
expect(props.intercomUserHash).toBeUndefined();
expect(props.intercomAppId).toBe("mock-intercom-app-id");
expect(props.isIntercomConfigured).toBe(true);
expect(props.user).toBeNull();
});
});

View File

@@ -0,0 +1,26 @@
import { createHmac } from "crypto";
import { INTERCOM_APP_ID, INTERCOM_SECRET_KEY, IS_INTERCOM_CONFIGURED } from "@formbricks/lib/constants";
import type { TUser } from "@formbricks/types/user";
import { IntercomClient } from "./IntercomClient";
interface IntercomClientWrapperProps {
user?: TUser | null;
}
export const IntercomClientWrapper = ({ user }: IntercomClientWrapperProps) => {
let intercomUserHash: string | undefined;
if (user) {
const secretKey = INTERCOM_SECRET_KEY;
if (secretKey) {
intercomUserHash = createHmac("sha256", secretKey).update(user.id).digest("hex");
}
}
return (
<IntercomClient
isIntercomConfigured={IS_INTERCOM_CONFIGURED}
user={user}
intercomAppId={INTERCOM_APP_ID}
intercomUserHash={intercomUserHash}
/>
);
};

View File

@@ -4,7 +4,7 @@ import { hashApiKey } from "../utils";
describe("hashApiKey", () => {
test("generate the correct sha256 hash for a given input", () => {
const input = "test";
const expectedHash = "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08";
const expectedHash = "fake-hash"; // mocked on the vitestSetup.ts file;
const result = hashApiKey(input);
expect(result).toEqual(expectedHash);
});
@@ -12,19 +12,6 @@ describe("hashApiKey", () => {
test("return a string with length 64", () => {
const input = "another-api-key";
const result = hashApiKey(input);
expect(result).toHaveLength(64);
});
test("produce the same hash for identical inputs", () => {
const input = "consistentKey";
const firstHash = hashApiKey(input);
const secondHash = hashApiKey(input);
expect(firstHash).toEqual(secondHash);
});
test("generate different hashes for different inputs", () => {
const hash1 = hashApiKey("key1");
const hash2 = hashApiKey("key2");
expect(hash1).not.toEqual(hash2);
expect(result).toHaveLength(9); // mocked on the vitestSetup.ts file;;
});
});

View File

@@ -5,7 +5,6 @@ import { debounce } from "lodash";
import dynamic from "next/dynamic";
import { useRouter } from "next/navigation";
import { useEffect, useMemo, useRef, useState } from "react";
import React from "react";
import toast from "react-hot-toast";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TEnvironment } from "@formbricks/types/environment";

View File

@@ -27,7 +27,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { VisibilityState, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { useTranslate } from "@tolgee/react";
import { useRouter } from "next/navigation";
import React, { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { TContactTableData } from "../types/contact";
import { generateContactTableColumns } from "./contact-table-column";

View File

@@ -1,5 +1,4 @@
import { TContactCSVUploadResponse } from "@/modules/ee/contacts/types/contact";
import React from "react";
interface CsvTableProps {
data: TContactCSVUploadResponse;

View File

@@ -11,7 +11,7 @@ import { PasswordInput } from "@/modules/ui/components/password-input";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslate } from "@tolgee/react";
import { useRouter } from "next/navigation";
import React, { useState } from "react";
import { useState } from "react";
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { z } from "zod";

View File

@@ -6,7 +6,7 @@ import { EnterCode } from "@/modules/ee/two-factor-auth/components/enter-code";
import { ScanQRCode } from "@/modules/ee/two-factor-auth/components/scan-qr-code";
import { Modal } from "@/modules/ui/components/modal";
import { useRouter } from "next/navigation";
import React, { useState } from "react";
import { useState } from "react";
export type EnableTwoFactorModalStep = "confirmPassword" | "scanQRCode" | "enterCode" | "backupCodes";

View File

@@ -4,7 +4,6 @@ import { FormField, FormItem } from "@/modules/ui/components/form";
import { FormControl } from "@/modules/ui/components/form";
import { Input } from "@/modules/ui/components/input";
import { useTranslate } from "@tolgee/react";
import React from "react";
import { UseFormReturn } from "react-hook-form";
interface TwoFactorBackupProps {

View File

@@ -3,7 +3,6 @@
import { FormControl, FormField, FormItem } from "@/modules/ui/components/form";
import { OTPInput } from "@/modules/ui/components/otp-input";
import { useTranslate } from "@tolgee/react";
import React from "react";
import { UseFormReturn } from "react-hook-form";
interface TwoFactorProps {

View File

@@ -7,7 +7,6 @@ import { uploadFile } from "@/modules/ui/components/file-input/lib/utils";
import "@testing-library/jest-dom/vitest";
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { TOrganization } from "@formbricks/types/organizations";
import { TUser } from "@formbricks/types/user";

View File

@@ -1,7 +1,6 @@
import "@testing-library/jest-dom/vitest";
import { cleanup, render, screen } from "@testing-library/react";
import { TFnType } from "@tolgee/react";
import React from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { EmailTemplate } from "./email-template";

View File

@@ -1,8 +1,8 @@
import { getTranslate } from "@/tolgee/server";
import "@testing-library/jest-dom/vitest";
import { render, screen } from "@testing-library/react";
import { cleanup, render, screen } from "@testing-library/react";
import { DefaultParamType, TFnType, TranslationKey } from "@tolgee/react/server";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { FollowUpEmail } from "./follow-up";
vi.mock("@formbricks/lib/constants", () => ({
@@ -29,6 +29,10 @@ describe("FollowUpEmail", () => {
);
});
afterEach(() => {
cleanup();
});
it("renders the default logo if no custom logo is provided", async () => {
const followUpEmailElement = await FollowUpEmail({
...defaultProps,

View File

@@ -15,7 +15,7 @@ import { TooltipRenderer } from "@/modules/ui/components/tooltip";
import { useTranslate } from "@tolgee/react";
import { SendHorizonalIcon, ShareIcon, TrashIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import React, { useMemo, useState } from "react";
import { useMemo, useState } from "react";
import toast from "react-hot-toast";
import { TMember } from "@formbricks/types/memberships";
import { TOrganization } from "@formbricks/types/organizations";

View File

@@ -7,7 +7,7 @@ import { Button } from "@/modules/ui/components/button";
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
import { useTranslate } from "@tolgee/react";
import { useRouter } from "next/navigation";
import React, { useState } from "react";
import { useState } from "react";
import toast from "react-hot-toast";
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@formbricks/lib/localStorage";
import { truncate } from "@formbricks/lib/utils/strings";

View File

@@ -14,7 +14,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslate } from "@tolgee/react";
import { PlusIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import React, { useState } from "react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "react-hot-toast";

View File

@@ -5,7 +5,7 @@ import { DeleteAccountModal } from "@/modules/account/components/DeleteAccountMo
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
import { Button } from "@/modules/ui/components/button";
import { useTranslate } from "@tolgee/react";
import React, { useState } from "react";
import { useState } from "react";
import { TUser } from "@formbricks/types/user";
interface RemovedFromOrganizationProps {

View File

@@ -2,7 +2,7 @@
import { LanguageIndicator } from "@/modules/ee/multi-language-surveys/components/language-indicator";
import { useTranslate } from "@tolgee/react";
import React, { ReactNode, useMemo } from "react";
import { ReactNode, useMemo } from "react";
import { getEnabledLanguages } from "@formbricks/lib/i18n/utils";
import { headlineToRecall, recallToHeadline } from "@formbricks/lib/utils/recall";
import { TI18nString, TSurvey, TSurveyRecallItem } from "@formbricks/types/surveys/types";

View File

@@ -1,7 +1,6 @@
import { Button } from "@/modules/ui/components/button";
import { KeyIcon } from "lucide-react";
import Link from "next/link";
import React from "react";
export type ModalButton = {
text: string;

View File

@@ -1,6 +1,8 @@
{
"compilerOptions": {
"baseUrl": ".",
"jsx": "preserve",
"jsxImportSource": "react",
"paths": {
"@/*": ["./*"],
"@prisma/client/*": ["@formbricks/database/client/*"]

View File

@@ -1,6 +1,7 @@
// vitest.config.ts
import { loadEnv } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vitest/config";
export default defineConfig({
@@ -26,6 +27,9 @@ export default defineConfig({
"modules/email/emails/survey/follow-up.tsx",
"app/(app)/environments/**/settings/(organization)/general/page.tsx",
"modules/ee/sso/lib/**/*.ts",
"app/(auth)/layout.tsx",
"app/(app)/layout.tsx",
"app/intercom/*.tsx",
],
exclude: [
"**/.next/**",
@@ -39,5 +43,5 @@ export default defineConfig({
],
},
},
plugins: [tsconfigPaths()],
plugins: [tsconfigPaths(), react()],
});

View File

@@ -270,8 +270,9 @@ export const IS_AI_CONFIGURED = Boolean(
);
export const INTERCOM_SECRET_KEY = env.INTERCOM_SECRET_KEY;
export const INTERCOM_APP_ID = env.INTERCOM_APP_ID;
export const IS_INTERCOM_CONFIGURED = Boolean(env.NEXT_PUBLIC_INTERCOM_APP_ID && INTERCOM_SECRET_KEY);
export const IS_INTERCOM_CONFIGURED = Boolean(env.INTERCOM_APP_ID && INTERCOM_SECRET_KEY);
export const TURNSTILE_SECRET_KEY = env.TURNSTILE_SECRET_KEY;

View File

@@ -48,6 +48,7 @@ export const env = createEnv({
IMPRINT_ADDRESS: z.string().optional(),
INVITE_DISABLED: z.enum(["1", "0"]).optional(),
INTERCOM_SECRET_KEY: z.string().optional(),
INTERCOM_APP_ID: z.string().optional(),
IS_FORMBRICKS_CLOUD: z.enum(["1", "0"]).optional(),
MAIL_FROM: z.string().email().optional(),
MAIL_FROM_NAME: z.string().optional(),
@@ -125,7 +126,6 @@ export const env = createEnv({
NEXT_PUBLIC_POSTHOG_API_KEY: z.string().optional(),
NEXT_PUBLIC_POSTHOG_API_HOST: z.string().optional(),
NEXT_PUBLIC_SENTRY_DSN: z.string().optional(),
NEXT_PUBLIC_INTERCOM_APP_ID: z.string().optional(),
NEXT_PUBLIC_TURNSTILE_SITE_KEY: z.string().optional(),
},
/*
@@ -187,7 +187,7 @@ export const env = createEnv({
NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
NEXT_PUBLIC_TURNSTILE_SITE_KEY: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY,
OPENTELEMETRY_LISTENER_URL: process.env.OPENTELEMETRY_LISTENER_URL,
NEXT_PUBLIC_INTERCOM_APP_ID: process.env.NEXT_PUBLIC_INTERCOM_APP_ID,
INTERCOM_APP_ID: process.env.INTERCOM_APP_ID,
NOTION_OAUTH_CLIENT_ID: process.env.NOTION_OAUTH_CLIENT_ID,
NOTION_OAUTH_CLIENT_SECRET: process.env.NOTION_OAUTH_CLIENT_SECRET,
OIDC_CLIENT_ID: process.env.OIDC_CLIENT_ID,

View File

@@ -3,6 +3,8 @@ import "@testing-library/jest-dom/vitest";
import { afterEach, beforeEach, expect, it, vi } from "vitest";
import { ValidationError } from "@formbricks/types/errors";
// mock next cache
vi.mock("next/cache", () => ({
__esModule: true,
unstable_cache: (fn: (params: unknown[]) => {}) => {
@@ -23,6 +25,8 @@ vi.mock("react", async () => {
};
});
// mock tolgee useTranslate on components
vi.mock("@tolgee/react", () => ({
useTranslate: () => {
return {
@@ -31,6 +35,8 @@ vi.mock("@tolgee/react", () => ({
},
}));
// mock next/router navigation
vi.mock("next/navigation", () => ({
useRouter: () => ({
push: vi.fn(),
@@ -47,6 +53,8 @@ vi.mock("server-only", () => {
return {};
});
// mock prisma client
vi.mock("@prisma/client", async () => {
const actual = await vi.importActual<typeof import("@prisma/client")>("@prisma/client");
@@ -67,6 +75,8 @@ vi.mock("@prisma/client", async () => {
};
});
// mock URL object
if (typeof URL.revokeObjectURL !== "function") {
URL.revokeObjectURL = () => {};
}
@@ -75,6 +85,28 @@ if (typeof URL.createObjectURL !== "function") {
URL.createObjectURL = () => "blob://fake-url";
}
// mock crypto function used in the license check utils, every component that checks the license will use this mock
// also used in a lot of other places too
vi.mock("crypto", async () => {
const actual = await vi.importActual<typeof import("crypto")>("crypto");
return {
...actual,
createHash: () => ({
update: vi.fn().mockReturnThis(),
digest: vi.fn().mockReturnValue("fake-hash"),
}),
default: {
...actual,
createHash: () => ({
update: vi.fn().mockReturnThis(),
digest: vi.fn().mockReturnValue("fake-hash"),
}),
},
};
});
beforeEach(() => {
vi.resetModules();
vi.resetAllMocks();

View File

@@ -121,6 +121,7 @@
"IMPRINT_ADDRESS",
"INVITE_DISABLED",
"IS_FORMBRICKS_CLOUD",
"INTERCOM_APP_ID",
"INTERCOM_SECRET_KEY",
"MAIL_FROM",
"MAIL_FROM_NAME",
@@ -135,7 +136,6 @@
"NEXT_PUBLIC_FORMBRICKS_URL",
"NEXT_PUBLIC_SENTRY_DSN",
"NEXT_PUBLIC_POSTHOG_API_HOST",
"NEXT_PUBLIC_INTERCOM_APP_ID",
"NEXT_PUBLIC_POSTHOG_API_KEY",
"NEXT_PUBLIC_FORMBRICKS_COM_API_HOST",
"NEXT_PUBLIC_FORMBRICKS_COM_ENVIRONMENT_ID",