chore: add tests for apps/web/lib files (#5725) (#5726)

This commit is contained in:
Matti Nannt
2025-05-09 01:35:50 +02:00
committed by GitHub
parent dea40d9757
commit 9df791b5ff
5 changed files with 864 additions and 0 deletions

219
apps/web/lib/auth.test.ts Normal file
View File

@@ -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);
});
});

View File

@@ -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");
});
});

View File

@@ -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);
});
});

195
apps/web/lib/jwt.test.ts Normal file
View File

@@ -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");
});
});
});

View File

@@ -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");
});
});
});