Repository: formbricks/formbricks (2025-07-21T07:29:36Z)

Issue:
Personalized survey link with single-use feature throws an error
### Issue Summary

In surveys with the single-use link feature enabled, creating personalized survey links results in submission failures. This occurs because the `suID` (single-use ID) is not being added to the personalized link, which causes the survey to reject the submission.

Discussion: https://github.com/formbricks/formbricks/discussions/6122

### Expected Behavior

When a personalized survey link is created, it should also include an `suID` if the survey has the single-use feature enabled.

### Technical considerations

We already have two different links / router pages for personalized links (`/c/[jwt]`) and anonymous links / single use links (`/s/[surveyId]`.
We can use this to move the check for the single use id requirement and query parameter to the `/s/[surveyId]` specific page or component and remove this check from the shared `<LinkSurvey />` component to avoid that the check happens when the `/c/[jwt]` route is used.

### Other information (incl. screenshots, Formbricks version, steps to reproduce,...)

Steps to reproduce:

1. Create a link survey with "single-use survey links" enabled.
2. Generate personalized survey links using the [Management API](https://formbricks.com/docs/api-v2-reference/management-api--surveys--contact-links/get-personalized-survey-link-for-a-contact#get-personalized-survey-link-for-a-contact).
3. Try to fill out the survey using the generated link.
4. You'll encounter an error indicating a missing suID.

![Image](https://github.com/user-attachments/assets/d8066943-d087-42f8-8de9-29c823551365)

### Your Environment

None


Discussion:
This is exactly why we are reorganizing the single use link functionality, moving it to the Share modal. These functionalities shouldn't interact the way they currently can.



Patch:
diff --git a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/route.ts b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/route.ts
index 200d319c0622..8bf733ecfa4b 100644
--- a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/route.ts
+++ b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/route.ts
@@ -92,7 +92,7 @@ export const GET = async (request: Request, props: { params: Promise<TContactLin
         });
       }
 
-      const surveyUrlResult = getContactSurveyLink(params.contactId, params.surveyId, 7);
+      const surveyUrlResult = await getContactSurveyLink(params.contactId, params.surveyId, 7);
 
       if (!surveyUrlResult.ok) {
         return handleApiError(request, surveyUrlResult.error);
diff --git a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/route.ts b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/route.ts
index 75032c17536a..2e67aa9bd169 100644
--- a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/route.ts
+++ b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/route.ts
@@ -82,11 +82,11 @@ export const GET = async (
       }
 
       // Generate survey links for each contact
-      const contactLinks = contacts
-        .map((contact) => {
+      const contactLinks = await Promise.all(
+        contacts.map(async (contact) => {
           const { contactId, attributes } = contact;
 
-          const surveyUrlResult = getContactSurveyLink(
+          const surveyUrlResult = await getContactSurveyLink(
             contactId,
             params.surveyId,
             query?.expirationDays || undefined
@@ -107,10 +107,11 @@ export const GET = async (
             expiresAt,
           };
         })
-        .filter(Boolean);
+      );
 
+      const filteredContactLinks = contactLinks.filter(Boolean);
       return responses.successResponse({
-        data: contactLinks,
+        data: filteredContactLinks,
         meta,
       });
     },
diff --git a/apps/web/modules/ee/contacts/lib/contact-survey-link.ts b/apps/web/modules/ee/contacts/lib/contact-survey-link.ts
index 30cd4204dfa7..8ec376096d92 100644
--- a/apps/web/modules/ee/contacts/lib/contact-survey-link.ts
+++ b/apps/web/modules/ee/contacts/lib/contact-survey-link.ts
@@ -1,17 +1,19 @@
 import { ENCRYPTION_KEY } from "@/lib/constants";
 import { symmetricDecrypt, symmetricEncrypt } from "@/lib/crypto";
 import { getPublicDomain } from "@/lib/getPublicUrl";
+import { generateSurveySingleUseId } from "@/lib/utils/single-use-surveys";
 import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
+import { getSurvey } from "@/modules/survey/lib/survey";
 import jwt from "jsonwebtoken";
 import { logger } from "@formbricks/logger";
 import { Result, err, ok } from "@formbricks/types/error-handlers";
 
 // Creates an encrypted personalized survey link for a contact
-export const getContactSurveyLink = (
+export const getContactSurveyLink = async (
   contactId: string,
   surveyId: string,
   expirationDays?: number
-): Result<string, ApiErrorResponseV2> => {
+): Promise<Result<string, ApiErrorResponseV2>> => {
   if (!ENCRYPTION_KEY) {
     return err({
       type: "internal_server_error",
@@ -19,10 +21,27 @@ export const getContactSurveyLink = (
     });
   }
 
+  const survey = await getSurvey(surveyId);
+  if (!survey) {
+    return err({
+      type: "not_found",
+      message: "Survey not found",
+      details: [{ field: "surveyId", issue: "not_found" }],
+    });
+  }
+
+  const { enabled: isSingleUseEnabled, isEncrypted: isSingleUseEncrypted } = survey.singleUse ?? {};
+
   // Encrypt the contact and survey IDs
   const encryptedContactId = symmetricEncrypt(contactId, ENCRYPTION_KEY);
   const encryptedSurveyId = symmetricEncrypt(surveyId, ENCRYPTION_KEY);
 
+  let singleUseId: string | undefined;
+
+  if (isSingleUseEnabled) {
+    singleUseId = generateSurveySingleUseId(isSingleUseEncrypted ?? false);
+  }
+
   // Create JWT payload with encrypted IDs
   const payload = {
     contactId: encryptedContactId,
@@ -43,7 +62,9 @@ export const getContactSurveyLink = (
   const token = jwt.sign(payload, ENCRYPTION_KEY, tokenOptions);
 
   // Return the personalized URL
-  return ok(`${getPublicDomain()}/c/${token}`);
+  return singleUseId
+    ? ok(`${getPublicDomain()}/c/${token}?suId=${singleUseId}`)
+    : ok(`${getPublicDomain()}/c/${token}`);
 };
 
 // Validates and decrypts a contact survey JWT token
@@ -59,7 +80,10 @@ export const verifyContactSurveyToken = (
 
   try {
     // Verify the token
-    const decoded = jwt.verify(token, ENCRYPTION_KEY) as { contactId: string; surveyId: string };
+    const decoded = jwt.verify(token, ENCRYPTION_KEY) as {
+      contactId: string;
+      surveyId: string;
+    };
 
     if (!decoded || !decoded.contactId || !decoded.surveyId) {
       throw err("Invalid token format");
diff --git a/apps/web/modules/ee/contacts/lib/contacts.ts b/apps/web/modules/ee/contacts/lib/contacts.ts
index 499a44b151dc..a6a2a6bf0b75 100644
--- a/apps/web/modules/ee/contacts/lib/contacts.ts
+++ b/apps/web/modules/ee/contacts/lib/contacts.ts
@@ -501,11 +501,11 @@ export const generatePersonalLinks = async (surveyId: string, segmentId: string,
   }
 
   // Generate survey links for each contact
-  const contactLinks = contactsResult
-    .map((contact) => {
+  const contactLinks = await Promise.all(
+    contactsResult.map(async (contact) => {
       const { contactId, attributes } = contact;
 
-      const surveyUrlResult = getContactSurveyLink(contactId, surveyId, expirationDays);
+      const surveyUrlResult = await getContactSurveyLink(contactId, surveyId, expirationDays);
 
       if (!surveyUrlResult.ok) {
         logger.error(
@@ -522,7 +522,8 @@ export const generatePersonalLinks = async (surveyId: string, segmentId: string,
         expirationDays,
       };
     })
-    .filter(Boolean);
+  );
 
-  return contactLinks;
+  const filteredContactLinks = contactLinks.filter(Boolean);
+  return filteredContactLinks;
 };
diff --git a/apps/web/modules/survey/link/contact-survey/page.tsx b/apps/web/modules/survey/link/contact-survey/page.tsx
index aeeac245a6ee..063b48db04b0 100644
--- a/apps/web/modules/survey/link/contact-survey/page.tsx
+++ b/apps/web/modules/survey/link/contact-survey/page.tsx
@@ -3,6 +3,7 @@ import { getSurvey } from "@/modules/survey/lib/survey";
 import { SurveyInactive } from "@/modules/survey/link/components/survey-inactive";
 import { renderSurvey } from "@/modules/survey/link/components/survey-renderer";
 import { getExistingContactResponse } from "@/modules/survey/link/lib/data";
+import { checkAndValidateSingleUseId } from "@/modules/survey/link/lib/helper";
 import { getBasicSurveyMetadata } from "@/modules/survey/link/lib/metadata-utils";
 import { getProjectByEnvironmentId } from "@/modules/survey/link/lib/project";
 import { getTranslate } from "@/tolgee/server";
@@ -14,6 +15,7 @@ interface ContactSurveyPageProps {
     jwt: string;
   }>;
   searchParams: Promise<{
+    suId?: string;
     verify?: string;
     lang?: string;
     embed?: string;
@@ -46,9 +48,10 @@ export const generateMetadata = async (props: ContactSurveyPageProps): Promise<M
 export const ContactSurveyPage = async (props: ContactSurveyPageProps) => {
   const searchParams = await props.searchParams;
   const params = await props.params;
+
   const t = await getTranslate();
   const { jwt } = params;
-  const { preview } = searchParams;
+  const { preview, suId } = searchParams;
 
   const result = verifyContactSurveyToken(jwt);
   if (!result.ok) {
@@ -62,6 +65,7 @@ export const ContactSurveyPage = async (props: ContactSurveyPageProps) => {
     // So we show SurveyInactive without project data (shows branding by default for backward compatibility)
     return <SurveyInactive status="link invalid" />;
   }
+
   const { surveyId, contactId } = result.data;
 
   const existingResponse = await getExistingContactResponse(surveyId, contactId)();
@@ -81,10 +85,26 @@ export const ContactSurveyPage = async (props: ContactSurveyPageProps) => {
     notFound();
   }
 
+  const isSingleUseSurvey = survey?.singleUse?.enabled;
+  const isSingleUseSurveyEncrypted = survey?.singleUse?.isEncrypted;
+
+  let singleUseId: string | undefined = undefined;
+
+  if (isSingleUseSurvey) {
+    const validatedSingleUseId = checkAndValidateSingleUseId(suId, isSingleUseSurveyEncrypted);
+    if (!validatedSingleUseId) {
+      const project = await getProjectByEnvironmentId(survey.environmentId);
+      return <SurveyInactive status="link invalid" project={project ?? undefined} />;
+    }
+
+    singleUseId = validatedSingleUseId;
+  }
+
   return renderSurvey({
     survey,
     searchParams,
     contactId,
     isPreview,
+    singleUseId,
   });
 };
diff --git a/apps/web/modules/survey/link/lib/helper.ts b/apps/web/modules/survey/link/lib/helper.ts
index 902b2d5491ec..85809001cd41 100644
--- a/apps/web/modules/survey/link/lib/helper.ts
+++ b/apps/web/modules/survey/link/lib/helper.ts
@@ -1,4 +1,5 @@
 import "server-only";
+import { validateSurveySingleUseId } from "@/app/lib/singleUseSurveys";
 import { verifyTokenForLinkSurvey } from "@/lib/jwt";
 
 interface emailVerificationDetails {
@@ -25,3 +26,15 @@ export const getEmailVerificationDetails = async (
     }
   }
 };
+
+export const checkAndValidateSingleUseId = (suid?: string, isEncrypted = false): string | null => {
+  if (!suid?.trim()) return null;
+
+  if (isEncrypted) {
+    const validatedSingleUseId = validateSurveySingleUseId(suid);
+    if (!validatedSingleUseId) return null;
+    return validatedSingleUseId;
+  }
+
+  return suid;
+};
diff --git a/apps/web/modules/survey/link/page.tsx b/apps/web/modules/survey/link/page.tsx
index ed060bbd0ea7..587d6c676643 100644
--- a/apps/web/modules/survey/link/page.tsx
+++ b/apps/web/modules/survey/link/page.tsx
@@ -1,7 +1,7 @@
-import { validateSurveySingleUseId } from "@/app/lib/singleUseSurveys";
 import { SurveyInactive } from "@/modules/survey/link/components/survey-inactive";
 import { renderSurvey } from "@/modules/survey/link/components/survey-renderer";
 import { getResponseBySingleUseId, getSurveyWithMetadata } from "@/modules/survey/link/lib/data";
+import { checkAndValidateSingleUseId } from "@/modules/survey/link/lib/helper";
 import { getProjectByEnvironmentId } from "@/modules/survey/link/lib/project";
 import { getMetadataForLinkSurvey } from "@/modules/survey/link/metadata";
 import type { Metadata } from "next";
@@ -60,23 +60,13 @@ export const LinkSurveyPage = async (props: LinkSurveyPageProps) => {
   let singleUseId: string | undefined = undefined;
 
   if (isSingleUseSurvey) {
-    // check if the single use id is present for single use surveys
-    if (!suId) {
+    const validatedSingleUseId = checkAndValidateSingleUseId(suId, isSingleUseSurveyEncrypted);
+    if (!validatedSingleUseId) {
       const project = await getProjectByEnvironmentId(survey.environmentId);
       return <SurveyInactive status="link invalid" project={project ?? undefined} />;
     }
 
-    // if encryption is enabled, validate the single use id
-    let validatedSingleUseId: string | undefined = undefined;
-    if (isSingleUseSurveyEncrypted) {
-      validatedSingleUseId = validateSurveySingleUseId(suId);
-      if (!validatedSingleUseId) {
-        const project = await getProjectByEnvironmentId(survey.environmentId);
-        return <SurveyInactive status="link invalid" project={project ?? undefined} />;
-      }
-    }
-    // if encryption is disabled, use the suId as is
-    singleUseId = validatedSingleUseId ?? suId;
+    singleUseId = validatedSingleUseId;
   }
 
   let singleUseResponse;


Test patch:
diff --git a/apps/web/modules/ee/contacts/lib/contact-survey-link.test.ts b/apps/web/modules/ee/contacts/lib/contact-survey-link.test.ts
index bbba2228ce95..0699095e2c1e 100644
--- a/apps/web/modules/ee/contacts/lib/contact-survey-link.test.ts
+++ b/apps/web/modules/ee/contacts/lib/contact-survey-link.test.ts
@@ -1,8 +1,11 @@
 import { ENCRYPTION_KEY } from "@/lib/constants";
 import * as crypto from "@/lib/crypto";
 import { getPublicDomain } from "@/lib/getPublicUrl";
+import { generateSurveySingleUseId } from "@/lib/utils/single-use-surveys";
+import { getSurvey } from "@/modules/survey/lib/survey";
 import jwt from "jsonwebtoken";
 import { beforeEach, describe, expect, test, vi } from "vitest";
+import { TSurvey } from "@formbricks/types/surveys/types";
 import * as contactSurveyLink from "./contact-survey-link";
 
 // Mock all modules needed (this gets hoisted to the top of the file)
@@ -33,12 +36,22 @@ vi.mock("@/lib/crypto", () => ({
   symmetricDecrypt: vi.fn(),
 }));
 
+vi.mock("@/modules/survey/lib/survey", () => ({
+  getSurvey: vi.fn(),
+}));
+
+vi.mock("@/lib/utils/single-use-surveys", () => ({
+  generateSurveySingleUseId: vi.fn(),
+}));
+
 describe("Contact Survey Link", () => {
   const mockContactId = "contact-123";
   const mockSurveyId = "survey-456";
   const mockToken = "mock.jwt.token";
   const mockEncryptedContactId = "encrypted-contact-id";
   const mockEncryptedSurveyId = "encrypted-survey-id";
+  const mockedGetSurvey = vi.mocked(getSurvey);
+  const mockedGenerateSurveySingleUseId = vi.mocked(generateSurveySingleUseId);
 
   beforeEach(() => {
     vi.clearAllMocks();
@@ -60,11 +73,17 @@ describe("Contact Survey Link", () => {
       contactId: mockEncryptedContactId,
       surveyId: mockEncryptedSurveyId,
     } as any);
+
+    mockedGetSurvey.mockResolvedValue({
+      id: mockSurveyId,
+      singleUse: { enabled: false, isEncrypted: false },
+    } as TSurvey);
+    mockedGenerateSurveySingleUseId.mockReturnValue("single-use-id");
   });
 
   describe("getContactSurveyLink", () => {
-    test("creates a survey link with encrypted contact and survey IDs", () => {
-      const result = contactSurveyLink.getContactSurveyLink(mockContactId, mockSurveyId);
+    test("creates a survey link with encrypted contact and survey IDs", async () => {
+      const result = await contactSurveyLink.getContactSurveyLink(mockContactId, mockSurveyId);
 
       // Verify encryption was called for both IDs
       expect(crypto.symmetricEncrypt).toHaveBeenCalledWith(mockContactId, ENCRYPTION_KEY);
@@ -85,11 +104,13 @@ describe("Contact Survey Link", () => {
         ok: true,
         data: `${getPublicDomain()}/c/${mockToken}`,
       });
+
+      expect(mockedGenerateSurveySingleUseId).not.toHaveBeenCalled();
     });
 
-    test("adds expiration to the token when expirationDays is provided", () => {
+    test("adds expiration to the token when expirationDays is provided", async () => {
       const expirationDays = 7;
-      contactSurveyLink.getContactSurveyLink(mockContactId, mockSurveyId, expirationDays);
+      await contactSurveyLink.getContactSurveyLink(mockContactId, mockSurveyId, expirationDays);
 
       // Verify JWT sign was called with expiration
       expect(jwt.sign).toHaveBeenCalledWith(
@@ -102,7 +123,55 @@ describe("Contact Survey Link", () => {
       );
     });
 
-    test("throws an error when ENCRYPTION_KEY is not available", async () => {
+    test("returns a not_found error when survey does not exist", async () => {
+      mockedGetSurvey.mockResolvedValue(null as unknown as TSurvey);
+
+      const result = await contactSurveyLink.getContactSurveyLink(mockContactId, "unfound-survey-id");
+
+      expect(result).toEqual({
+        ok: false,
+        error: {
+          type: "not_found",
+          message: "Survey not found",
+          details: [{ field: "surveyId", issue: "not_found" }],
+        },
+      });
+      expect(mockedGetSurvey).toHaveBeenCalledWith("unfound-survey-id");
+    });
+
+    test("creates a link with unencrypted single use ID when enabled", async () => {
+      mockedGetSurvey.mockResolvedValue({
+        id: mockSurveyId,
+        singleUse: { enabled: true, isEncrypted: false },
+      } as TSurvey);
+      mockedGenerateSurveySingleUseId.mockReturnValue("suId-unencrypted");
+
+      const result = await contactSurveyLink.getContactSurveyLink(mockContactId, mockSurveyId);
+
+      expect(mockedGenerateSurveySingleUseId).toHaveBeenCalledWith(false);
+      expect(result).toEqual({
+        ok: true,
+        data: `${getPublicDomain()}/c/${mockToken}?suId=suId-unencrypted`,
+      });
+    });
+
+    test("creates a link with encrypted single use ID when enabled and encrypted", async () => {
+      mockedGetSurvey.mockResolvedValue({
+        id: mockSurveyId,
+        singleUse: { enabled: true, isEncrypted: true },
+      } as TSurvey);
+      mockedGenerateSurveySingleUseId.mockReturnValue("suId-encrypted");
+
+      const result = await contactSurveyLink.getContactSurveyLink(mockContactId, mockSurveyId);
+
+      expect(mockedGenerateSurveySingleUseId).toHaveBeenCalledWith(true);
+      expect(result).toEqual({
+        ok: true,
+        data: `${getPublicDomain()}/c/${mockToken}?suId=suId-encrypted`,
+      });
+    });
+
+    test("returns an error when ENCRYPTION_KEY is not available", async () => {
       // Reset modules so the new mock is used by the module under test
       vi.resetModules();
       // Re‑mock constants to simulate missing ENCRYPTION_KEY
@@ -113,7 +182,7 @@ describe("Contact Survey Link", () => {
       // Re‑import the modules so they pick up the new mock
       const { getContactSurveyLink } = await import("./contact-survey-link");
 
-      const result = getContactSurveyLink(mockContactId, mockSurveyId);
+      const result = await getContactSurveyLink(mockContactId, mockSurveyId);
       expect(result).toEqual({
         ok: false,
         error: {
@@ -141,7 +210,7 @@ describe("Contact Survey Link", () => {
       });
     });
 
-    test("throws an error when token verification fails", () => {
+    test("returns an error when token verification fails", () => {
       vi.mocked(jwt.verify).mockImplementation(() => {
         throw new Error("Token verification failed");
       });
@@ -157,7 +226,7 @@ describe("Contact Survey Link", () => {
       }
     });
 
-    test("throws an error when token has invalid format", () => {
+    test("returns an error when token has invalid format", () => {
       // Mock JWT.verify to return an incomplete payload
       vi.mocked(jwt.verify).mockReturnValue({
         // Missing surveyId
@@ -178,7 +247,7 @@ describe("Contact Survey Link", () => {
       }
     });
 
-    test("throws an error when ENCRYPTION_KEY is not available", async () => {
+    test("returns an error when ENCRYPTION_KEY is not available", async () => {
       vi.resetModules();
       vi.doMock("@/lib/constants", () => ({
         ENCRYPTION_KEY: undefined,
diff --git a/apps/web/modules/survey/link/lib/helper.test.ts b/apps/web/modules/survey/link/lib/helper.test.ts
index 59beb802bf08..2f69b2ea9492 100644
--- a/apps/web/modules/survey/link/lib/helper.test.ts
+++ b/apps/web/modules/survey/link/lib/helper.test.ts
@@ -1,11 +1,16 @@
+import { validateSurveySingleUseId } from "@/app/lib/singleUseSurveys";
 import { verifyTokenForLinkSurvey } from "@/lib/jwt";
 import { beforeEach, describe, expect, test, vi } from "vitest";
-import { getEmailVerificationDetails } from "./helper";
+import { checkAndValidateSingleUseId, getEmailVerificationDetails } from "./helper";
 
 vi.mock("@/lib/jwt", () => ({
   verifyTokenForLinkSurvey: vi.fn(),
 }));
 
+vi.mock("@/app/lib/singleUseSurveys", () => ({
+  validateSurveySingleUseId: vi.fn(),
+}));
+
 describe("getEmailVerificationDetails", () => {
   const mockedVerifyTokenForLinkSurvey = vi.mocked(verifyTokenForLinkSurvey);
   const testSurveyId = "survey-123";
@@ -54,3 +59,82 @@ describe("getEmailVerificationDetails", () => {
     expect(mockedVerifyTokenForLinkSurvey).toHaveBeenCalledWith(testToken, testSurveyId);
   });
 });
+
+describe("checkAndValidateSingleUseId", () => {
+  const mockedValidateSurveySingleUseId = vi.mocked(validateSurveySingleUseId);
+
+  beforeEach(() => {
+    vi.resetAllMocks();
+  });
+
+  test("returns null when no suid is provided", () => {
+    const result = checkAndValidateSingleUseId();
+
+    expect(result).toBeNull();
+    expect(mockedValidateSurveySingleUseId).not.toHaveBeenCalled();
+  });
+
+  test("returns null when suid is empty string", () => {
+    const result = checkAndValidateSingleUseId("");
+
+    expect(result).toBeNull();
+    expect(mockedValidateSurveySingleUseId).not.toHaveBeenCalled();
+  });
+
+  test("returns suid as-is when isEncrypted is false", () => {
+    const testSuid = "plain-suid-123";
+    const result = checkAndValidateSingleUseId(testSuid, false);
+
+    expect(result).toBe(testSuid);
+    expect(mockedValidateSurveySingleUseId).not.toHaveBeenCalled();
+  });
+
+  test("returns suid as-is when isEncrypted is not provided (defaults to false)", () => {
+    const testSuid = "plain-suid-123";
+    const result = checkAndValidateSingleUseId(testSuid);
+
+    expect(result).toBe(testSuid);
+    expect(mockedValidateSurveySingleUseId).not.toHaveBeenCalled();
+  });
+
+  test("returns validated suid when isEncrypted is true and validation succeeds", () => {
+    const encryptedSuid = "encrypted-suid-123";
+    const validatedSuid = "validated-suid-456";
+    mockedValidateSurveySingleUseId.mockReturnValueOnce(validatedSuid);
+
+    const result = checkAndValidateSingleUseId(encryptedSuid, true);
+
+    expect(result).toBe(validatedSuid);
+    expect(mockedValidateSurveySingleUseId).toHaveBeenCalledWith(encryptedSuid);
+  });
+
+  test("returns null when isEncrypted is true and validation returns undefined", () => {
+    const encryptedSuid = "invalid-encrypted-suid";
+    mockedValidateSurveySingleUseId.mockReturnValueOnce(undefined);
+
+    const result = checkAndValidateSingleUseId(encryptedSuid, true);
+
+    expect(result).toBeNull();
+    expect(mockedValidateSurveySingleUseId).toHaveBeenCalledWith(encryptedSuid);
+  });
+
+  test("returns null when isEncrypted is true and validation returns empty string", () => {
+    const encryptedSuid = "invalid-encrypted-suid";
+    mockedValidateSurveySingleUseId.mockReturnValueOnce("");
+
+    const result = checkAndValidateSingleUseId(encryptedSuid, true);
+
+    expect(result).toBeNull();
+    expect(mockedValidateSurveySingleUseId).toHaveBeenCalledWith(encryptedSuid);
+  });
+
+  test("returns null when isEncrypted is true and validation returns null", () => {
+    const encryptedSuid = "invalid-encrypted-suid";
+    mockedValidateSurveySingleUseId.mockReturnValueOnce(null as any);
+
+    const result = checkAndValidateSingleUseId(encryptedSuid, true);
+
+    expect(result).toBeNull();
+    expect(mockedValidateSurveySingleUseId).toHaveBeenCalledWith(encryptedSuid);
+  });
+});
diff --git a/apps/web/modules/survey/link/page.test.tsx b/apps/web/modules/survey/link/page.test.tsx
index ca14b569f2b3..88c6f78e7eb8 100644
--- a/apps/web/modules/survey/link/page.test.tsx
+++ b/apps/web/modules/survey/link/page.test.tsx
@@ -13,6 +13,16 @@ import { logger } from "@formbricks/logger";
 import { TSurvey } from "@formbricks/types/surveys/types";
 import { LinkSurveyPage, generateMetadata } from "./page";
 
+// Mock server-side constants to prevent client-side access
+vi.mock("@/lib/constants", () => ({
+  IS_FORMBRICKS_CLOUD: false,
+  IS_RECAPTCHA_CONFIGURED: false,
+  RECAPTCHA_SITE_KEY: "test-key",
+  IMPRINT_URL: "https://example.com/imprint",
+  PRIVACY_URL: "https://example.com/privacy",
+  ENCRYPTION_KEY: "0".repeat(32),
+}));
+
 // Mock dependencies
 vi.mock("next/navigation", () => ({
   notFound: vi.fn(),