From 9df791b5ffa32e31e630c07bf039a6a3f5819a3e Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Fri, 9 May 2025 01:35:50 +0200 Subject: [PATCH] chore: add tests for apps/web/lib files (#5725) (#5726) --- apps/web/lib/auth.test.ts | 219 ++++++++++++++++++ apps/web/lib/getSurveyUrl.test.ts | 46 ++++ apps/web/lib/hashString.test.ts | 51 +++++ apps/web/lib/jwt.test.ts | 195 +++++++++++++++++ apps/web/lib/responses.test.ts | 353 ++++++++++++++++++++++++++++++ 5 files changed, 864 insertions(+) create mode 100644 apps/web/lib/auth.test.ts create mode 100644 apps/web/lib/getSurveyUrl.test.ts create mode 100644 apps/web/lib/hashString.test.ts create mode 100644 apps/web/lib/jwt.test.ts create mode 100644 apps/web/lib/responses.test.ts diff --git a/apps/web/lib/auth.test.ts b/apps/web/lib/auth.test.ts new file mode 100644 index 0000000000..d1cc1a1f56 --- /dev/null +++ b/apps/web/lib/auth.test.ts @@ -0,0 +1,219 @@ +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { prisma } from "@formbricks/database"; +import { AuthenticationError } from "@formbricks/types/errors"; +import { + hasOrganizationAccess, + hasOrganizationAuthority, + hasOrganizationOwnership, + hashPassword, + isManagerOrOwner, + isOwner, + verifyPassword, +} from "./auth"; + +// Mock prisma +vi.mock("@formbricks/database", () => ({ + prisma: { + membership: { + findUnique: vi.fn(), + }, + }, +})); + +describe("Password Management", () => { + test("hashPassword should hash a password", async () => { + const password = "testPassword123"; + const hashedPassword = await hashPassword(password); + expect(hashedPassword).toBeDefined(); + expect(hashedPassword).not.toBe(password); + }); + + test("verifyPassword should verify a correct password", async () => { + const password = "testPassword123"; + const hashedPassword = await hashPassword(password); + const isValid = await verifyPassword(password, hashedPassword); + expect(isValid).toBe(true); + }); + + test("verifyPassword should reject an incorrect password", async () => { + const password = "testPassword123"; + const hashedPassword = await hashPassword(password); + const isValid = await verifyPassword("wrongPassword", hashedPassword); + expect(isValid).toBe(false); + }); +}); + +describe("Organization Access", () => { + const mockUserId = "user123"; + const mockOrgId = "org123"; + + beforeEach(() => { + vi.resetAllMocks(); + }); + + test("hasOrganizationAccess should return true when user has membership", async () => { + vi.mocked(prisma.membership.findUnique).mockResolvedValue({ + id: "membership123", + userId: mockUserId, + organizationId: mockOrgId, + role: "member", + createdAt: new Date(), + updatedAt: new Date(), + }); + + const hasAccess = await hasOrganizationAccess(mockUserId, mockOrgId); + expect(hasAccess).toBe(true); + }); + + test("hasOrganizationAccess should return false when user has no membership", async () => { + vi.mocked(prisma.membership.findUnique).mockResolvedValue(null); + + const hasAccess = await hasOrganizationAccess(mockUserId, mockOrgId); + expect(hasAccess).toBe(false); + }); + + test("isManagerOrOwner should return true for manager role", async () => { + vi.mocked(prisma.membership.findUnique).mockResolvedValue({ + id: "membership123", + userId: mockUserId, + organizationId: mockOrgId, + role: "manager", + createdAt: new Date(), + updatedAt: new Date(), + }); + + const isManager = await isManagerOrOwner(mockUserId, mockOrgId); + expect(isManager).toBe(true); + }); + + test("isManagerOrOwner should return true for owner role", async () => { + vi.mocked(prisma.membership.findUnique).mockResolvedValue({ + id: "membership123", + userId: mockUserId, + organizationId: mockOrgId, + role: "owner", + createdAt: new Date(), + updatedAt: new Date(), + }); + + const isOwner = await isManagerOrOwner(mockUserId, mockOrgId); + expect(isOwner).toBe(true); + }); + + test("isManagerOrOwner should return false for member role", async () => { + vi.mocked(prisma.membership.findUnique).mockResolvedValue({ + id: "membership123", + userId: mockUserId, + organizationId: mockOrgId, + role: "member", + createdAt: new Date(), + updatedAt: new Date(), + }); + + const isManagerOrOwnerRole = await isManagerOrOwner(mockUserId, mockOrgId); + expect(isManagerOrOwnerRole).toBe(false); + }); + + test("isOwner should return true only for owner role", async () => { + vi.mocked(prisma.membership.findUnique).mockResolvedValue({ + id: "membership123", + userId: mockUserId, + organizationId: mockOrgId, + role: "owner", + createdAt: new Date(), + updatedAt: new Date(), + }); + + const isOwnerRole = await isOwner(mockUserId, mockOrgId); + expect(isOwnerRole).toBe(true); + }); + + test("isOwner should return false for non-owner roles", async () => { + vi.mocked(prisma.membership.findUnique).mockResolvedValue({ + id: "membership123", + userId: mockUserId, + organizationId: mockOrgId, + role: "manager", + createdAt: new Date(), + updatedAt: new Date(), + }); + + const isOwnerRole = await isOwner(mockUserId, mockOrgId); + expect(isOwnerRole).toBe(false); + }); +}); + +describe("Organization Authority", () => { + const mockUserId = "user123"; + const mockOrgId = "org123"; + + beforeEach(() => { + vi.resetAllMocks(); + }); + + test("hasOrganizationAuthority should return true for manager", async () => { + vi.mocked(prisma.membership.findUnique).mockResolvedValue({ + id: "membership123", + userId: mockUserId, + organizationId: mockOrgId, + role: "manager", + createdAt: new Date(), + updatedAt: new Date(), + }); + + const hasAuthority = await hasOrganizationAuthority(mockUserId, mockOrgId); + expect(hasAuthority).toBe(true); + }); + + test("hasOrganizationAuthority should throw for non-member", async () => { + vi.mocked(prisma.membership.findUnique).mockResolvedValue(null); + + await expect(hasOrganizationAuthority(mockUserId, mockOrgId)).rejects.toThrow(AuthenticationError); + }); + + test("hasOrganizationAuthority should throw for member role", async () => { + vi.mocked(prisma.membership.findUnique).mockResolvedValue({ + id: "membership123", + userId: mockUserId, + organizationId: mockOrgId, + role: "member", + createdAt: new Date(), + updatedAt: new Date(), + }); + + await expect(hasOrganizationAuthority(mockUserId, mockOrgId)).rejects.toThrow(AuthenticationError); + }); + + test("hasOrganizationOwnership should return true for owner", async () => { + vi.mocked(prisma.membership.findUnique).mockResolvedValue({ + id: "membership123", + userId: mockUserId, + organizationId: mockOrgId, + role: "owner", + createdAt: new Date(), + updatedAt: new Date(), + }); + + const hasOwnership = await hasOrganizationOwnership(mockUserId, mockOrgId); + expect(hasOwnership).toBe(true); + }); + + test("hasOrganizationOwnership should throw for non-member", async () => { + vi.mocked(prisma.membership.findUnique).mockResolvedValue(null); + + await expect(hasOrganizationOwnership(mockUserId, mockOrgId)).rejects.toThrow(AuthenticationError); + }); + + test("hasOrganizationOwnership should throw for non-owner roles", async () => { + vi.mocked(prisma.membership.findUnique).mockResolvedValue({ + id: "membership123", + userId: mockUserId, + organizationId: mockOrgId, + role: "manager", + createdAt: new Date(), + updatedAt: new Date(), + }); + + await expect(hasOrganizationOwnership(mockUserId, mockOrgId)).rejects.toThrow(AuthenticationError); + }); +}); diff --git a/apps/web/lib/getSurveyUrl.test.ts b/apps/web/lib/getSurveyUrl.test.ts new file mode 100644 index 0000000000..ff48cfa9ad --- /dev/null +++ b/apps/web/lib/getSurveyUrl.test.ts @@ -0,0 +1,46 @@ +import { beforeEach, describe, expect, test, vi } from "vitest"; + +// Create a mock module for constants with proper types +const constantsMock = { + SURVEY_URL: undefined as string | undefined, + WEBAPP_URL: "http://localhost:3000" as string, +}; + +// Mock the constants module +vi.mock("./constants", () => constantsMock); + +describe("getSurveyDomain", () => { + beforeEach(() => { + // Reset the mock values before each test + constantsMock.SURVEY_URL = undefined; + constantsMock.WEBAPP_URL = "http://localhost:3000"; + vi.resetModules(); + }); + + test("should return WEBAPP_URL when SURVEY_URL is not set", async () => { + const { getSurveyDomain } = await import("./getSurveyUrl"); + const domain = getSurveyDomain(); + expect(domain).toBe("http://localhost:3000"); + }); + + test("should return SURVEY_URL when it is set", async () => { + constantsMock.SURVEY_URL = "https://surveys.example.com"; + const { getSurveyDomain } = await import("./getSurveyUrl"); + const domain = getSurveyDomain(); + expect(domain).toBe("https://surveys.example.com"); + }); + + test("should handle empty string SURVEY_URL by returning WEBAPP_URL", async () => { + constantsMock.SURVEY_URL = ""; + const { getSurveyDomain } = await import("./getSurveyUrl"); + const domain = getSurveyDomain(); + expect(domain).toBe("http://localhost:3000"); + }); + + test("should handle undefined SURVEY_URL by returning WEBAPP_URL", async () => { + constantsMock.SURVEY_URL = undefined; + const { getSurveyDomain } = await import("./getSurveyUrl"); + const domain = getSurveyDomain(); + expect(domain).toBe("http://localhost:3000"); + }); +}); diff --git a/apps/web/lib/hashString.test.ts b/apps/web/lib/hashString.test.ts new file mode 100644 index 0000000000..95174914f4 --- /dev/null +++ b/apps/web/lib/hashString.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, test } from "vitest"; +import { hashString } from "./hashString"; + +describe("hashString", () => { + test("should return a string", () => { + const input = "test string"; + const hash = hashString(input); + + expect(typeof hash).toBe("string"); + expect(hash.length).toBeGreaterThan(0); + }); + + test("should produce consistent hashes for the same input", () => { + const input = "test string"; + const hash1 = hashString(input); + const hash2 = hashString(input); + + expect(hash1).toBe(hash2); + }); + + test("should handle empty strings", () => { + const hash = hashString(""); + + expect(typeof hash).toBe("string"); + expect(hash.length).toBeGreaterThan(0); + }); + + test("should handle special characters", () => { + const input = "!@#$%^&*()_+{}|:<>?"; + const hash = hashString(input); + + expect(typeof hash).toBe("string"); + expect(hash.length).toBeGreaterThan(0); + }); + + test("should handle unicode characters", () => { + const input = "Hello, 世界!"; + const hash = hashString(input); + + expect(typeof hash).toBe("string"); + expect(hash.length).toBeGreaterThan(0); + }); + + test("should handle long strings", () => { + const input = "a".repeat(1000); + const hash = hashString(input); + + expect(typeof hash).toBe("string"); + expect(hash.length).toBeGreaterThan(0); + }); +}); diff --git a/apps/web/lib/jwt.test.ts b/apps/web/lib/jwt.test.ts new file mode 100644 index 0000000000..ad1210c813 --- /dev/null +++ b/apps/web/lib/jwt.test.ts @@ -0,0 +1,195 @@ +import { env } from "@/lib/env"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { prisma } from "@formbricks/database"; +import { + createEmailToken, + createInviteToken, + createToken, + createTokenForLinkSurvey, + getEmailFromEmailToken, + verifyInviteToken, + verifyToken, + verifyTokenForLinkSurvey, +} from "./jwt"; + +// Mock environment variables +vi.mock("@/lib/env", () => ({ + env: { + ENCRYPTION_KEY: "0".repeat(32), // 32-byte key for AES-256-GCM + NEXTAUTH_SECRET: "test-nextauth-secret", + } as typeof env, +})); + +// Mock prisma +vi.mock("@formbricks/database", () => ({ + prisma: { + user: { + findUnique: vi.fn(), + }, + }, +})); + +describe("JWT Functions", () => { + const mockUser = { + id: "test-user-id", + email: "test@example.com", + }; + + beforeEach(() => { + vi.clearAllMocks(); + (prisma.user.findUnique as any).mockResolvedValue(mockUser); + }); + + describe("createToken", () => { + test("should create a valid token", () => { + const token = createToken(mockUser.id, mockUser.email); + expect(token).toBeDefined(); + expect(typeof token).toBe("string"); + }); + + test("should throw error if ENCRYPTION_KEY is not set", () => { + const originalKey = env.ENCRYPTION_KEY; + try { + (env as any).ENCRYPTION_KEY = undefined; + expect(() => createToken(mockUser.id, mockUser.email)).toThrow("ENCRYPTION_KEY is not set"); + } finally { + (env as any).ENCRYPTION_KEY = originalKey; + } + }); + }); + + describe("createTokenForLinkSurvey", () => { + test("should create a valid survey link token", () => { + const surveyId = "test-survey-id"; + const token = createTokenForLinkSurvey(surveyId, mockUser.email); + expect(token).toBeDefined(); + expect(typeof token).toBe("string"); + }); + + test("should throw error if ENCRYPTION_KEY is not set", () => { + const originalKey = env.ENCRYPTION_KEY; + try { + (env as any).ENCRYPTION_KEY = undefined; + expect(() => createTokenForLinkSurvey("test-survey-id", mockUser.email)).toThrow( + "ENCRYPTION_KEY is not set" + ); + } finally { + (env as any).ENCRYPTION_KEY = originalKey; + } + }); + }); + + describe("createEmailToken", () => { + test("should create a valid email token", () => { + const token = createEmailToken(mockUser.email); + expect(token).toBeDefined(); + expect(typeof token).toBe("string"); + }); + + test("should throw error if ENCRYPTION_KEY is not set", () => { + const originalKey = env.ENCRYPTION_KEY; + try { + (env as any).ENCRYPTION_KEY = undefined; + expect(() => createEmailToken(mockUser.email)).toThrow("ENCRYPTION_KEY is not set"); + } finally { + (env as any).ENCRYPTION_KEY = originalKey; + } + }); + + test("should throw error if NEXTAUTH_SECRET is not set", () => { + const originalSecret = env.NEXTAUTH_SECRET; + try { + (env as any).NEXTAUTH_SECRET = undefined; + expect(() => createEmailToken(mockUser.email)).toThrow("NEXTAUTH_SECRET is not set"); + } finally { + (env as any).NEXTAUTH_SECRET = originalSecret; + } + }); + }); + + describe("getEmailFromEmailToken", () => { + test("should extract email from valid token", () => { + const token = createEmailToken(mockUser.email); + const extractedEmail = getEmailFromEmailToken(token); + expect(extractedEmail).toBe(mockUser.email); + }); + + test("should throw error if ENCRYPTION_KEY is not set", () => { + const originalKey = env.ENCRYPTION_KEY; + try { + (env as any).ENCRYPTION_KEY = undefined; + expect(() => getEmailFromEmailToken("invalid-token")).toThrow("ENCRYPTION_KEY is not set"); + } finally { + (env as any).ENCRYPTION_KEY = originalKey; + } + }); + }); + + describe("createInviteToken", () => { + test("should create a valid invite token", () => { + const inviteId = "test-invite-id"; + const token = createInviteToken(inviteId, mockUser.email); + expect(token).toBeDefined(); + expect(typeof token).toBe("string"); + }); + + test("should throw error if ENCRYPTION_KEY is not set", () => { + const originalKey = env.ENCRYPTION_KEY; + try { + (env as any).ENCRYPTION_KEY = undefined; + expect(() => createInviteToken("test-invite-id", mockUser.email)).toThrow( + "ENCRYPTION_KEY is not set" + ); + } finally { + (env as any).ENCRYPTION_KEY = originalKey; + } + }); + }); + + describe("verifyTokenForLinkSurvey", () => { + test("should verify valid survey link token", () => { + const surveyId = "test-survey-id"; + const token = createTokenForLinkSurvey(surveyId, mockUser.email); + const verifiedEmail = verifyTokenForLinkSurvey(token, surveyId); + expect(verifiedEmail).toBe(mockUser.email); + }); + + test("should return null for invalid token", () => { + const result = verifyTokenForLinkSurvey("invalid-token", "test-survey-id"); + expect(result).toBeNull(); + }); + }); + + describe("verifyToken", () => { + test("should verify valid token", async () => { + const token = createToken(mockUser.id, mockUser.email); + const verified = await verifyToken(token); + expect(verified).toEqual({ + id: mockUser.id, + email: mockUser.email, + }); + }); + + test("should throw error if user not found", async () => { + (prisma.user.findUnique as any).mockResolvedValue(null); + const token = createToken(mockUser.id, mockUser.email); + await expect(verifyToken(token)).rejects.toThrow("User not found"); + }); + }); + + describe("verifyInviteToken", () => { + test("should verify valid invite token", () => { + const inviteId = "test-invite-id"; + const token = createInviteToken(inviteId, mockUser.email); + const verified = verifyInviteToken(token); + expect(verified).toEqual({ + inviteId, + email: mockUser.email, + }); + }); + + test("should throw error for invalid token", () => { + expect(() => verifyInviteToken("invalid-token")).toThrow("Invalid or expired invite token"); + }); + }); +}); diff --git a/apps/web/lib/responses.test.ts b/apps/web/lib/responses.test.ts new file mode 100644 index 0000000000..d534f8c46c --- /dev/null +++ b/apps/web/lib/responses.test.ts @@ -0,0 +1,353 @@ +import { describe, expect, test, vi } from "vitest"; +import { TSurveyQuestionType, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; +import { convertResponseValue, getQuestionResponseMapping, processResponseData } from "./responses"; + +// Mock the recall and i18n utils +vi.mock("@/lib/utils/recall", () => ({ + parseRecallInfo: vi.fn((text) => text), +})); + +vi.mock("./i18n/utils", () => ({ + getLocalizedValue: vi.fn((obj, lang) => obj[lang] || obj.default), +})); + +describe("Response Processing", () => { + describe("processResponseData", () => { + test("should handle string input", () => { + expect(processResponseData("test")).toBe("test"); + }); + + test("should handle number input", () => { + expect(processResponseData(42)).toBe("42"); + }); + + test("should handle array input", () => { + expect(processResponseData(["a", "b", "c"])).toBe("a; b; c"); + }); + + test("should filter out empty values from array", () => { + const input = ["a", "", "c"]; + expect(processResponseData(input)).toBe("a; c"); + }); + + test("should handle object input", () => { + const input = { key1: "value1", key2: "value2" }; + expect(processResponseData(input)).toBe("key1: value1\nkey2: value2"); + }); + + test("should filter out empty values from object", () => { + const input = { key1: "value1", key2: "", key3: "value3" }; + expect(processResponseData(input)).toBe("key1: value1\nkey3: value3"); + }); + + test("should return empty string for unsupported types", () => { + expect(processResponseData(undefined as any)).toBe(""); + }); + }); + + describe("convertResponseValue", () => { + const mockOpenTextQuestion = { + id: "q1", + type: TSurveyQuestionTypeEnum.OpenText as const, + headline: { default: "Test Question" }, + required: true, + inputType: "text" as const, + longAnswer: false, + charLimit: { enabled: false }, + }; + + const mockRankingQuestion = { + id: "q1", + type: TSurveyQuestionTypeEnum.Ranking as const, + headline: { default: "Test Question" }, + required: true, + choices: [ + { id: "1", label: { default: "Choice 1" } }, + { id: "2", label: { default: "Choice 2" } }, + ], + shuffleOption: "none" as const, + }; + + const mockFileUploadQuestion = { + id: "q1", + type: TSurveyQuestionTypeEnum.FileUpload as const, + headline: { default: "Test Question" }, + required: true, + allowMultipleFiles: true, + }; + + const mockPictureSelectionQuestion = { + id: "q1", + type: TSurveyQuestionTypeEnum.PictureSelection as const, + headline: { default: "Test Question" }, + required: true, + allowMulti: false, + choices: [ + { id: "1", imageUrl: "image1.jpg", label: { default: "Choice 1" } }, + { id: "2", imageUrl: "image2.jpg", label: { default: "Choice 2" } }, + ], + }; + + test("should handle ranking type with string input", () => { + expect(convertResponseValue("answer", mockRankingQuestion)).toEqual(["answer"]); + }); + + test("should handle ranking type with array input", () => { + expect(convertResponseValue(["answer1", "answer2"], mockRankingQuestion)).toEqual([ + "answer1", + "answer2", + ]); + }); + + test("should handle fileUpload type with string input", () => { + expect(convertResponseValue("file.jpg", mockFileUploadQuestion)).toEqual(["file.jpg"]); + }); + + test("should handle fileUpload type with array input", () => { + expect(convertResponseValue(["file1.jpg", "file2.jpg"], mockFileUploadQuestion)).toEqual([ + "file1.jpg", + "file2.jpg", + ]); + }); + + test("should handle pictureSelection type with string input", () => { + expect(convertResponseValue("1", mockPictureSelectionQuestion)).toEqual(["image1.jpg"]); + }); + + test("should handle pictureSelection type with array input", () => { + expect(convertResponseValue(["1", "2"], mockPictureSelectionQuestion)).toEqual([ + "image1.jpg", + "image2.jpg", + ]); + }); + + test("should handle pictureSelection type with invalid choice", () => { + expect(convertResponseValue("invalid", mockPictureSelectionQuestion)).toEqual([]); + }); + + test("should handle default case with string input", () => { + expect(convertResponseValue("answer", mockOpenTextQuestion)).toBe("answer"); + }); + + test("should handle default case with number input", () => { + expect(convertResponseValue(42, mockOpenTextQuestion)).toBe("42"); + }); + + test("should handle default case with array input", () => { + expect(convertResponseValue(["a", "b", "c"], mockOpenTextQuestion)).toBe("a; b; c"); + }); + + test("should handle default case with object input", () => { + const input = { key1: "value1", key2: "value2" }; + expect(convertResponseValue(input, mockOpenTextQuestion)).toBe("key1: value1\nkey2: value2"); + }); + }); + + describe("getQuestionResponseMapping", () => { + const mockSurvey = { + id: "survey1", + type: "link" as const, + status: "inProgress" as const, + createdAt: new Date(), + updatedAt: new Date(), + name: "Test Survey", + environmentId: "env1", + createdBy: null, + questions: [ + { + id: "q1", + type: TSurveyQuestionTypeEnum.OpenText as const, + headline: { default: "Question 1" }, + required: true, + inputType: "text" as const, + longAnswer: false, + charLimit: { enabled: false }, + }, + { + id: "q2", + type: TSurveyQuestionTypeEnum.MultipleChoiceMulti as const, + headline: { default: "Question 2" }, + required: true, + choices: [ + { id: "1", label: { default: "Option 1" } }, + { id: "2", label: { default: "Option 2" } }, + ], + shuffleOption: "none" as const, + }, + ], + hiddenFields: { + enabled: false, + fieldIds: [], + }, + displayOption: "displayOnce" as const, + delay: 0, + languages: [ + { + language: { + id: "lang1", + code: "default", + createdAt: new Date(), + updatedAt: new Date(), + alias: null, + projectId: "proj1", + }, + default: true, + enabled: true, + }, + ], + variables: [], + endings: [], + displayLimit: null, + autoClose: null, + autoComplete: null, + recontactDays: null, + runOnDate: null, + closeOnDate: null, + welcomeCard: { + enabled: false, + timeToFinish: false, + showResponseCount: false, + }, + showLanguageSwitch: false, + isBackButtonHidden: false, + isVerifyEmailEnabled: false, + isSingleResponsePerEmailEnabled: false, + displayPercentage: 100, + styling: null, + projectOverwrites: null, + verifyEmail: null, + inlineTriggers: [], + pin: null, + triggers: [], + followUps: [], + segment: null, + recaptcha: null, + surveyClosedMessage: null, + singleUse: { + enabled: false, + isEncrypted: false, + }, + resultShareKey: null, + }; + + const mockResponse = { + id: "response1", + surveyId: "survey1", + createdAt: new Date(), + updatedAt: new Date(), + finished: true, + data: { + q1: "Answer 1", + q2: ["Option 1", "Option 2"], + }, + language: "default", + meta: { + url: undefined, + country: undefined, + action: undefined, + source: undefined, + userAgent: undefined, + }, + notes: [], + tags: [], + person: null, + personAttributes: {}, + ttc: {}, + variables: {}, + contact: null, + contactAttributes: {}, + singleUseId: null, + }; + + test("should map questions to responses correctly", () => { + const mapping = getQuestionResponseMapping(mockSurvey, mockResponse); + expect(mapping).toHaveLength(2); + expect(mapping[0]).toEqual({ + question: "Question 1", + response: "Answer 1", + type: TSurveyQuestionTypeEnum.OpenText, + }); + expect(mapping[1]).toEqual({ + question: "Question 2", + response: "Option 1; Option 2", + type: TSurveyQuestionTypeEnum.MultipleChoiceMulti, + }); + }); + + test("should handle missing response data", () => { + const response = { + id: "response1", + surveyId: "survey1", + createdAt: new Date(), + updatedAt: new Date(), + finished: true, + data: {}, + language: "default", + meta: { + url: undefined, + country: undefined, + action: undefined, + source: undefined, + userAgent: undefined, + }, + notes: [], + tags: [], + person: null, + personAttributes: {}, + ttc: {}, + variables: {}, + contact: null, + contactAttributes: {}, + singleUseId: null, + }; + const mapping = getQuestionResponseMapping(mockSurvey, response); + expect(mapping).toHaveLength(2); + expect(mapping[0].response).toBe(""); + expect(mapping[1].response).toBe(""); + }); + + test("should handle different language", () => { + const survey = { + ...mockSurvey, + questions: [ + { + id: "q1", + type: TSurveyQuestionTypeEnum.OpenText as const, + headline: { default: "Question 1", en: "Question 1 EN" }, + required: true, + inputType: "text" as const, + longAnswer: false, + charLimit: { enabled: false }, + }, + ], + }; + const response = { + id: "response1", + surveyId: "survey1", + createdAt: new Date(), + updatedAt: new Date(), + finished: true, + data: { q1: "Answer 1" }, + language: "en", + meta: { + url: undefined, + country: undefined, + action: undefined, + source: undefined, + userAgent: undefined, + }, + notes: [], + tags: [], + person: null, + personAttributes: {}, + ttc: {}, + variables: {}, + contact: null, + contactAttributes: {}, + singleUseId: null, + }; + const mapping = getQuestionResponseMapping(survey, response); + expect(mapping[0].question).toBe("Question 1 EN"); + }); + }); +});