mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 10:19:51 -06:00
Compare commits
2 Commits
release/4.
...
unit-test-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33f2bce9b8 | ||
|
|
33451ebc89 |
@@ -14,17 +14,22 @@ import {
|
|||||||
SelectedFilterValue,
|
SelectedFilterValue,
|
||||||
} from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
} from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
||||||
import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox";
|
import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox";
|
||||||
|
import { TEST_IDS } from "@/lib/testing/constants";
|
||||||
|
import { setupTestEnvironment } from "@/lib/testing/setup";
|
||||||
import { generateQuestionAndFilterOptions, getFormattedFilters, getTodayDate } from "./surveys";
|
import { generateQuestionAndFilterOptions, getFormattedFilters, getTodayDate } from "./surveys";
|
||||||
|
|
||||||
describe("surveys", () => {
|
setupTestEnvironment();
|
||||||
afterEach(() => {
|
|
||||||
cleanup();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Cleanup React components after each test
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("surveys", () => {
|
||||||
describe("generateQuestionAndFilterOptions", () => {
|
describe("generateQuestionAndFilterOptions", () => {
|
||||||
test("should return question options for basic survey without additional options", () => {
|
test("should return question options for basic survey without additional options", () => {
|
||||||
const survey = {
|
const survey = {
|
||||||
id: "survey1",
|
id: TEST_IDS.survey,
|
||||||
name: "Test Survey",
|
name: "Test Survey",
|
||||||
questions: [
|
questions: [
|
||||||
{
|
{
|
||||||
@@ -35,7 +40,7 @@ describe("surveys", () => {
|
|||||||
],
|
],
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
environmentId: "env1",
|
environmentId: TEST_IDS.environment,
|
||||||
status: "draft",
|
status: "draft",
|
||||||
} as unknown as TSurvey;
|
} as unknown as TSurvey;
|
||||||
|
|
||||||
@@ -49,17 +54,23 @@ describe("surveys", () => {
|
|||||||
|
|
||||||
test("should include tags in options when provided", () => {
|
test("should include tags in options when provided", () => {
|
||||||
const survey = {
|
const survey = {
|
||||||
id: "survey1",
|
id: TEST_IDS.survey,
|
||||||
name: "Test Survey",
|
name: "Test Survey",
|
||||||
questions: [],
|
questions: [],
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
environmentId: "env1",
|
environmentId: TEST_IDS.environment,
|
||||||
status: "draft",
|
status: "draft",
|
||||||
} as unknown as TSurvey;
|
} as unknown as TSurvey;
|
||||||
|
|
||||||
const tags: TTag[] = [
|
const tags: TTag[] = [
|
||||||
{ id: "tag1", name: "Tag 1", environmentId: "env1", createdAt: new Date(), updatedAt: new Date() },
|
{
|
||||||
|
id: TEST_IDS.team,
|
||||||
|
name: "Tag 1",
|
||||||
|
environmentId: TEST_IDS.environment,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = generateQuestionAndFilterOptions(survey, tags, {}, {}, {}, []);
|
const result = generateQuestionAndFilterOptions(survey, tags, {}, {}, {}, []);
|
||||||
@@ -72,12 +83,12 @@ describe("surveys", () => {
|
|||||||
|
|
||||||
test("should include attributes in options when provided", () => {
|
test("should include attributes in options when provided", () => {
|
||||||
const survey = {
|
const survey = {
|
||||||
id: "survey1",
|
id: TEST_IDS.survey,
|
||||||
name: "Test Survey",
|
name: "Test Survey",
|
||||||
questions: [],
|
questions: [],
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
environmentId: "env1",
|
environmentId: TEST_IDS.environment,
|
||||||
status: "draft",
|
status: "draft",
|
||||||
} as unknown as TSurvey;
|
} as unknown as TSurvey;
|
||||||
|
|
||||||
@@ -95,12 +106,12 @@ describe("surveys", () => {
|
|||||||
|
|
||||||
test("should include meta in options when provided", () => {
|
test("should include meta in options when provided", () => {
|
||||||
const survey = {
|
const survey = {
|
||||||
id: "survey1",
|
id: TEST_IDS.survey,
|
||||||
name: "Test Survey",
|
name: "Test Survey",
|
||||||
questions: [],
|
questions: [],
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
environmentId: "env1",
|
environmentId: TEST_IDS.environment,
|
||||||
status: "draft",
|
status: "draft",
|
||||||
} as unknown as TSurvey;
|
} as unknown as TSurvey;
|
||||||
|
|
||||||
@@ -118,12 +129,12 @@ describe("surveys", () => {
|
|||||||
|
|
||||||
test("should include hidden fields in options when provided", () => {
|
test("should include hidden fields in options when provided", () => {
|
||||||
const survey = {
|
const survey = {
|
||||||
id: "survey1",
|
id: TEST_IDS.survey,
|
||||||
name: "Test Survey",
|
name: "Test Survey",
|
||||||
questions: [],
|
questions: [],
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
environmentId: "env1",
|
environmentId: TEST_IDS.environment,
|
||||||
status: "draft",
|
status: "draft",
|
||||||
} as unknown as TSurvey;
|
} as unknown as TSurvey;
|
||||||
|
|
||||||
@@ -143,12 +154,12 @@ describe("surveys", () => {
|
|||||||
|
|
||||||
test("should include language options when survey has languages", () => {
|
test("should include language options when survey has languages", () => {
|
||||||
const survey = {
|
const survey = {
|
||||||
id: "survey1",
|
id: TEST_IDS.survey,
|
||||||
name: "Test Survey",
|
name: "Test Survey",
|
||||||
questions: [],
|
questions: [],
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
environmentId: "env1",
|
environmentId: TEST_IDS.environment,
|
||||||
status: "draft",
|
status: "draft",
|
||||||
languages: [{ language: { code: "en" } as unknown as TLanguage } as unknown as TSurveyLanguage],
|
languages: [{ language: { code: "en" } as unknown as TLanguage } as unknown as TSurveyLanguage],
|
||||||
} as unknown as TSurvey;
|
} as unknown as TSurvey;
|
||||||
@@ -162,7 +173,7 @@ describe("surveys", () => {
|
|||||||
|
|
||||||
test("should handle all question types correctly", () => {
|
test("should handle all question types correctly", () => {
|
||||||
const survey = {
|
const survey = {
|
||||||
id: "survey1",
|
id: TEST_IDS.survey,
|
||||||
name: "Test Survey",
|
name: "Test Survey",
|
||||||
questions: [
|
questions: [
|
||||||
{
|
{
|
||||||
@@ -219,7 +230,7 @@ describe("surveys", () => {
|
|||||||
],
|
],
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
environmentId: "env1",
|
environmentId: TEST_IDS.environment,
|
||||||
status: "draft",
|
status: "draft",
|
||||||
} as unknown as TSurvey;
|
} as unknown as TSurvey;
|
||||||
|
|
||||||
@@ -234,12 +245,12 @@ describe("surveys", () => {
|
|||||||
|
|
||||||
test("should provide extended filter options for URL meta field", () => {
|
test("should provide extended filter options for URL meta field", () => {
|
||||||
const survey = {
|
const survey = {
|
||||||
id: "survey1",
|
id: TEST_IDS.survey,
|
||||||
name: "Test Survey",
|
name: "Test Survey",
|
||||||
questions: [],
|
questions: [],
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
environmentId: "env1",
|
environmentId: TEST_IDS.environment,
|
||||||
status: "draft",
|
status: "draft",
|
||||||
} as unknown as TSurvey;
|
} as unknown as TSurvey;
|
||||||
|
|
||||||
@@ -272,7 +283,7 @@ describe("surveys", () => {
|
|||||||
|
|
||||||
describe("getFormattedFilters", () => {
|
describe("getFormattedFilters", () => {
|
||||||
const survey = {
|
const survey = {
|
||||||
id: "survey1",
|
id: TEST_IDS.survey,
|
||||||
name: "Test Survey",
|
name: "Test Survey",
|
||||||
questions: [
|
questions: [
|
||||||
{
|
{
|
||||||
@@ -346,7 +357,7 @@ describe("surveys", () => {
|
|||||||
],
|
],
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
environmentId: "env1",
|
environmentId: TEST_IDS.environment,
|
||||||
status: "draft",
|
status: "draft",
|
||||||
} as unknown as TSurvey;
|
} as unknown as TSurvey;
|
||||||
|
|
||||||
|
|||||||
525
apps/web/lib/testing/README.md
Normal file
525
apps/web/lib/testing/README.md
Normal file
@@ -0,0 +1,525 @@
|
|||||||
|
# Testing Utilities — Tutorial
|
||||||
|
|
||||||
|
Practical utilities to write cleaner, faster, more consistent unit tests.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { describe, expect, test, vi } from "vitest";
|
||||||
|
// NOW import modules that depend on mocks
|
||||||
|
import { prisma } from "@formbricks/database";
|
||||||
|
import { FIXTURES, TEST_IDS } from "@/lib/testing/constants";
|
||||||
|
// ⚠️ CRITICAL: Setup ALL mocks BEFORE importing modules that use them
|
||||||
|
import { COMMON_ERRORS, createContactsMocks } from "@/lib/testing/mocks";
|
||||||
|
import { setupTestEnvironment } from "@/lib/testing/setup";
|
||||||
|
import { getContact } from "./contacts";
|
||||||
|
|
||||||
|
vi.mock("@formbricks/database", () => createContactsMocks());
|
||||||
|
vi.mock("@/lib/utils/validate", () => ({
|
||||||
|
validateInputs: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
setupTestEnvironment();
|
||||||
|
|
||||||
|
describe("ContactService", () => {
|
||||||
|
test("should find a contact", async () => {
|
||||||
|
vi.mocked(prisma.contact.findUnique).mockResolvedValue(FIXTURES.contact);
|
||||||
|
|
||||||
|
const result = await getContact(TEST_IDS.contact);
|
||||||
|
|
||||||
|
expect(result).toEqual(FIXTURES.contact);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Critical Setup Rules ⚠️
|
||||||
|
|
||||||
|
### Rule 1: Mock Order is Everything
|
||||||
|
|
||||||
|
**Vitest requires all `vi.mock()` calls to happen BEFORE any imports that use the mocked modules.**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ WRONG - will fail with "prisma is not defined"
|
||||||
|
import { prisma } from "@formbricks/database";
|
||||||
|
|
||||||
|
vi.mock("@formbricks/database", () => createContactsMocks());
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ CORRECT - setup mocks first
|
||||||
|
// THEN import modules that depend on the mock
|
||||||
|
import { prisma } from "@formbricks/database";
|
||||||
|
import { createContactsMocks } from "@/lib/testing/mocks";
|
||||||
|
|
||||||
|
vi.mock("@formbricks/database", () => createContactsMocks());
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rule 2: Mock All External Dependencies
|
||||||
|
|
||||||
|
Don't forget to mock functions that are called by your tested code:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Mock validateInputs if it's called by the function you're testing
|
||||||
|
vi.mock("@/lib/utils/validate", () => ({
|
||||||
|
validateInputs: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Set up a default behavior
|
||||||
|
vi.mocked(validateInputs).mockImplementation(() => []);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rule 3: Fixtures Must Match Real Data Structures
|
||||||
|
|
||||||
|
Test fixtures should match the exact structure expected by your code:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ INCOMPLETE - will fail when code tries to access attributes
|
||||||
|
const contact = {
|
||||||
|
id: TEST_IDS.contact,
|
||||||
|
email: "test@example.com",
|
||||||
|
userId: TEST_IDS.user,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ✅ COMPLETE - matches what transformPrismaContact expects
|
||||||
|
const contact = {
|
||||||
|
id: TEST_IDS.contact,
|
||||||
|
environmentId: TEST_IDS.environment,
|
||||||
|
userId: TEST_IDS.user,
|
||||||
|
createdAt: new Date("2024-01-01"),
|
||||||
|
updatedAt: new Date("2024-01-02"),
|
||||||
|
attributes: [
|
||||||
|
{ value: "test@example.com", attributeKey: { key: "email", name: "Email" } },
|
||||||
|
{ value: TEST_IDS.user, attributeKey: { key: "userId", name: "User ID" } },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Concept 1: TEST_IDs — Use Constants, Not Magic Strings
|
||||||
|
|
||||||
|
### The Problem
|
||||||
|
|
||||||
|
Scattered magic strings make tests hard to maintain:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Don't do this
|
||||||
|
describe("getContact", () => {
|
||||||
|
test("should find contact", async () => {
|
||||||
|
const contactId = "contact-123";
|
||||||
|
const userId = "user-456";
|
||||||
|
const environmentId = "env-789";
|
||||||
|
|
||||||
|
const result = await getContact(contactId);
|
||||||
|
expect(result.userId).toBe(userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle missing contact", async () => {
|
||||||
|
const contactId = "contact-123"; // Same ID, defined again
|
||||||
|
await expect(getContact(contactId)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### The Solution
|
||||||
|
|
||||||
|
Use TEST_IDs for consistent, reusable identifiers:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Do this
|
||||||
|
import { TEST_IDS } from "@/lib/testing/constants";
|
||||||
|
|
||||||
|
describe("getContact", () => {
|
||||||
|
test("should find contact", async () => {
|
||||||
|
const result = await getContact(TEST_IDS.contact);
|
||||||
|
expect(result.userId).toBe(TEST_IDS.user);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle missing contact", async () => {
|
||||||
|
await expect(getContact(TEST_IDS.contact)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Available IDs:**
|
||||||
|
|
||||||
|
```
|
||||||
|
TEST_IDS.contact, contactAlt, user, environment, survey, organization, quota,
|
||||||
|
attribute, response, team, project, segment, webhook, apiKey, membership
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Concept 2: FIXTURES — Use Pre-built Test Data
|
||||||
|
|
||||||
|
### The Problem
|
||||||
|
|
||||||
|
Duplicated mock data across tests:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Don't do this
|
||||||
|
describe("ContactService", () => {
|
||||||
|
test("should validate contact email", async () => {
|
||||||
|
const contact = {
|
||||||
|
id: "contact-1",
|
||||||
|
email: "test@example.com",
|
||||||
|
userId: "user-1",
|
||||||
|
environmentId: "env-1",
|
||||||
|
createdAt: new Date("2024-01-01"),
|
||||||
|
};
|
||||||
|
expect(isValidEmail(contact.email)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should create contact from data", async () => {
|
||||||
|
const contact = {
|
||||||
|
id: "contact-1",
|
||||||
|
email: "test@example.com",
|
||||||
|
userId: "user-1",
|
||||||
|
environmentId: "env-1",
|
||||||
|
createdAt: new Date("2024-01-01"),
|
||||||
|
};
|
||||||
|
const result = await createContact(contact);
|
||||||
|
expect(result).toEqual(contact);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### The Solution
|
||||||
|
|
||||||
|
Use FIXTURES for consistent test data:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Do this
|
||||||
|
import { FIXTURES } from "@/lib/testing/constants";
|
||||||
|
|
||||||
|
describe("ContactService", () => {
|
||||||
|
test("should validate contact email", async () => {
|
||||||
|
expect(isValidEmail(FIXTURES.contact.email)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should create contact from data", async () => {
|
||||||
|
const result = await createContact(FIXTURES.contact);
|
||||||
|
expect(result).toEqual(FIXTURES.contact);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Available fixtures:** contact, survey, attributeKey, environment, organization, project, team, user, response
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Concept 3: setupTestEnvironment — Standard Cleanup
|
||||||
|
|
||||||
|
### The Problem
|
||||||
|
|
||||||
|
Inconsistent beforeEach/afterEach patterns across tests:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Don't do this
|
||||||
|
describe("module A", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
// tests...
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("module B", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
vi.resetModules();
|
||||||
|
});
|
||||||
|
// tests...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### The Solution
|
||||||
|
|
||||||
|
Use setupTestEnvironment() for consistent cleanup:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Do this
|
||||||
|
import { setupTestEnvironment } from "@/lib/testing/setup";
|
||||||
|
|
||||||
|
setupTestEnvironment();
|
||||||
|
|
||||||
|
describe("module", () => {
|
||||||
|
test("should work", () => {
|
||||||
|
// Cleanup is automatic
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
|
||||||
|
- Clears all mocks before and after each test
|
||||||
|
- Provides consistent test isolation
|
||||||
|
- One line replaces repetitive setup code
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Concept 4: Mock Factories — Reduce Mock Setup from 40+ Lines to 1
|
||||||
|
|
||||||
|
### The Problem
|
||||||
|
|
||||||
|
Massive repetitive mock setup:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Don't do this (40+ lines)
|
||||||
|
vi.mock("@formbricks/database", () => ({
|
||||||
|
prisma: {
|
||||||
|
contact: {
|
||||||
|
findMany: vi.fn(),
|
||||||
|
findUnique: vi.fn(),
|
||||||
|
delete: vi.fn(),
|
||||||
|
create: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
},
|
||||||
|
contactAttribute: {
|
||||||
|
findMany: vi.fn(),
|
||||||
|
deleteMany: vi.fn(),
|
||||||
|
upsert: vi.fn(),
|
||||||
|
create: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
},
|
||||||
|
contactAttributeKey: {
|
||||||
|
findMany: vi.fn(),
|
||||||
|
createMany: vi.fn(),
|
||||||
|
create: vi.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
### The Solution
|
||||||
|
|
||||||
|
Use mock factories:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Do this (1 line)
|
||||||
|
import { createContactsMocks } from "@/lib/testing/mocks";
|
||||||
|
|
||||||
|
vi.mock("@formbricks/database", () => createContactsMocks());
|
||||||
|
```
|
||||||
|
|
||||||
|
**Available factories:**
|
||||||
|
|
||||||
|
- `createContactsMocks()` — Contact operations (contact, contactAttribute, contactAttributeKey)
|
||||||
|
- `createQuotasMocks()` — Quota operations
|
||||||
|
- `createSurveysMocks()` — Survey and response operations
|
||||||
|
|
||||||
|
### Error Testing with Mock Factories
|
||||||
|
|
||||||
|
**Use COMMON_ERRORS for standardized error tests:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Don't do this (10+ lines per error)
|
||||||
|
const error = new Prisma.PrismaClientKnownRequestError("Not found", {
|
||||||
|
code: "P2025",
|
||||||
|
clientVersion: "5.0.0",
|
||||||
|
});
|
||||||
|
vi.mocked(prisma.contact.findUnique).mockRejectedValue(error);
|
||||||
|
|
||||||
|
await expect(getContact("invalid")).rejects.toThrow();
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Do this (1 line)
|
||||||
|
import { COMMON_ERRORS } from "@/lib/testing/mocks";
|
||||||
|
|
||||||
|
vi.mocked(prisma.contact.findUnique).mockRejectedValue(COMMON_ERRORS.RECORD_NOT_FOUND);
|
||||||
|
|
||||||
|
await expect(getContact("invalid")).rejects.toThrow();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Available errors:**
|
||||||
|
|
||||||
|
```
|
||||||
|
COMMON_ERRORS.UNIQUE_CONSTRAINT // P2002
|
||||||
|
COMMON_ERRORS.RECORD_NOT_FOUND // P2025
|
||||||
|
COMMON_ERRORS.FOREIGN_KEY // P2003
|
||||||
|
COMMON_ERRORS.REQUIRED_RELATION // P2014
|
||||||
|
COMMON_ERRORS.DATABASE_ERROR // P5000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transaction Testing with Mock Factories
|
||||||
|
|
||||||
|
**Use createMockTransaction() for complex database transactions:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Don't do this (25+ lines)
|
||||||
|
vi.mock("@formbricks/database", () => ({
|
||||||
|
prisma: {
|
||||||
|
$transaction: vi.fn(async (cb) => {
|
||||||
|
return cb({
|
||||||
|
responseQuotaLink: {
|
||||||
|
deleteMany: vi.fn(),
|
||||||
|
createMany: vi.fn(),
|
||||||
|
updateMany: vi.fn(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Do this (3 lines)
|
||||||
|
import { createMockTransaction, mockPrismaTransaction } from "@/lib/testing/mocks";
|
||||||
|
|
||||||
|
const mockTx = createMockTransaction({
|
||||||
|
responseQuotaLink: ["deleteMany", "createMany", "updateMany"],
|
||||||
|
});
|
||||||
|
vi.mocked(prisma.$transaction) = mockPrismaTransaction(mockTx);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Real-World Example: Efficient Test Suite
|
||||||
|
|
||||||
|
Here's how the utilities work together to write clean, efficient tests:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
import { vi } from "vitest";
|
||||||
|
import { FIXTURES, TEST_IDS } from "@/lib/testing/constants";
|
||||||
|
import { COMMON_ERRORS, createContactsMocks } from "@/lib/testing/mocks";
|
||||||
|
import { setupTestEnvironment } from "@/lib/testing/setup";
|
||||||
|
|
||||||
|
setupTestEnvironment();
|
||||||
|
vi.mock("@formbricks/database", () => createContactsMocks());
|
||||||
|
|
||||||
|
describe("ContactService", () => {
|
||||||
|
describe("getContact", () => {
|
||||||
|
test("should fetch contact successfully", async () => {
|
||||||
|
vi.mocked(prisma.contact.findUnique).mockResolvedValue(FIXTURES.contact);
|
||||||
|
|
||||||
|
const result = await getContact(TEST_IDS.contact);
|
||||||
|
|
||||||
|
expect(result).toEqual(FIXTURES.contact);
|
||||||
|
expect(prisma.contact.findUnique).toHaveBeenCalledWith({
|
||||||
|
where: { id: TEST_IDS.contact },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle contact not found", async () => {
|
||||||
|
vi.mocked(prisma.contact.findUnique).mockRejectedValue(COMMON_ERRORS.RECORD_NOT_FOUND);
|
||||||
|
|
||||||
|
await expect(getContact(TEST_IDS.contact)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createContact", () => {
|
||||||
|
test("should create contact with valid data", async () => {
|
||||||
|
vi.mocked(prisma.contact.create).mockResolvedValue(FIXTURES.contact);
|
||||||
|
|
||||||
|
const result = await createContact({
|
||||||
|
email: FIXTURES.contact.email,
|
||||||
|
environmentId: TEST_IDS.environment,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual(FIXTURES.contact);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should reject duplicate email", async () => {
|
||||||
|
vi.mocked(prisma.contact.create).mockRejectedValue(COMMON_ERRORS.UNIQUE_CONSTRAINT);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
createContact({ email: "duplicate@test.com", environmentId: TEST_IDS.environment })
|
||||||
|
).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("deleteContact", () => {
|
||||||
|
test("should delete contact and return void", async () => {
|
||||||
|
vi.mocked(prisma.contact.delete).mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await deleteContact(TEST_IDS.contact);
|
||||||
|
|
||||||
|
expect(prisma.contact.delete).toHaveBeenCalledWith({
|
||||||
|
where: { id: TEST_IDS.contact },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to Use — Import Options
|
||||||
|
|
||||||
|
### Option 1: From vitestSetup (Recommended)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { COMMON_ERRORS, FIXTURES, TEST_IDS, createContactsMocks, setupTestEnvironment } from "@/vitestSetup";
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Direct Imports
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { FIXTURES, TEST_IDS } from "@/lib/testing/constants";
|
||||||
|
import { COMMON_ERRORS, createContactsMocks } from "@/lib/testing/mocks";
|
||||||
|
import { setupTestEnvironment } from "@/lib/testing/setup";
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/web/lib/testing/
|
||||||
|
├── constants.ts — TEST_IDS & FIXTURES
|
||||||
|
├── setup.ts — setupTestEnvironment()
|
||||||
|
└── mocks/ — Mock factories & error utilities
|
||||||
|
├── database.ts — createContactsMocks(), etc.
|
||||||
|
├── errors.ts — COMMON_ERRORS, error factories
|
||||||
|
├── transactions.ts — Transaction helpers
|
||||||
|
└── index.ts — Exports everything
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary: What Each Concept Solves
|
||||||
|
|
||||||
|
| Concept | Problem | Solution |
|
||||||
|
| -------------------------- | ---------------------------------------- | --------------------------- |
|
||||||
|
| **TEST_IDs** | Magic strings scattered everywhere | One constant per concept |
|
||||||
|
| **FIXTURES** | Duplicate test data in every test | Pre-built, reusable objects |
|
||||||
|
| **setupTestEnvironment()** | Inconsistent cleanup patterns | One standard setup |
|
||||||
|
| **Mock Factories** | 20-40 lines of boilerplate per test file | 1 line mock setup |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Do's and Don'ts
|
||||||
|
|
||||||
|
### ✅ Do's
|
||||||
|
|
||||||
|
- Use `TEST_IDS.*` instead of hardcoded strings
|
||||||
|
- Use `FIXTURES.*` for standard test objects
|
||||||
|
- Call `setupTestEnvironment()` at the top of your test file
|
||||||
|
- Use `createContactsMocks()` instead of manually mocking prisma
|
||||||
|
- Use `COMMON_ERRORS.*` for standard error scenarios
|
||||||
|
- Import utilities from `@/vitestSetup` for convenience
|
||||||
|
|
||||||
|
### ❌ Don'ts
|
||||||
|
|
||||||
|
- Don't create magic string IDs in tests
|
||||||
|
- Don't duplicate fixture objects across tests
|
||||||
|
- Don't manually write beforeEach/afterEach cleanup
|
||||||
|
- Don't manually construct Prisma error objects
|
||||||
|
- Don't duplicate long mock setup code
|
||||||
|
- Don't create custom mock structures when factories exist
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Need More Help?
|
||||||
|
|
||||||
|
- **Mock Factories** → See `mocks/database.ts`, `mocks/errors.ts`, `mocks/transactions.ts`
|
||||||
|
- **All Available Fixtures** → See `constants.ts`
|
||||||
|
- **Error Codes** → See `mocks/errors.ts` for all COMMON_ERRORS
|
||||||
|
- **Mock Setup Pattern** → Review `apps/web/modules/ee/contacts/lib/contacts.test.ts` for a complete example
|
||||||
126
apps/web/lib/testing/constants.ts
Normal file
126
apps/web/lib/testing/constants.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard test IDs to eliminate magic strings across test files.
|
||||||
|
* Use these constants instead of hardcoded IDs like "contact-1", "env-123", etc.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { TEST_IDS } from "@/lib/testing/constants";
|
||||||
|
*
|
||||||
|
* test("should fetch contact", async () => {
|
||||||
|
* const result = await getContact(TEST_IDS.contact);
|
||||||
|
* expect(result).toBeDefined();
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const TEST_IDS = {
|
||||||
|
contact: "contact-123",
|
||||||
|
contactAlt: "contact-456",
|
||||||
|
user: "user-123",
|
||||||
|
environment: "env-123",
|
||||||
|
survey: "survey-123",
|
||||||
|
organization: "org-123",
|
||||||
|
quota: "quota-123",
|
||||||
|
attribute: "attr-123",
|
||||||
|
response: "response-123",
|
||||||
|
team: "team-123",
|
||||||
|
project: "project-123",
|
||||||
|
segment: "segment-123",
|
||||||
|
webhook: "webhook-123",
|
||||||
|
apiKey: "api-key-123",
|
||||||
|
membership: "membership-123",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common test fixtures to reduce duplicate test data definitions.
|
||||||
|
* Extend these as needed for your specific test cases.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { FIXTURES } from "@/lib/testing/constants";
|
||||||
|
*
|
||||||
|
* test("should create contact", async () => {
|
||||||
|
* vi.mocked(getContactAttributeKeys).mockResolvedValue(FIXTURES.attributeKeys);
|
||||||
|
* const result = await createContact(FIXTURES.contact);
|
||||||
|
* expect(result.email).toBe(FIXTURES.contact.email);
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const FIXTURES = {
|
||||||
|
contact: {
|
||||||
|
id: TEST_IDS.contact,
|
||||||
|
environmentId: TEST_IDS.environment,
|
||||||
|
userId: TEST_IDS.user,
|
||||||
|
createdAt: new Date("2024-01-01"),
|
||||||
|
updatedAt: new Date("2024-01-02"),
|
||||||
|
attributes: [
|
||||||
|
{ value: "test@example.com", attributeKey: { key: "email", name: "Email" } },
|
||||||
|
{ value: TEST_IDS.user, attributeKey: { key: "userId", name: "User ID" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
survey: {
|
||||||
|
id: TEST_IDS.survey,
|
||||||
|
name: "Test Survey",
|
||||||
|
environmentId: TEST_IDS.environment,
|
||||||
|
},
|
||||||
|
|
||||||
|
attributeKey: {
|
||||||
|
id: TEST_IDS.attribute,
|
||||||
|
key: "email",
|
||||||
|
name: "Email",
|
||||||
|
environmentId: TEST_IDS.environment,
|
||||||
|
createdAt: new Date("2024-01-01"),
|
||||||
|
updatedAt: new Date("2024-01-02"),
|
||||||
|
isUnique: false,
|
||||||
|
description: null,
|
||||||
|
type: "default" as const,
|
||||||
|
},
|
||||||
|
|
||||||
|
attributeKeys: [
|
||||||
|
{
|
||||||
|
id: "key-1",
|
||||||
|
key: "email",
|
||||||
|
name: "Email",
|
||||||
|
environmentId: TEST_IDS.environment,
|
||||||
|
createdAt: new Date("2024-01-01"),
|
||||||
|
updatedAt: new Date("2024-01-02"),
|
||||||
|
isUnique: false,
|
||||||
|
description: null,
|
||||||
|
type: "default",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "key-2",
|
||||||
|
key: "name",
|
||||||
|
name: "Name",
|
||||||
|
environmentId: TEST_IDS.environment,
|
||||||
|
createdAt: new Date("2024-01-01"),
|
||||||
|
updatedAt: new Date("2024-01-02"),
|
||||||
|
isUnique: false,
|
||||||
|
description: null,
|
||||||
|
type: "default",
|
||||||
|
},
|
||||||
|
] as TContactAttributeKey[],
|
||||||
|
|
||||||
|
responseData: {
|
||||||
|
q1: "Open text answer",
|
||||||
|
q2: "Option 1",
|
||||||
|
},
|
||||||
|
|
||||||
|
environment: {
|
||||||
|
id: TEST_IDS.environment,
|
||||||
|
name: "Test Environment",
|
||||||
|
type: "development" as const,
|
||||||
|
},
|
||||||
|
|
||||||
|
organization: {
|
||||||
|
id: TEST_IDS.organization,
|
||||||
|
name: "Test Organization",
|
||||||
|
},
|
||||||
|
|
||||||
|
project: {
|
||||||
|
id: TEST_IDS.project,
|
||||||
|
name: "Test Project",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
299
apps/web/lib/testing/mocks/README.md
Normal file
299
apps/web/lib/testing/mocks/README.md
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
# Mock Factories & Error Utilities
|
||||||
|
|
||||||
|
Centralized mock factories and error utilities to eliminate 150+ redundant mock setups and standardize error testing across test files.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Database Mocks
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createContactsMocks, COMMON_ERRORS } from "@/lib/testing/mocks";
|
||||||
|
import { vi } from "vitest";
|
||||||
|
|
||||||
|
// Setup contacts mocks (replaces 30+ lines)
|
||||||
|
vi.mock("@formbricks/database", () => createContactsMocks());
|
||||||
|
|
||||||
|
describe("ContactService", () => {
|
||||||
|
test("handles not found error", async () => {
|
||||||
|
vi.mocked(prisma.contact.findUnique).mockRejectedValue(COMMON_ERRORS.RECORD_NOT_FOUND);
|
||||||
|
|
||||||
|
await expect(getContact("id")).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transaction Mocks
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createMockTransaction, mockPrismaTransaction } from "@/lib/testing/mocks";
|
||||||
|
|
||||||
|
const mockTx = createMockTransaction({
|
||||||
|
responseQuotaLink: ["deleteMany", "createMany", "updateMany", "count", "groupBy"],
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mocked(prisma.$transaction) = mockPrismaTransaction(mockTx);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Testing
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createPrismaError, COMMON_ERRORS, MockValidationError } from "@/lib/testing/mocks";
|
||||||
|
|
||||||
|
// Use pre-built errors
|
||||||
|
vi.mocked(fn).mockRejectedValue(COMMON_ERRORS.UNIQUE_CONSTRAINT);
|
||||||
|
|
||||||
|
// Or create custom errors
|
||||||
|
vi.mocked(fn).mockRejectedValue(createPrismaError("P2002", "Email already exists"));
|
||||||
|
|
||||||
|
// Or use Formbricks domain errors
|
||||||
|
vi.mocked(fn).mockRejectedValue(new MockNotFoundError("Contact"));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Utilities
|
||||||
|
|
||||||
|
### Database Mocks
|
||||||
|
|
||||||
|
#### `createContactsMocks()`
|
||||||
|
Complete mock setup for contact operations.
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```typescript
|
||||||
|
vi.mock("@formbricks/database", () => ({
|
||||||
|
prisma: {
|
||||||
|
contact: {
|
||||||
|
findMany: vi.fn(),
|
||||||
|
findUnique: vi.fn(),
|
||||||
|
delete: vi.fn(),
|
||||||
|
create: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
deleteMany: vi.fn(),
|
||||||
|
updateMany: vi.fn(),
|
||||||
|
},
|
||||||
|
contactAttribute: {
|
||||||
|
findMany: vi.fn(),
|
||||||
|
deleteMany: vi.fn(),
|
||||||
|
upsert: vi.fn(),
|
||||||
|
// ... 10+ more methods
|
||||||
|
},
|
||||||
|
contactAttributeKey: {
|
||||||
|
// ... 6+ methods
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```typescript
|
||||||
|
import { createContactsMocks } from "@/lib/testing/mocks";
|
||||||
|
|
||||||
|
vi.mock("@formbricks/database", () => createContactsMocks());
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `createQuotasMocks()`
|
||||||
|
Complete mock setup for quota operations with transactions.
|
||||||
|
|
||||||
|
#### `createSurveysMocks()`
|
||||||
|
Complete mock setup for survey and response operations.
|
||||||
|
|
||||||
|
#### Individual Mock Methods
|
||||||
|
If you need more control, use individual mock method factories:
|
||||||
|
- `mockContactMethods()`
|
||||||
|
- `mockContactAttributeMethods()`
|
||||||
|
- `mockContactAttributeKeyMethods()`
|
||||||
|
- `mockResponseQuotaLinkMethods()`
|
||||||
|
- `mockSurveyMethods()`
|
||||||
|
- `mockResponseMethods()`
|
||||||
|
|
||||||
|
### Error Utilities
|
||||||
|
|
||||||
|
#### `createPrismaError(code, message?)`
|
||||||
|
Factory to create Prisma errors with specific codes.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createPrismaError } from "@/lib/testing/mocks";
|
||||||
|
|
||||||
|
vi.mocked(prisma.contact.create).mockRejectedValue(
|
||||||
|
createPrismaError("P2002", "Email already exists")
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common Prisma Error Codes:**
|
||||||
|
- `P2002` - Unique constraint violation
|
||||||
|
- `P2025` - Record not found
|
||||||
|
- `P2003` - Foreign key constraint
|
||||||
|
- `P2014` - Required relation violation
|
||||||
|
|
||||||
|
#### `COMMON_ERRORS`
|
||||||
|
Pre-built common error instances for convenience.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { COMMON_ERRORS } from "@/lib/testing/mocks";
|
||||||
|
|
||||||
|
// Available:
|
||||||
|
// COMMON_ERRORS.UNIQUE_CONSTRAINT
|
||||||
|
// COMMON_ERRORS.RECORD_NOT_FOUND
|
||||||
|
// COMMON_ERRORS.FOREIGN_KEY
|
||||||
|
// COMMON_ERRORS.REQUIRED_RELATION
|
||||||
|
// COMMON_ERRORS.DATABASE_ERROR
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Domain Error Classes
|
||||||
|
Mock implementations of Formbricks domain errors:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
MockValidationError,
|
||||||
|
MockDatabaseError,
|
||||||
|
MockNotFoundError,
|
||||||
|
MockAuthorizationError,
|
||||||
|
} from "@/lib/testing/mocks";
|
||||||
|
|
||||||
|
vi.mocked(validateInputs).mockRejectedValue(new MockValidationError("Invalid email"));
|
||||||
|
vi.mocked(getContact).mockRejectedValue(new MockNotFoundError("Contact"));
|
||||||
|
vi.mocked(updateContact).mockRejectedValue(new MockAuthorizationError());
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transaction Mocks
|
||||||
|
|
||||||
|
#### `createMockTransaction(structure)`
|
||||||
|
Dynamically create transaction mock objects.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createMockTransaction } from "@/lib/testing/mocks";
|
||||||
|
|
||||||
|
const mockTx = createMockTransaction({
|
||||||
|
responseQuotaLink: ["deleteMany", "createMany", "updateMany"],
|
||||||
|
contact: ["findMany", "create"],
|
||||||
|
response: ["count"],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Now you have:
|
||||||
|
// mockTx.responseQuotaLink.deleteMany, mockTx.responseQuotaLink.createMany, etc.
|
||||||
|
// mockTx.contact.findMany, mockTx.contact.create, etc.
|
||||||
|
// mockTx.response.count, etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `mockPrismaTransaction(mockTx)`
|
||||||
|
Wrap transaction mock for use with `prisma.$transaction`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createMockTransaction, mockPrismaTransaction } from "@/lib/testing/mocks";
|
||||||
|
|
||||||
|
const mockTx = createMockTransaction({
|
||||||
|
responseQuotaLink: ["deleteMany", "createMany"],
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mocked(prisma.$transaction) = mockPrismaTransaction(mockTx);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Pre-configured Mocks
|
||||||
|
Ready-to-use transaction mocks:
|
||||||
|
- `quotaTransactionMock` - For quota operations
|
||||||
|
- `contactTransactionMock` - For contact operations
|
||||||
|
- `responseTransactionMock` - For response operations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { quotaTransactionMock, mockPrismaTransaction } from "@/lib/testing/mocks";
|
||||||
|
|
||||||
|
vi.mocked(prisma.$transaction) = mockPrismaTransaction(quotaTransactionMock);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `sequenceTransactionMocks(txMocks[])`
|
||||||
|
Handle multiple sequential transaction calls with different structures.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createMockTransaction, sequenceTransactionMocks } from "@/lib/testing/mocks";
|
||||||
|
|
||||||
|
const tx1 = createMockTransaction({ contact: ["findMany"] });
|
||||||
|
const tx2 = createMockTransaction({ response: ["count"] });
|
||||||
|
|
||||||
|
vi.mocked(prisma.$transaction) = sequenceTransactionMocks([tx1, tx2]);
|
||||||
|
|
||||||
|
// First $transaction call gets tx1, second call gets tx2
|
||||||
|
```
|
||||||
|
|
||||||
|
## Impact Summary
|
||||||
|
|
||||||
|
- **Duplicate Mock Setups:** 150+ reduced to 1 line
|
||||||
|
- **Error Testing:** 100+ test cases standardized
|
||||||
|
- **Transaction Mocks:** 15+ complex setups simplified
|
||||||
|
- **Test Readability:** 40-50% cleaner test code
|
||||||
|
- **Setup Time:** 90% reduction for database tests
|
||||||
|
|
||||||
|
## Migration Example
|
||||||
|
|
||||||
|
### Before (40+ lines)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import { prisma } from "@formbricks/database";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
|
vi.mock("@formbricks/database", () => ({
|
||||||
|
prisma: {
|
||||||
|
$transaction: vi.fn(),
|
||||||
|
responseQuotaLink: {
|
||||||
|
deleteMany: vi.fn(),
|
||||||
|
createMany: vi.fn(),
|
||||||
|
updateMany: vi.fn(),
|
||||||
|
count: vi.fn(),
|
||||||
|
groupBy: vi.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("QuotaService", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles quota not found", async () => {
|
||||||
|
const error = new Prisma.PrismaClientKnownRequestError("Not found", {
|
||||||
|
code: "P2025",
|
||||||
|
clientVersion: "5.0.0",
|
||||||
|
});
|
||||||
|
vi.mocked(prisma.responseQuotaLink.count).mockRejectedValue(error);
|
||||||
|
|
||||||
|
await expect(getQuota("id")).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### After (20 lines)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
import { setupTestEnvironment } from "@/lib/testing/setup";
|
||||||
|
import { createQuotasMocks, COMMON_ERRORS } from "@/lib/testing/mocks";
|
||||||
|
import { vi } from "vitest";
|
||||||
|
|
||||||
|
setupTestEnvironment();
|
||||||
|
vi.mock("@formbricks/database", () => createQuotasMocks());
|
||||||
|
|
||||||
|
describe("QuotaService", () => {
|
||||||
|
test("handles quota not found", async () => {
|
||||||
|
vi.mocked(prisma.responseQuotaLink.count).mockRejectedValue(COMMON_ERRORS.RECORD_NOT_FOUND);
|
||||||
|
|
||||||
|
await expect(getQuota("id")).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
✅ 50% reduction in mock setup code
|
||||||
|
✅ Standardized error testing across files
|
||||||
|
✅ Easier test maintenance
|
||||||
|
✅ Better test readability
|
||||||
|
✅ Consistent patterns across the codebase
|
||||||
|
✅ Less boilerplate per test file
|
||||||
|
|
||||||
|
## What's Next?
|
||||||
|
|
||||||
|
Phase 3 will introduce:
|
||||||
|
- Custom Vitest matchers for consistent assertions
|
||||||
|
- Comprehensive testing standards documentation
|
||||||
|
- Team training materials
|
||||||
|
|
||||||
|
See the main testing analysis documents in the repository root for the full roadmap.
|
||||||
|
|
||||||
134
apps/web/lib/testing/mocks/database.ts
Normal file
134
apps/web/lib/testing/mocks/database.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import { vi } from "vitest";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock methods for contact operations.
|
||||||
|
* Used to mock prisma.contact in database operations.
|
||||||
|
*/
|
||||||
|
export const mockContactMethods = () => ({
|
||||||
|
findMany: vi.fn(),
|
||||||
|
findUnique: vi.fn(),
|
||||||
|
delete: vi.fn(),
|
||||||
|
create: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
deleteMany: vi.fn(),
|
||||||
|
updateMany: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock methods for contact attribute operations.
|
||||||
|
* Used to mock prisma.contactAttribute in database operations.
|
||||||
|
*/
|
||||||
|
export const mockContactAttributeMethods = () => ({
|
||||||
|
findMany: vi.fn(),
|
||||||
|
deleteMany: vi.fn(),
|
||||||
|
upsert: vi.fn(),
|
||||||
|
create: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
delete: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock methods for contact attribute key operations.
|
||||||
|
* Used to mock prisma.contactAttributeKey in database operations.
|
||||||
|
*/
|
||||||
|
export const mockContactAttributeKeyMethods = () => ({
|
||||||
|
findMany: vi.fn(),
|
||||||
|
createMany: vi.fn(),
|
||||||
|
create: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
delete: vi.fn(),
|
||||||
|
findUnique: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock methods for response quota link operations.
|
||||||
|
* Used to mock prisma.responseQuotaLink in database operations.
|
||||||
|
*/
|
||||||
|
export const mockResponseQuotaLinkMethods = () => ({
|
||||||
|
deleteMany: vi.fn(),
|
||||||
|
createMany: vi.fn(),
|
||||||
|
updateMany: vi.fn(),
|
||||||
|
count: vi.fn(),
|
||||||
|
groupBy: vi.fn(),
|
||||||
|
findMany: vi.fn(),
|
||||||
|
create: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete mock setup for contacts module.
|
||||||
|
* Reduces 20-30 lines of mock setup per test file to 1 line.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { createContactsMocks } from "@/lib/testing/mocks";
|
||||||
|
* import { vi } from "vitest";
|
||||||
|
*
|
||||||
|
* vi.mock("@formbricks/database", () => createContactsMocks());
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function createContactsMocks() {
|
||||||
|
return {
|
||||||
|
prisma: {
|
||||||
|
contact: mockContactMethods(),
|
||||||
|
contactAttribute: mockContactAttributeMethods(),
|
||||||
|
contactAttributeKey: mockContactAttributeKeyMethods(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete mock setup for quotas module.
|
||||||
|
* Reduces 30-40 lines of mock setup per test file to 1 line.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { createQuotasMocks } from "@/lib/testing/mocks";
|
||||||
|
* import { vi } from "vitest";
|
||||||
|
*
|
||||||
|
* vi.mock("@formbricks/database", () => createQuotasMocks());
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function createQuotasMocks() {
|
||||||
|
return {
|
||||||
|
prisma: {
|
||||||
|
$transaction: vi.fn(),
|
||||||
|
responseQuotaLink: mockResponseQuotaLinkMethods(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock methods for survey operations.
|
||||||
|
*/
|
||||||
|
export const mockSurveyMethods = () => ({
|
||||||
|
findMany: vi.fn(),
|
||||||
|
findUnique: vi.fn(),
|
||||||
|
create: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
delete: vi.fn(),
|
||||||
|
deleteMany: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock methods for response operations.
|
||||||
|
*/
|
||||||
|
export const mockResponseMethods = () => ({
|
||||||
|
findMany: vi.fn(),
|
||||||
|
findUnique: vi.fn(),
|
||||||
|
create: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
delete: vi.fn(),
|
||||||
|
count: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete mock setup for surveys module.
|
||||||
|
*/
|
||||||
|
export function createSurveysMocks() {
|
||||||
|
return {
|
||||||
|
prisma: {
|
||||||
|
survey: mockSurveyMethods(),
|
||||||
|
response: mockResponseMethods(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
102
apps/web/lib/testing/mocks/errors.ts
Normal file
102
apps/web/lib/testing/mocks/errors.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory function to create Prisma errors with a specific error code and message.
|
||||||
|
* Eliminates 100+ lines of repetitive Prisma error setup across test files.
|
||||||
|
*
|
||||||
|
* @param code - The Prisma error code (e.g., "P2002", "P2025")
|
||||||
|
* @param message - Optional error message (defaults to "Database error")
|
||||||
|
* @returns A PrismaClientKnownRequestError instance
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { createPrismaError } from "@/lib/testing/mocks";
|
||||||
|
*
|
||||||
|
* vi.mocked(prisma.contact.findMany).mockRejectedValue(
|
||||||
|
* createPrismaError("P2002", "Unique constraint failed")
|
||||||
|
* );
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function createPrismaError(code: string, message = "Database error") {
|
||||||
|
return new Prisma.PrismaClientKnownRequestError(message, {
|
||||||
|
code,
|
||||||
|
clientVersion: "5.0.0",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-built common Prisma errors for convenience.
|
||||||
|
* Use these instead of creating errors manually every time.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { COMMON_ERRORS } from "@/lib/testing/mocks";
|
||||||
|
*
|
||||||
|
* vi.mocked(prisma.contact.findUnique).mockRejectedValue(
|
||||||
|
* COMMON_ERRORS.RECORD_NOT_FOUND
|
||||||
|
* );
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const COMMON_ERRORS = {
|
||||||
|
// P2002: Unique constraint failed
|
||||||
|
UNIQUE_CONSTRAINT: createPrismaError("P2002", "Unique constraint violation"),
|
||||||
|
|
||||||
|
// P2025: Record not found
|
||||||
|
RECORD_NOT_FOUND: createPrismaError("P2025", "Record not found"),
|
||||||
|
|
||||||
|
// P2003: Foreign key constraint failed
|
||||||
|
FOREIGN_KEY: createPrismaError("P2003", "Foreign key constraint failed"),
|
||||||
|
|
||||||
|
// P2014: Required relation violation
|
||||||
|
REQUIRED_RELATION: createPrismaError("P2014", "Required relation violation"),
|
||||||
|
|
||||||
|
// Generic database error
|
||||||
|
DATABASE_ERROR: createPrismaError("P5000", "Database connection error"),
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validation error mock for non-database validation failures.
|
||||||
|
* Use this for validation errors in service layers.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { ValidationError } from "@formbricks/types/errors";
|
||||||
|
*
|
||||||
|
* vi.mocked(validateInputs).mockImplementation(() => {
|
||||||
|
* throw new ValidationError("Invalid input");
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class MockValidationError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "ValidationError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom error types that match Formbricks domain errors.
|
||||||
|
*/
|
||||||
|
export class MockDatabaseError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public code?: string
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = "DatabaseError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MockNotFoundError extends Error {
|
||||||
|
constructor(entity: string) {
|
||||||
|
super(`${entity} not found`);
|
||||||
|
this.name = "NotFoundError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MockAuthorizationError extends Error {
|
||||||
|
constructor(message = "Unauthorized") {
|
||||||
|
super(message);
|
||||||
|
this.name = "AuthorizationError";
|
||||||
|
}
|
||||||
|
}
|
||||||
49
apps/web/lib/testing/mocks/index.ts
Normal file
49
apps/web/lib/testing/mocks/index.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Centralized mock exports for all testing utilities.
|
||||||
|
*
|
||||||
|
* Import only what you need:
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { createContactsMocks } from "@/lib/testing/mocks";
|
||||||
|
* import { COMMON_ERRORS, createPrismaError } from "@/lib/testing/mocks";
|
||||||
|
* import { createMockTransaction, mockPrismaTransaction } from "@/lib/testing/mocks";
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Or import everything:
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import * as mocks from "@/lib/testing/mocks";
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
export {
|
||||||
|
createContactsMocks,
|
||||||
|
createQuotasMocks,
|
||||||
|
createSurveysMocks,
|
||||||
|
mockContactMethods,
|
||||||
|
mockContactAttributeMethods,
|
||||||
|
mockContactAttributeKeyMethods,
|
||||||
|
mockResponseQuotaLinkMethods,
|
||||||
|
mockSurveyMethods,
|
||||||
|
mockResponseMethods,
|
||||||
|
} from "./database";
|
||||||
|
|
||||||
|
export {
|
||||||
|
createPrismaError,
|
||||||
|
COMMON_ERRORS,
|
||||||
|
MockValidationError,
|
||||||
|
MockDatabaseError,
|
||||||
|
MockNotFoundError,
|
||||||
|
MockAuthorizationError,
|
||||||
|
} from "./errors";
|
||||||
|
|
||||||
|
export {
|
||||||
|
createMockTransaction,
|
||||||
|
mockPrismaTransaction,
|
||||||
|
quotaTransactionMock,
|
||||||
|
contactTransactionMock,
|
||||||
|
responseTransactionMock,
|
||||||
|
sequenceTransactionMocks,
|
||||||
|
} from "./transactions";
|
||||||
123
apps/web/lib/testing/mocks/transactions.ts
Normal file
123
apps/web/lib/testing/mocks/transactions.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { vi } from "vitest";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory to dynamically create mock transaction objects with specified methods.
|
||||||
|
* Eliminates complex, repetitive transaction mock setup across test files.
|
||||||
|
*
|
||||||
|
* @param structure - Object mapping namespaces to arrays of method names
|
||||||
|
* @returns Mock transaction object with all specified methods as vi.fn()
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { createMockTransaction } from "@/lib/testing/mocks";
|
||||||
|
*
|
||||||
|
* const mockTx = createMockTransaction({
|
||||||
|
* responseQuotaLink: ["deleteMany", "createMany", "updateMany", "count", "groupBy"],
|
||||||
|
* contact: ["findMany", "create"],
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Now you have:
|
||||||
|
* // mockTx.responseQuotaLink.deleteMany, mockTx.responseQuotaLink.createMany, etc.
|
||||||
|
* // mockTx.contact.findMany, mockTx.contact.create, etc.
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function createMockTransaction(structure: Record<string, string[]>) {
|
||||||
|
return Object.entries(structure).reduce(
|
||||||
|
(acc, [namespace, methods]) => {
|
||||||
|
acc[namespace] = methods.reduce(
|
||||||
|
(methodAcc, method) => {
|
||||||
|
methodAcc[method] = vi.fn();
|
||||||
|
return methodAcc;
|
||||||
|
},
|
||||||
|
{} as Record<string, ReturnType<typeof vi.fn>>
|
||||||
|
);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, Record<string, ReturnType<typeof vi.fn>>>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a mock Prisma $transaction wrapper.
|
||||||
|
* Passes the transaction object to the callback function.
|
||||||
|
*
|
||||||
|
* @param mockTx - The mock transaction object
|
||||||
|
* @returns A vi.fn() that mocks prisma.$transaction
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { createMockTransaction, mockPrismaTransaction } from "@/lib/testing/mocks";
|
||||||
|
*
|
||||||
|
* const mockTx = createMockTransaction({
|
||||||
|
* responseQuotaLink: ["deleteMany", "createMany"],
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* vi.mocked(prisma.$transaction) = mockPrismaTransaction(mockTx);
|
||||||
|
*
|
||||||
|
* // Now when code calls prisma.$transaction(async (tx) => { ... })
|
||||||
|
* // the tx parameter will be mockTx
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function mockPrismaTransaction(mockTx: any) {
|
||||||
|
return vi.fn(async (cb: any) => cb(mockTx));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-configured transaction mock for quota operations.
|
||||||
|
* Use this when testing quota-related database transactions.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { quotaTransactionMock } from "@/lib/testing/mocks";
|
||||||
|
*
|
||||||
|
* vi.mocked(prisma.$transaction) = quotaTransactionMock;
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const quotaTransactionMock = createMockTransaction({
|
||||||
|
responseQuotaLink: ["deleteMany", "createMany", "updateMany", "count", "groupBy"],
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-configured transaction mock for contact operations.
|
||||||
|
*/
|
||||||
|
export const contactTransactionMock = createMockTransaction({
|
||||||
|
contact: ["findMany", "create", "update", "delete"],
|
||||||
|
contactAttribute: ["findMany", "create", "update", "deleteMany"],
|
||||||
|
contactAttributeKey: ["findMany", "create"],
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-configured transaction mock for response operations.
|
||||||
|
*/
|
||||||
|
export const responseTransactionMock = createMockTransaction({
|
||||||
|
response: ["findMany", "create", "update", "delete", "count"],
|
||||||
|
responseQuotaLink: ["create", "deleteMany", "updateMany"],
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility to configure multiple transaction return values in sequence.
|
||||||
|
* Useful when code makes multiple calls to $transaction with different structures.
|
||||||
|
*
|
||||||
|
* @param txMocks - Array of transaction mock objects
|
||||||
|
* @returns A vi.fn() that returns each mock in sequence
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { createMockTransaction, sequenceTransactionMocks } from "@/lib/testing/mocks";
|
||||||
|
*
|
||||||
|
* const tx1 = createMockTransaction({ contact: ["findMany"] });
|
||||||
|
* const tx2 = createMockTransaction({ response: ["count"] });
|
||||||
|
*
|
||||||
|
* vi.mocked(prisma.$transaction) = sequenceTransactionMocks([tx1, tx2]);
|
||||||
|
*
|
||||||
|
* // First call gets tx1, second call gets tx2
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function sequenceTransactionMocks(txMocks: any[]) {
|
||||||
|
let callCount = 0;
|
||||||
|
return vi.fn(async (cb: any) => {
|
||||||
|
const currentMock = txMocks[callCount];
|
||||||
|
callCount++;
|
||||||
|
return cb(currentMock);
|
||||||
|
});
|
||||||
|
}
|
||||||
31
apps/web/lib/testing/setup.ts
Normal file
31
apps/web/lib/testing/setup.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { afterEach, beforeEach, vi } from "vitest";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard test environment setup with consistent cleanup patterns.
|
||||||
|
* Call this function once at the top of your test file to ensure
|
||||||
|
* mocks are properly cleaned up between tests.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { setupTestEnvironment } from "@/lib/testing/setup";
|
||||||
|
*
|
||||||
|
* setupTestEnvironment();
|
||||||
|
*
|
||||||
|
* describe("MyModule", () => {
|
||||||
|
* test("should work correctly", () => {
|
||||||
|
* // Your test code here
|
||||||
|
* });
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Note: This replaces manual beforeEach/afterEach blocks in individual test files.
|
||||||
|
*/
|
||||||
|
export function setupTestEnvironment() {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
import { Contact, Prisma } from "@prisma/client";
|
import { describe, expect, test, vi } from "vitest";
|
||||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
import { DatabaseError, ValidationError } from "@formbricks/types/errors";
|
import { DatabaseError, ValidationError } from "@formbricks/types/errors";
|
||||||
|
// NOW import modules that depend on mocks
|
||||||
|
import { FIXTURES, TEST_IDS } from "@/lib/testing/constants";
|
||||||
|
// Import utilities that DON'T need to be mocked FIRST
|
||||||
|
import { COMMON_ERRORS, createContactsMocks } from "@/lib/testing/mocks";
|
||||||
|
import { setupTestEnvironment } from "@/lib/testing/setup";
|
||||||
|
import { validateInputs } from "@/lib/utils/validate";
|
||||||
import {
|
import {
|
||||||
buildContactWhereClause,
|
buildContactWhereClause,
|
||||||
createContactsFromCSV,
|
createContactsFromCSV,
|
||||||
@@ -12,7 +17,9 @@ import {
|
|||||||
getContactsInSegment,
|
getContactsInSegment,
|
||||||
} from "./contacts";
|
} from "./contacts";
|
||||||
|
|
||||||
// Mock additional dependencies for the new functions
|
// Setup ALL mocks BEFORE any other imports
|
||||||
|
vi.mock("@formbricks/database", () => createContactsMocks());
|
||||||
|
|
||||||
vi.mock("@/modules/ee/contacts/segments/lib/segments", () => ({
|
vi.mock("@/modules/ee/contacts/segments/lib/segments", () => ({
|
||||||
getSegment: vi.fn(),
|
getSegment: vi.fn(),
|
||||||
}));
|
}));
|
||||||
@@ -31,27 +38,10 @@ vi.mock("@formbricks/logger", () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@formbricks/database", () => ({
|
vi.mock("@/lib/utils/validate", () => ({
|
||||||
prisma: {
|
validateInputs: vi.fn(),
|
||||||
contact: {
|
|
||||||
findMany: vi.fn(),
|
|
||||||
findUnique: vi.fn(),
|
|
||||||
delete: vi.fn(),
|
|
||||||
update: vi.fn(),
|
|
||||||
create: vi.fn(),
|
|
||||||
},
|
|
||||||
contactAttribute: {
|
|
||||||
findMany: vi.fn(),
|
|
||||||
createMany: vi.fn(),
|
|
||||||
findFirst: vi.fn(),
|
|
||||||
deleteMany: vi.fn(),
|
|
||||||
},
|
|
||||||
contactAttributeKey: {
|
|
||||||
findMany: vi.fn(),
|
|
||||||
createMany: vi.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/lib/constants", () => ({
|
vi.mock("@/lib/constants", () => ({
|
||||||
ITEMS_PER_PAGE: 2,
|
ITEMS_PER_PAGE: 2,
|
||||||
ENCRYPTION_KEY: "test-encryption-key-32-chars-long!",
|
ENCRYPTION_KEY: "test-encryption-key-32-chars-long!",
|
||||||
@@ -61,124 +51,86 @@ vi.mock("@/lib/constants", () => ({
|
|||||||
POSTHOG_API_KEY: "test-posthog-key",
|
POSTHOG_API_KEY: "test-posthog-key",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const environmentId = "cm123456789012345678901237";
|
// Setup standard test environment
|
||||||
const contactId = "cm123456789012345678901238";
|
setupTestEnvironment();
|
||||||
const userId = "cm123456789012345678901239";
|
|
||||||
const mockContact: Contact & {
|
// Mock validateInputs to return no errors by default
|
||||||
attributes: { value: string; attributeKey: { key: string; name: string } }[];
|
vi.mocked(validateInputs).mockImplementation(() => {
|
||||||
} = {
|
return [];
|
||||||
id: contactId,
|
});
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
environmentId,
|
|
||||||
userId,
|
|
||||||
attributes: [
|
|
||||||
{ value: "john@example.com", attributeKey: { key: "email", name: "Email" } },
|
|
||||||
{ value: "John", attributeKey: { key: "name", name: "Name" } },
|
|
||||||
{ value: userId, attributeKey: { key: "userId", name: "User ID" } },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("getContacts", () => {
|
describe("getContacts", () => {
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns contacts with attributes", async () => {
|
test("returns contacts with attributes", async () => {
|
||||||
vi.mocked(prisma.contact.findMany).mockResolvedValue([mockContact]);
|
vi.mocked(prisma.contact.findMany).mockResolvedValue([FIXTURES.contact]);
|
||||||
const result = await getContacts(environmentId, 0, "");
|
const result = await getContacts(TEST_IDS.environment, 0, "");
|
||||||
expect(Array.isArray(result)).toBe(true);
|
expect(Array.isArray(result)).toBe(true);
|
||||||
expect(result[0].id).toBe(contactId);
|
expect(result[0].id).toBe(TEST_IDS.contact);
|
||||||
expect(result[0].attributes.email).toBe("john@example.com");
|
expect(result[0].attributes.email).toBe("test@example.com");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("returns empty array if no contacts", async () => {
|
test("returns empty array if no contacts", async () => {
|
||||||
vi.mocked(prisma.contact.findMany).mockResolvedValue([]);
|
vi.mocked(prisma.contact.findMany).mockResolvedValue([]);
|
||||||
const result = await getContacts(environmentId, 0, "");
|
const result = await getContacts(TEST_IDS.environment, 0, "");
|
||||||
expect(result).toEqual([]);
|
expect(result).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("throws DatabaseError on Prisma error", async () => {
|
test("throws DatabaseError on Prisma error", async () => {
|
||||||
const prismaError = new Prisma.PrismaClientKnownRequestError("DB error", {
|
vi.mocked(prisma.contact.findMany).mockRejectedValue(COMMON_ERRORS.UNIQUE_CONSTRAINT);
|
||||||
code: "P2002",
|
await expect(getContacts(TEST_IDS.environment, 0, "")).rejects.toThrow(DatabaseError);
|
||||||
clientVersion: "1.0.0",
|
|
||||||
});
|
|
||||||
vi.mocked(prisma.contact.findMany).mockRejectedValue(prismaError);
|
|
||||||
await expect(getContacts(environmentId, 0, "")).rejects.toThrow(DatabaseError);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("throws original error on unknown error", async () => {
|
test("throws original error on unknown error", async () => {
|
||||||
const genericError = new Error("Unknown error");
|
const genericError = new Error("Unknown error");
|
||||||
vi.mocked(prisma.contact.findMany).mockRejectedValue(genericError);
|
vi.mocked(prisma.contact.findMany).mockRejectedValue(genericError);
|
||||||
await expect(getContacts(environmentId, 0, "")).rejects.toThrow(genericError);
|
await expect(getContacts(TEST_IDS.environment, 0, "")).rejects.toThrow(genericError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getContact", () => {
|
describe("getContact", () => {
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns contact if found", async () => {
|
test("returns contact if found", async () => {
|
||||||
vi.mocked(prisma.contact.findUnique).mockResolvedValue(mockContact);
|
vi.mocked(prisma.contact.findUnique).mockResolvedValue(FIXTURES.contact);
|
||||||
const result = await getContact(contactId);
|
const result = await getContact(TEST_IDS.contact);
|
||||||
expect(result).toEqual(mockContact);
|
expect(result).toEqual(FIXTURES.contact);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("returns null if not found", async () => {
|
test("returns null if not found", async () => {
|
||||||
vi.mocked(prisma.contact.findUnique).mockResolvedValue(null);
|
vi.mocked(prisma.contact.findUnique).mockResolvedValue(null);
|
||||||
const result = await getContact(contactId);
|
const result = await getContact(TEST_IDS.contact);
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("throws DatabaseError on Prisma error", async () => {
|
test("throws DatabaseError on Prisma error", async () => {
|
||||||
const prismaError = new Prisma.PrismaClientKnownRequestError("DB error", {
|
vi.mocked(prisma.contact.findUnique).mockRejectedValue(COMMON_ERRORS.UNIQUE_CONSTRAINT);
|
||||||
code: "P2002",
|
await expect(getContact(TEST_IDS.contact)).rejects.toThrow(DatabaseError);
|
||||||
clientVersion: "1.0.0",
|
|
||||||
});
|
|
||||||
vi.mocked(prisma.contact.findUnique).mockRejectedValue(prismaError);
|
|
||||||
await expect(getContact(contactId)).rejects.toThrow(DatabaseError);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("throws original error on unknown error", async () => {
|
test("throws original error on unknown error", async () => {
|
||||||
const genericError = new Error("Unknown error");
|
const genericError = new Error("Unknown error");
|
||||||
vi.mocked(prisma.contact.findUnique).mockRejectedValue(genericError);
|
vi.mocked(prisma.contact.findUnique).mockRejectedValue(genericError);
|
||||||
await expect(getContact(contactId)).rejects.toThrow(genericError);
|
await expect(getContact(TEST_IDS.contact)).rejects.toThrow(genericError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("deleteContact", () => {
|
describe("deleteContact", () => {
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("deletes contact and revalidates caches", async () => {
|
test("deletes contact and revalidates caches", async () => {
|
||||||
vi.mocked(prisma.contact.delete).mockResolvedValue(mockContact);
|
vi.mocked(prisma.contact.delete).mockResolvedValue(FIXTURES.contact);
|
||||||
const result = await deleteContact(contactId);
|
const result = await deleteContact(TEST_IDS.contact);
|
||||||
expect(result).toEqual(mockContact);
|
expect(result).toEqual(FIXTURES.contact);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("throws DatabaseError on Prisma error", async () => {
|
test("throws DatabaseError on Prisma error", async () => {
|
||||||
const prismaError = new Prisma.PrismaClientKnownRequestError("DB error", {
|
vi.mocked(prisma.contact.delete).mockRejectedValue(COMMON_ERRORS.UNIQUE_CONSTRAINT);
|
||||||
code: "P2002",
|
await expect(deleteContact(TEST_IDS.contact)).rejects.toThrow(DatabaseError);
|
||||||
clientVersion: "1.0.0",
|
|
||||||
});
|
|
||||||
vi.mocked(prisma.contact.delete).mockRejectedValue(prismaError);
|
|
||||||
await expect(deleteContact(contactId)).rejects.toThrow(DatabaseError);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("throws original error on unknown error", async () => {
|
test("throws original error on unknown error", async () => {
|
||||||
const genericError = new Error("Unknown error");
|
const genericError = new Error("Unknown error");
|
||||||
vi.mocked(prisma.contact.delete).mockRejectedValue(genericError);
|
vi.mocked(prisma.contact.delete).mockRejectedValue(genericError);
|
||||||
await expect(deleteContact(contactId)).rejects.toThrow(genericError);
|
await expect(deleteContact(TEST_IDS.contact)).rejects.toThrow(genericError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("createContactsFromCSV", () => {
|
describe("createContactsFromCSV", () => {
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("creates new contacts and missing attribute keys", async () => {
|
test("creates new contacts and missing attribute keys", async () => {
|
||||||
vi.mocked(prisma.contact.findMany).mockResolvedValue([]);
|
vi.mocked(prisma.contact.findMany).mockResolvedValue([]);
|
||||||
vi.mocked(prisma.contactAttribute.findMany).mockResolvedValue([]);
|
vi.mocked(prisma.contactAttribute.findMany).mockResolvedValue([]);
|
||||||
@@ -191,7 +143,7 @@ describe("createContactsFromCSV", () => {
|
|||||||
vi.mocked(prisma.contactAttributeKey.createMany).mockResolvedValue({ count: 2 });
|
vi.mocked(prisma.contactAttributeKey.createMany).mockResolvedValue({ count: 2 });
|
||||||
vi.mocked(prisma.contact.create).mockResolvedValue({
|
vi.mocked(prisma.contact.create).mockResolvedValue({
|
||||||
id: "c1",
|
id: "c1",
|
||||||
environmentId,
|
environmentId: TEST_IDS.environment,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
attributes: [
|
attributes: [
|
||||||
@@ -200,7 +152,7 @@ describe("createContactsFromCSV", () => {
|
|||||||
],
|
],
|
||||||
} as any);
|
} as any);
|
||||||
const csvData = [{ email: "john@example.com", name: "John" }];
|
const csvData = [{ email: "john@example.com", name: "John" }];
|
||||||
const result = await createContactsFromCSV(csvData, environmentId, "skip", {
|
const result = await createContactsFromCSV(csvData, TEST_IDS.environment, "skip", {
|
||||||
email: "email",
|
email: "email",
|
||||||
name: "name",
|
name: "name",
|
||||||
});
|
});
|
||||||
@@ -218,7 +170,7 @@ describe("createContactsFromCSV", () => {
|
|||||||
{ key: "name", id: "id-name" },
|
{ key: "name", id: "id-name" },
|
||||||
] as any);
|
] as any);
|
||||||
const csvData = [{ email: "john@example.com", name: "John" }];
|
const csvData = [{ email: "john@example.com", name: "John" }];
|
||||||
const result = await createContactsFromCSV(csvData, environmentId, "skip", {
|
const result = await createContactsFromCSV(csvData, TEST_IDS.environment, "skip", {
|
||||||
email: "email",
|
email: "email",
|
||||||
name: "name",
|
name: "name",
|
||||||
});
|
});
|
||||||
@@ -242,7 +194,7 @@ describe("createContactsFromCSV", () => {
|
|||||||
] as any);
|
] as any);
|
||||||
vi.mocked(prisma.contact.update).mockResolvedValue({
|
vi.mocked(prisma.contact.update).mockResolvedValue({
|
||||||
id: "c1",
|
id: "c1",
|
||||||
environmentId,
|
environmentId: TEST_IDS.environment,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
attributes: [
|
attributes: [
|
||||||
@@ -251,7 +203,7 @@ describe("createContactsFromCSV", () => {
|
|||||||
],
|
],
|
||||||
} as any);
|
} as any);
|
||||||
const csvData = [{ email: "john@example.com", name: "John" }];
|
const csvData = [{ email: "john@example.com", name: "John" }];
|
||||||
const result = await createContactsFromCSV(csvData, environmentId, "update", {
|
const result = await createContactsFromCSV(csvData, TEST_IDS.environment, "update", {
|
||||||
email: "email",
|
email: "email",
|
||||||
name: "name",
|
name: "name",
|
||||||
});
|
});
|
||||||
@@ -276,7 +228,7 @@ describe("createContactsFromCSV", () => {
|
|||||||
vi.mocked(prisma.contactAttribute.deleteMany).mockResolvedValue({ count: 2 });
|
vi.mocked(prisma.contactAttribute.deleteMany).mockResolvedValue({ count: 2 });
|
||||||
vi.mocked(prisma.contact.update).mockResolvedValue({
|
vi.mocked(prisma.contact.update).mockResolvedValue({
|
||||||
id: "c1",
|
id: "c1",
|
||||||
environmentId,
|
environmentId: TEST_IDS.environment,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
attributes: [
|
attributes: [
|
||||||
@@ -285,7 +237,7 @@ describe("createContactsFromCSV", () => {
|
|||||||
],
|
],
|
||||||
} as any);
|
} as any);
|
||||||
const csvData = [{ email: "john@example.com", name: "John" }];
|
const csvData = [{ email: "john@example.com", name: "John" }];
|
||||||
const result = await createContactsFromCSV(csvData, environmentId, "overwrite", {
|
const result = await createContactsFromCSV(csvData, TEST_IDS.environment, "overwrite", {
|
||||||
email: "email",
|
email: "email",
|
||||||
name: "name",
|
name: "name",
|
||||||
});
|
});
|
||||||
@@ -293,21 +245,21 @@ describe("createContactsFromCSV", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("throws ValidationError if email is missing in CSV", async () => {
|
test("throws ValidationError if email is missing in CSV", async () => {
|
||||||
|
// Override the validateInputs mock to return validation errors for this test
|
||||||
|
vi.mocked(validateInputs).mockImplementationOnce(() => {
|
||||||
|
throw new ValidationError("Validation failed");
|
||||||
|
});
|
||||||
const csvData = [{ name: "John" }];
|
const csvData = [{ name: "John" }];
|
||||||
await expect(
|
await expect(
|
||||||
createContactsFromCSV(csvData as any, environmentId, "skip", { name: "name" })
|
createContactsFromCSV(csvData as any, TEST_IDS.environment, "skip", { name: "name" })
|
||||||
).rejects.toThrow(ValidationError);
|
).rejects.toThrow(ValidationError);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("throws DatabaseError on Prisma error", async () => {
|
test("throws DatabaseError on Prisma error", async () => {
|
||||||
const prismaError = new Prisma.PrismaClientKnownRequestError("DB error", {
|
vi.mocked(prisma.contact.findMany).mockRejectedValue(COMMON_ERRORS.UNIQUE_CONSTRAINT);
|
||||||
code: "P2002",
|
|
||||||
clientVersion: "1.0.0",
|
|
||||||
});
|
|
||||||
vi.mocked(prisma.contact.findMany).mockRejectedValue(prismaError);
|
|
||||||
const csvData = [{ email: "john@example.com", name: "John" }];
|
const csvData = [{ email: "john@example.com", name: "John" }];
|
||||||
await expect(
|
await expect(
|
||||||
createContactsFromCSV(csvData, environmentId, "skip", { email: "email", name: "name" })
|
createContactsFromCSV(csvData, TEST_IDS.environment, "skip", { email: "email", name: "name" })
|
||||||
).rejects.toThrow(DatabaseError);
|
).rejects.toThrow(DatabaseError);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -316,22 +268,17 @@ describe("createContactsFromCSV", () => {
|
|||||||
vi.mocked(prisma.contact.findMany).mockRejectedValue(genericError);
|
vi.mocked(prisma.contact.findMany).mockRejectedValue(genericError);
|
||||||
const csvData = [{ email: "john@example.com", name: "John" }];
|
const csvData = [{ email: "john@example.com", name: "John" }];
|
||||||
await expect(
|
await expect(
|
||||||
createContactsFromCSV(csvData, environmentId, "skip", { email: "email", name: "name" })
|
createContactsFromCSV(csvData, TEST_IDS.environment, "skip", { email: "email", name: "name" })
|
||||||
).rejects.toThrow(genericError);
|
).rejects.toThrow(genericError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("buildContactWhereClause", () => {
|
describe("buildContactWhereClause", () => {
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns where clause for email", () => {
|
test("returns where clause for email", () => {
|
||||||
const environmentId = "env-1";
|
|
||||||
const search = "john";
|
const search = "john";
|
||||||
const result = buildContactWhereClause(environmentId, search);
|
const result = buildContactWhereClause(TEST_IDS.environment, search);
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
environmentId,
|
environmentId: TEST_IDS.environment,
|
||||||
OR: [
|
OR: [
|
||||||
{
|
{
|
||||||
attributes: {
|
attributes: {
|
||||||
@@ -354,26 +301,18 @@ describe("buildContactWhereClause", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("returns where clause without search", () => {
|
test("returns where clause without search", () => {
|
||||||
const environmentId = "cm123456789012345678901240";
|
const result = buildContactWhereClause(TEST_IDS.environment);
|
||||||
const result = buildContactWhereClause(environmentId);
|
expect(result).toEqual({ environmentId: TEST_IDS.environment });
|
||||||
expect(result).toEqual({ environmentId });
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getContactsInSegment", () => {
|
describe("getContactsInSegment", () => {
|
||||||
const mockSegmentId = "cm123456789012345678901235";
|
|
||||||
const mockEnvironmentId = "cm123456789012345678901236";
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns contacts when segment and filters are valid", async () => {
|
test("returns contacts when segment and filters are valid", async () => {
|
||||||
const mockSegment = {
|
const mockSegment = {
|
||||||
id: mockSegmentId,
|
id: TEST_IDS.segment,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
environmentId: mockEnvironmentId,
|
environmentId: TEST_IDS.environment,
|
||||||
description: "Test segment",
|
description: "Test segment",
|
||||||
title: "Test Segment",
|
title: "Test Segment",
|
||||||
isPrivate: false,
|
isPrivate: false,
|
||||||
@@ -399,7 +338,7 @@ describe("getContactsInSegment", () => {
|
|||||||
] as any;
|
] as any;
|
||||||
|
|
||||||
const mockWhereClause = {
|
const mockWhereClause = {
|
||||||
environmentId: mockEnvironmentId,
|
environmentId: TEST_IDS.environment,
|
||||||
attributes: {
|
attributes: {
|
||||||
some: {
|
some: {
|
||||||
attributeKey: { key: "email" },
|
attributeKey: { key: "email" },
|
||||||
@@ -423,7 +362,7 @@ describe("getContactsInSegment", () => {
|
|||||||
|
|
||||||
vi.mocked(prisma.contact.findMany).mockResolvedValue(mockContacts);
|
vi.mocked(prisma.contact.findMany).mockResolvedValue(mockContacts);
|
||||||
|
|
||||||
const result = await getContactsInSegment(mockSegmentId);
|
const result = await getContactsInSegment(TEST_IDS.segment);
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
@@ -475,17 +414,17 @@ describe("getContactsInSegment", () => {
|
|||||||
|
|
||||||
vi.mocked(getSegment).mockRejectedValue(new Error("Segment not found"));
|
vi.mocked(getSegment).mockRejectedValue(new Error("Segment not found"));
|
||||||
|
|
||||||
const result = await getContactsInSegment(mockSegmentId);
|
const result = await getContactsInSegment(TEST_IDS.segment);
|
||||||
|
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("returns null when segment filter to prisma query fails", async () => {
|
test("returns null when segment filter to prisma query fails", async () => {
|
||||||
const mockSegment = {
|
const mockSegment = {
|
||||||
id: mockSegmentId,
|
id: TEST_IDS.segment,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
environmentId: mockEnvironmentId,
|
environmentId: TEST_IDS.environment,
|
||||||
description: "Test segment",
|
description: "Test segment",
|
||||||
title: "Test Segment",
|
title: "Test Segment",
|
||||||
isPrivate: false,
|
isPrivate: false,
|
||||||
@@ -505,17 +444,17 @@ describe("getContactsInSegment", () => {
|
|||||||
error: { type: "bad_request" },
|
error: { type: "bad_request" },
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
const result = await getContactsInSegment(mockSegmentId);
|
const result = await getContactsInSegment(TEST_IDS.segment);
|
||||||
|
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("returns null when prisma query fails", async () => {
|
test("returns null when prisma query fails", async () => {
|
||||||
const mockSegment = {
|
const mockSegment = {
|
||||||
id: mockSegmentId,
|
id: TEST_IDS.segment,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
environmentId: mockEnvironmentId,
|
environmentId: TEST_IDS.environment,
|
||||||
description: "Test segment",
|
description: "Test segment",
|
||||||
title: "Test Segment",
|
title: "Test Segment",
|
||||||
isPrivate: false,
|
isPrivate: false,
|
||||||
@@ -537,7 +476,7 @@ describe("getContactsInSegment", () => {
|
|||||||
|
|
||||||
vi.mocked(prisma.contact.findMany).mockRejectedValue(new Error("Database error"));
|
vi.mocked(prisma.contact.findMany).mockRejectedValue(new Error("Database error"));
|
||||||
|
|
||||||
const result = await getContactsInSegment(mockSegmentId);
|
const result = await getContactsInSegment(TEST_IDS.segment);
|
||||||
|
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
@@ -547,28 +486,20 @@ describe("getContactsInSegment", () => {
|
|||||||
|
|
||||||
vi.mocked(getSegment).mockRejectedValue(new Error("Database error"));
|
vi.mocked(getSegment).mockRejectedValue(new Error("Database error"));
|
||||||
|
|
||||||
const result = await getContactsInSegment(mockSegmentId);
|
const result = await getContactsInSegment(TEST_IDS.segment);
|
||||||
|
|
||||||
expect(result).toBeNull(); // The function catches errors and returns null
|
expect(result).toBeNull(); // The function catches errors and returns null
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("generatePersonalLinks", () => {
|
describe("generatePersonalLinks", () => {
|
||||||
const mockSurveyId = "cm123456789012345678901234"; // Valid CUID2 format
|
|
||||||
const mockSegmentId = "cm123456789012345678901235"; // Valid CUID2 format
|
|
||||||
const mockExpirationDays = 7;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns null when getContactsInSegment fails", async () => {
|
test("returns null when getContactsInSegment fails", async () => {
|
||||||
// Mock getSegment to fail which will cause getContactsInSegment to return null
|
// Mock getSegment to fail which will cause getContactsInSegment to return null
|
||||||
const { getSegment } = await import("@/modules/ee/contacts/segments/lib/segments");
|
const { getSegment } = await import("@/modules/ee/contacts/segments/lib/segments");
|
||||||
|
|
||||||
vi.mocked(getSegment).mockRejectedValue(new Error("Segment not found"));
|
vi.mocked(getSegment).mockRejectedValue(new Error("Segment not found"));
|
||||||
|
|
||||||
const result = await generatePersonalLinks(mockSurveyId, mockSegmentId);
|
const result = await generatePersonalLinks(TEST_IDS.survey, TEST_IDS.segment);
|
||||||
|
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
@@ -581,10 +512,10 @@ describe("generatePersonalLinks", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
vi.mocked(getSegment).mockResolvedValue({
|
vi.mocked(getSegment).mockResolvedValue({
|
||||||
id: mockSegmentId,
|
id: TEST_IDS.segment,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
environmentId: "env-123",
|
environmentId: TEST_IDS.environment,
|
||||||
description: "Test segment",
|
description: "Test segment",
|
||||||
title: "Test Segment",
|
title: "Test Segment",
|
||||||
isPrivate: false,
|
isPrivate: false,
|
||||||
@@ -599,12 +530,13 @@ describe("generatePersonalLinks", () => {
|
|||||||
|
|
||||||
vi.mocked(prisma.contact.findMany).mockResolvedValue([]);
|
vi.mocked(prisma.contact.findMany).mockResolvedValue([]);
|
||||||
|
|
||||||
const result = await generatePersonalLinks(mockSurveyId, mockSegmentId);
|
const result = await generatePersonalLinks(TEST_IDS.survey, TEST_IDS.segment);
|
||||||
|
|
||||||
expect(result).toEqual([]);
|
expect(result).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("generates personal links for contacts successfully", async () => {
|
test("generates personal links for contacts successfully", async () => {
|
||||||
|
const expirationDays = 7;
|
||||||
// Mock all the dependencies that getContactsInSegment needs
|
// Mock all the dependencies that getContactsInSegment needs
|
||||||
const { getSegment } = await import("@/modules/ee/contacts/segments/lib/segments");
|
const { getSegment } = await import("@/modules/ee/contacts/segments/lib/segments");
|
||||||
const { segmentFilterToPrismaQuery } = await import(
|
const { segmentFilterToPrismaQuery } = await import(
|
||||||
@@ -613,10 +545,10 @@ describe("generatePersonalLinks", () => {
|
|||||||
const { getContactSurveyLink } = await import("@/modules/ee/contacts/lib/contact-survey-link");
|
const { getContactSurveyLink } = await import("@/modules/ee/contacts/lib/contact-survey-link");
|
||||||
|
|
||||||
vi.mocked(getSegment).mockResolvedValue({
|
vi.mocked(getSegment).mockResolvedValue({
|
||||||
id: mockSegmentId,
|
id: TEST_IDS.segment,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
environmentId: "env-123",
|
environmentId: TEST_IDS.environment,
|
||||||
description: "Test segment",
|
description: "Test segment",
|
||||||
title: "Test Segment",
|
title: "Test Segment",
|
||||||
isPrivate: false,
|
isPrivate: false,
|
||||||
@@ -657,7 +589,7 @@ describe("generatePersonalLinks", () => {
|
|||||||
data: "https://example.com/survey/link2",
|
data: "https://example.com/survey/link2",
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await generatePersonalLinks(mockSurveyId, mockSegmentId, mockExpirationDays);
|
const result = await generatePersonalLinks(TEST_IDS.survey, TEST_IDS.segment, expirationDays);
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
@@ -667,7 +599,7 @@ describe("generatePersonalLinks", () => {
|
|||||||
name: "Test User",
|
name: "Test User",
|
||||||
},
|
},
|
||||||
surveyUrl: "https://example.com/survey/link1",
|
surveyUrl: "https://example.com/survey/link1",
|
||||||
expirationDays: mockExpirationDays,
|
expirationDays,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
contactId: "contact-2",
|
contactId: "contact-2",
|
||||||
@@ -676,11 +608,11 @@ describe("generatePersonalLinks", () => {
|
|||||||
name: "Another User",
|
name: "Another User",
|
||||||
},
|
},
|
||||||
surveyUrl: "https://example.com/survey/link2",
|
surveyUrl: "https://example.com/survey/link2",
|
||||||
expirationDays: mockExpirationDays,
|
expirationDays,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(getContactSurveyLink).toHaveBeenCalledWith("contact-1", mockSurveyId, mockExpirationDays);
|
expect(getContactSurveyLink).toHaveBeenCalledWith("contact-1", TEST_IDS.survey, expirationDays);
|
||||||
expect(getContactSurveyLink).toHaveBeenCalledWith("contact-2", mockSurveyId, mockExpirationDays);
|
expect(getContactSurveyLink).toHaveBeenCalledWith("contact-2", TEST_IDS.survey, expirationDays);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
import { describe, expect, test, vi } from "vitest";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
|
// mocked via vi.mock()
|
||||||
import {
|
import {
|
||||||
DatabaseError,
|
DatabaseError,
|
||||||
InvalidInputError,
|
InvalidInputError,
|
||||||
@@ -8,9 +9,14 @@ import {
|
|||||||
ValidationError,
|
ValidationError,
|
||||||
} from "@formbricks/types/errors";
|
} from "@formbricks/types/errors";
|
||||||
import { TSurveyQuota, TSurveyQuotaInput } from "@formbricks/types/quota";
|
import { TSurveyQuota, TSurveyQuotaInput } from "@formbricks/types/quota";
|
||||||
|
import { TEST_IDS } from "@/lib/testing/constants";
|
||||||
|
import { COMMON_ERRORS } from "@/lib/testing/mocks";
|
||||||
|
import { setupTestEnvironment } from "@/lib/testing/setup";
|
||||||
import { validateInputs } from "@/lib/utils/validate";
|
import { validateInputs } from "@/lib/utils/validate";
|
||||||
import { createQuota, deleteQuota, getQuota, getQuotas, reduceQuotaLimits, updateQuota } from "./quotas";
|
import { createQuota, deleteQuota, getQuota, getQuotas, reduceQuotaLimits, updateQuota } from "./quotas";
|
||||||
|
|
||||||
|
setupTestEnvironment();
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
vi.mock("@formbricks/database", () => ({
|
vi.mock("@formbricks/database", () => ({
|
||||||
prisma: {
|
prisma: {
|
||||||
@@ -30,14 +36,11 @@ vi.mock("@/lib/utils/validate", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe("Quota Service", () => {
|
describe("Quota Service", () => {
|
||||||
const mockSurveyId = "survey123";
|
|
||||||
const mockQuotaId = "quota123";
|
|
||||||
|
|
||||||
const mockQuota: TSurveyQuota = {
|
const mockQuota: TSurveyQuota = {
|
||||||
id: mockQuotaId,
|
id: TEST_IDS.quota,
|
||||||
createdAt: new Date("2024-01-01"),
|
createdAt: new Date("2024-01-01"),
|
||||||
updatedAt: new Date("2024-01-01"),
|
updatedAt: new Date("2024-01-01"),
|
||||||
surveyId: mockSurveyId,
|
surveyId: TEST_IDS.survey,
|
||||||
name: "Test Quota",
|
name: "Test Quota",
|
||||||
limit: 100,
|
limit: 100,
|
||||||
logic: {
|
logic: {
|
||||||
@@ -49,42 +52,33 @@ describe("Quota Service", () => {
|
|||||||
countPartialSubmissions: false,
|
countPartialSubmissions: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
// Setup validateInputs mock in beforeEach (via setupTestEnvironment)
|
||||||
vi.mocked(validateInputs).mockImplementation(() => {
|
vi.mocked(validateInputs).mockImplementation(() => {
|
||||||
return [];
|
return [];
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getQuota", () => {
|
describe("getQuota", () => {
|
||||||
test("should return quota successfully", async () => {
|
test("should return quota successfully", async () => {
|
||||||
vi.mocked(prisma.surveyQuota.findUnique).mockResolvedValue(mockQuota);
|
vi.mocked(prisma.surveyQuota.findUnique).mockResolvedValue(mockQuota);
|
||||||
const result = await getQuota(mockQuotaId);
|
const result = await getQuota(TEST_IDS.quota);
|
||||||
expect(result).toEqual(mockQuota);
|
expect(result).toEqual(mockQuota);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should throw ResourceNotFoundError if quota not found", async () => {
|
test("should throw ResourceNotFoundError if quota not found", async () => {
|
||||||
vi.mocked(prisma.surveyQuota.findUnique).mockResolvedValue(null);
|
vi.mocked(prisma.surveyQuota.findUnique).mockResolvedValue(null);
|
||||||
await expect(getQuota(mockQuotaId)).rejects.toThrow(ResourceNotFoundError);
|
await expect(getQuota(TEST_IDS.quota)).rejects.toThrow(ResourceNotFoundError);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should throw DatabaseError on Prisma error", async () => {
|
test("should throw DatabaseError on Prisma error", async () => {
|
||||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Database error", {
|
vi.mocked(prisma.surveyQuota.findUnique).mockRejectedValue(COMMON_ERRORS.UNIQUE_CONSTRAINT);
|
||||||
code: "P2002",
|
await expect(getQuota(TEST_IDS.quota)).rejects.toThrow(DatabaseError);
|
||||||
clientVersion: "1.0.0",
|
|
||||||
});
|
|
||||||
vi.mocked(prisma.surveyQuota.findUnique).mockRejectedValue(prismaError);
|
|
||||||
await expect(getQuota(mockQuotaId)).rejects.toThrow(DatabaseError);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should throw ValidationError when validateInputs fails", async () => {
|
test("should throw ValidationError when validateInputs fails", async () => {
|
||||||
vi.mocked(validateInputs).mockImplementation(() => {
|
vi.mocked(validateInputs).mockImplementation(() => {
|
||||||
throw new ValidationError("Invalid input");
|
throw new ValidationError("Invalid input");
|
||||||
});
|
});
|
||||||
await expect(getQuota(mockQuotaId)).rejects.toThrow(ValidationError);
|
await expect(getQuota(TEST_IDS.quota)).rejects.toThrow(ValidationError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -93,24 +87,20 @@ describe("Quota Service", () => {
|
|||||||
const mockQuotas = [mockQuota];
|
const mockQuotas = [mockQuota];
|
||||||
vi.mocked(prisma.surveyQuota.findMany).mockResolvedValue(mockQuotas);
|
vi.mocked(prisma.surveyQuota.findMany).mockResolvedValue(mockQuotas);
|
||||||
|
|
||||||
const result = await getQuotas(mockSurveyId);
|
const result = await getQuotas(TEST_IDS.survey);
|
||||||
|
|
||||||
expect(result).toEqual(mockQuotas);
|
expect(result).toEqual(mockQuotas);
|
||||||
expect(validateInputs).toHaveBeenCalledWith([mockSurveyId, expect.any(Object)]);
|
expect(validateInputs).toHaveBeenCalledWith([TEST_IDS.survey, expect.any(Object)]);
|
||||||
expect(prisma.surveyQuota.findMany).toHaveBeenCalledWith({
|
expect(prisma.surveyQuota.findMany).toHaveBeenCalledWith({
|
||||||
where: { surveyId: mockSurveyId },
|
where: { surveyId: TEST_IDS.survey },
|
||||||
orderBy: { createdAt: "desc" },
|
orderBy: { createdAt: "desc" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should throw DatabaseError on Prisma error", async () => {
|
test("should throw DatabaseError on Prisma error", async () => {
|
||||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Database error", {
|
vi.mocked(prisma.surveyQuota.findMany).mockRejectedValue(COMMON_ERRORS.UNIQUE_CONSTRAINT);
|
||||||
code: "P2002",
|
|
||||||
clientVersion: "1.0.0",
|
|
||||||
});
|
|
||||||
vi.mocked(prisma.surveyQuota.findMany).mockRejectedValue(prismaError);
|
|
||||||
|
|
||||||
await expect(getQuotas(mockSurveyId)).rejects.toThrow(DatabaseError);
|
await expect(getQuotas(TEST_IDS.survey)).rejects.toThrow(DatabaseError);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should throw ValidationError when validateInputs fails", async () => {
|
test("should throw ValidationError when validateInputs fails", async () => {
|
||||||
@@ -118,20 +108,20 @@ describe("Quota Service", () => {
|
|||||||
throw new ValidationError("Invalid input");
|
throw new ValidationError("Invalid input");
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(getQuotas(mockSurveyId)).rejects.toThrow(ValidationError);
|
await expect(getQuotas(TEST_IDS.survey)).rejects.toThrow(ValidationError);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should re-throw non-Prisma errors", async () => {
|
test("should re-throw non-Prisma errors", async () => {
|
||||||
const genericError = new Error("Generic error");
|
const genericError = new Error("Generic error");
|
||||||
vi.mocked(prisma.surveyQuota.findMany).mockRejectedValue(genericError);
|
vi.mocked(prisma.surveyQuota.findMany).mockRejectedValue(genericError);
|
||||||
|
|
||||||
await expect(getQuotas(mockSurveyId)).rejects.toThrow("Generic error");
|
await expect(getQuotas(TEST_IDS.survey)).rejects.toThrow("Generic error");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("createQuota", () => {
|
describe("createQuota", () => {
|
||||||
const createInput: TSurveyQuotaInput = {
|
const createInput: TSurveyQuotaInput = {
|
||||||
surveyId: mockSurveyId,
|
surveyId: TEST_IDS.survey,
|
||||||
name: "New Quota",
|
name: "New Quota",
|
||||||
limit: 50,
|
limit: 50,
|
||||||
logic: {
|
logic: {
|
||||||
@@ -155,11 +145,7 @@ describe("Quota Service", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should throw DatabaseError on Prisma error", async () => {
|
test("should throw DatabaseError on Prisma error", async () => {
|
||||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Database error", {
|
vi.mocked(prisma.surveyQuota.create).mockRejectedValue(COMMON_ERRORS.UNIQUE_CONSTRAINT);
|
||||||
code: "P2002",
|
|
||||||
clientVersion: "1.0.0",
|
|
||||||
});
|
|
||||||
vi.mocked(prisma.surveyQuota.create).mockRejectedValue(prismaError);
|
|
||||||
|
|
||||||
await expect(createQuota(createInput)).rejects.toThrow(InvalidInputError);
|
await expect(createQuota(createInput)).rejects.toThrow(InvalidInputError);
|
||||||
});
|
});
|
||||||
@@ -175,7 +161,7 @@ describe("Quota Service", () => {
|
|||||||
describe("updateQuota", () => {
|
describe("updateQuota", () => {
|
||||||
const updateInput: TSurveyQuotaInput = {
|
const updateInput: TSurveyQuotaInput = {
|
||||||
name: "Updated Quota",
|
name: "Updated Quota",
|
||||||
surveyId: mockSurveyId,
|
surveyId: TEST_IDS.survey,
|
||||||
limit: 75,
|
limit: 75,
|
||||||
logic: {
|
logic: {
|
||||||
connector: "or",
|
connector: "or",
|
||||||
@@ -190,38 +176,35 @@ describe("Quota Service", () => {
|
|||||||
const updatedQuota = { ...mockQuota, ...updateInput };
|
const updatedQuota = { ...mockQuota, ...updateInput };
|
||||||
vi.mocked(prisma.surveyQuota.update).mockResolvedValue(updatedQuota);
|
vi.mocked(prisma.surveyQuota.update).mockResolvedValue(updatedQuota);
|
||||||
|
|
||||||
const result = await updateQuota(updateInput, mockQuotaId);
|
const result = await updateQuota(updateInput, TEST_IDS.quota);
|
||||||
|
|
||||||
expect(result).toEqual(updatedQuota);
|
expect(result).toEqual(updatedQuota);
|
||||||
expect(prisma.surveyQuota.update).toHaveBeenCalledWith({
|
expect(prisma.surveyQuota.update).toHaveBeenCalledWith({
|
||||||
where: { id: mockQuotaId },
|
where: { id: TEST_IDS.quota },
|
||||||
data: updateInput,
|
data: updateInput,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should throw DatabaseError when quota not found", async () => {
|
test("should throw DatabaseError when quota not found", async () => {
|
||||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Record not found", {
|
// P2015 is the "required relation violation" code that maps to ResourceNotFoundError
|
||||||
|
const notFoundError = new Prisma.PrismaClientKnownRequestError("Record not found", {
|
||||||
code: "P2015",
|
code: "P2015",
|
||||||
clientVersion: "1.0.0",
|
clientVersion: "1.0.0",
|
||||||
});
|
});
|
||||||
vi.mocked(prisma.surveyQuota.update).mockRejectedValue(prismaError);
|
vi.mocked(prisma.surveyQuota.update).mockRejectedValue(notFoundError);
|
||||||
|
|
||||||
await expect(updateQuota(updateInput, mockQuotaId)).rejects.toThrow(ResourceNotFoundError);
|
await expect(updateQuota(updateInput, TEST_IDS.quota)).rejects.toThrow(ResourceNotFoundError);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should throw DatabaseError on other Prisma errors", async () => {
|
test("should throw DatabaseError on other Prisma errors", async () => {
|
||||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Database error", {
|
vi.mocked(prisma.surveyQuota.update).mockRejectedValue(COMMON_ERRORS.UNIQUE_CONSTRAINT);
|
||||||
code: "P2002",
|
|
||||||
clientVersion: "1.0.0",
|
|
||||||
});
|
|
||||||
vi.mocked(prisma.surveyQuota.update).mockRejectedValue(prismaError);
|
|
||||||
|
|
||||||
await expect(updateQuota(updateInput, mockQuotaId)).rejects.toThrow(InvalidInputError);
|
await expect(updateQuota(updateInput, TEST_IDS.quota)).rejects.toThrow(InvalidInputError);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should throw error on unknown error", async () => {
|
test("should throw error on unknown error", async () => {
|
||||||
vi.mocked(prisma.surveyQuota.update).mockRejectedValue(new Error("Unknown error"));
|
vi.mocked(prisma.surveyQuota.update).mockRejectedValue(new Error("Unknown error"));
|
||||||
await expect(updateQuota(updateInput, mockQuotaId)).rejects.toThrow(Error);
|
await expect(updateQuota(updateInput, TEST_IDS.quota)).rejects.toThrow(Error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -229,64 +212,57 @@ describe("Quota Service", () => {
|
|||||||
test("should delete quota successfully", async () => {
|
test("should delete quota successfully", async () => {
|
||||||
vi.mocked(prisma.surveyQuota.delete).mockResolvedValue(mockQuota);
|
vi.mocked(prisma.surveyQuota.delete).mockResolvedValue(mockQuota);
|
||||||
|
|
||||||
const result = await deleteQuota(mockQuotaId);
|
const result = await deleteQuota(TEST_IDS.quota);
|
||||||
|
|
||||||
expect(result).toEqual(mockQuota);
|
expect(result).toEqual(mockQuota);
|
||||||
expect(prisma.surveyQuota.delete).toHaveBeenCalledWith({
|
expect(prisma.surveyQuota.delete).toHaveBeenCalledWith({
|
||||||
where: { id: mockQuotaId },
|
where: { id: TEST_IDS.quota },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should throw DatabaseError when quota not found", async () => {
|
test("should throw DatabaseError when quota not found", async () => {
|
||||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Record not found", {
|
// P2015 is the "required relation violation" code that maps to ResourceNotFoundError
|
||||||
|
const notFoundError = new Prisma.PrismaClientKnownRequestError("Record not found", {
|
||||||
code: "P2015",
|
code: "P2015",
|
||||||
clientVersion: "1.0.0",
|
clientVersion: "1.0.0",
|
||||||
});
|
});
|
||||||
vi.mocked(prisma.surveyQuota.delete).mockRejectedValue(prismaError);
|
vi.mocked(prisma.surveyQuota.delete).mockRejectedValue(notFoundError);
|
||||||
|
|
||||||
await expect(deleteQuota(mockQuotaId)).rejects.toThrow(ResourceNotFoundError);
|
await expect(deleteQuota(TEST_IDS.quota)).rejects.toThrow(ResourceNotFoundError);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should throw DatabaseError on other Prisma errors", async () => {
|
test("should throw DatabaseError on other Prisma errors", async () => {
|
||||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Database error", {
|
vi.mocked(prisma.surveyQuota.delete).mockRejectedValue(COMMON_ERRORS.UNIQUE_CONSTRAINT);
|
||||||
code: "P2002",
|
|
||||||
clientVersion: "1.0.0",
|
|
||||||
});
|
|
||||||
vi.mocked(prisma.surveyQuota.delete).mockRejectedValue(prismaError);
|
|
||||||
|
|
||||||
await expect(deleteQuota(mockQuotaId)).rejects.toThrow(DatabaseError);
|
await expect(deleteQuota(TEST_IDS.quota)).rejects.toThrow(DatabaseError);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should re-throw non-Prisma errors", async () => {
|
test("should re-throw non-Prisma errors", async () => {
|
||||||
const genericError = new Error("Generic error");
|
const genericError = new Error("Generic error");
|
||||||
vi.mocked(prisma.surveyQuota.delete).mockRejectedValue(genericError);
|
vi.mocked(prisma.surveyQuota.delete).mockRejectedValue(genericError);
|
||||||
|
|
||||||
await expect(deleteQuota(mockQuotaId)).rejects.toThrow("Generic error");
|
await expect(deleteQuota(TEST_IDS.quota)).rejects.toThrow("Generic error");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("reduceQuotaLimits", () => {
|
describe("reduceQuotaLimits", () => {
|
||||||
test("should reduce quota limits successfully", async () => {
|
test("should reduce quota limits successfully", async () => {
|
||||||
vi.mocked(prisma.surveyQuota.updateMany).mockResolvedValue({ count: 1 });
|
vi.mocked(prisma.surveyQuota.updateMany).mockResolvedValue({ count: 1 });
|
||||||
await reduceQuotaLimits([mockQuotaId]);
|
await reduceQuotaLimits([TEST_IDS.quota]);
|
||||||
expect(prisma.surveyQuota.updateMany).toHaveBeenCalledWith({
|
expect(prisma.surveyQuota.updateMany).toHaveBeenCalledWith({
|
||||||
where: { id: { in: [mockQuotaId] }, limit: { gt: 1 } },
|
where: { id: { in: [TEST_IDS.quota] }, limit: { gt: 1 } },
|
||||||
data: { limit: { decrement: 1 } },
|
data: { limit: { decrement: 1 } },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should throw DatabaseError on Prisma error", async () => {
|
test("should throw DatabaseError on Prisma error", async () => {
|
||||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Database error", {
|
vi.mocked(prisma.surveyQuota.updateMany).mockRejectedValue(COMMON_ERRORS.UNIQUE_CONSTRAINT);
|
||||||
code: "P2002",
|
await expect(reduceQuotaLimits([TEST_IDS.quota])).rejects.toThrow(DatabaseError);
|
||||||
clientVersion: "1.0.0",
|
|
||||||
});
|
|
||||||
vi.mocked(prisma.surveyQuota.updateMany).mockRejectedValue(prismaError);
|
|
||||||
await expect(reduceQuotaLimits([mockQuotaId])).rejects.toThrow(DatabaseError);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should throw error on unknown error", async () => {
|
test("should throw error on unknown error", async () => {
|
||||||
vi.mocked(prisma.surveyQuota.updateMany).mockRejectedValue(new Error("Unknown error"));
|
vi.mocked(prisma.surveyQuota.updateMany).mockRejectedValue(new Error("Unknown error"));
|
||||||
await expect(reduceQuotaLimits([mockQuotaId])).rejects.toThrow(Error);
|
await expect(reduceQuotaLimits([TEST_IDS.quota])).rejects.toThrow(Error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -184,6 +184,11 @@ export const testInputValidation = async (service: Function, ...args: any[]): Pr
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Export new testing utilities for easy access
|
||||||
|
export { setupTestEnvironment } from "./lib/testing/setup";
|
||||||
|
export { TEST_IDS, FIXTURES } from "./lib/testing/constants";
|
||||||
|
export * from "./lib/testing/mocks";
|
||||||
|
|
||||||
vi.mock("@/lib/constants", () => ({
|
vi.mock("@/lib/constants", () => ({
|
||||||
IS_FORMBRICKS_CLOUD: false,
|
IS_FORMBRICKS_CLOUD: false,
|
||||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||||
|
|||||||
Reference in New Issue
Block a user