fix: unformatted db message in client display api (#6176)

This commit is contained in:
Dhruwang Jariwala
2025-07-29 09:38:16 +05:30
committed by GitHub
parent 2166c44470
commit b1a35d4a69
7 changed files with 297 additions and 18 deletions
@@ -0,0 +1,222 @@
import { validateInputs } from "@/lib/utils/validate";
import { Prisma } from "@prisma/client";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { TDisplayCreateInput } from "@formbricks/types/displays";
import { DatabaseError, ResourceNotFoundError, ValidationError } from "@formbricks/types/errors";
import { getContactByUserId } from "./contact";
import { createDisplay } from "./display";
vi.mock("@/lib/utils/validate", () => ({
validateInputs: vi.fn((inputs) => inputs.map((input) => input[0])), // Pass through validation for testing
}));
vi.mock("@formbricks/database", () => ({
prisma: {
contact: {
create: vi.fn(),
},
display: {
create: vi.fn(),
},
survey: {
findUnique: vi.fn(),
},
},
}));
vi.mock("./contact", () => ({
getContactByUserId: vi.fn(),
}));
const environmentId = "test-env-id";
const surveyId = "test-survey-id";
const userId = "test-user-id";
const contactId = "test-contact-id";
const displayId = "test-display-id";
const displayInput: TDisplayCreateInput = {
environmentId,
surveyId,
userId,
};
const displayInputWithoutUserId: TDisplayCreateInput = {
environmentId,
surveyId,
};
const mockContact = {
id: contactId,
environmentId,
userId,
createdAt: new Date(),
updatedAt: new Date(),
};
const mockDisplay = {
id: displayId,
contactId,
surveyId,
responseId: null,
status: null,
createdAt: new Date(),
updatedAt: new Date(),
};
const mockDisplayWithoutContact = {
id: displayId,
contactId: null,
surveyId,
responseId: null,
status: null,
createdAt: new Date(),
updatedAt: new Date(),
};
const mockSurvey = {
id: surveyId,
name: "Test Survey",
environmentId,
} as any;
describe("createDisplay", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(prisma.survey.findUnique).mockResolvedValue(mockSurvey);
});
test("should create a display with existing contact successfully", async () => {
vi.mocked(getContactByUserId).mockResolvedValue(mockContact);
vi.mocked(prisma.display.create).mockResolvedValue(mockDisplay);
const result = await createDisplay(displayInput);
expect(validateInputs).toHaveBeenCalledWith([displayInput, expect.any(Object)]);
expect(getContactByUserId).toHaveBeenCalledWith(environmentId, userId);
expect(prisma.contact.create).not.toHaveBeenCalled();
expect(prisma.display.create).toHaveBeenCalledWith({
data: {
survey: { connect: { id: surveyId } },
contact: { connect: { id: contactId } },
},
select: { id: true, contactId: true, surveyId: true },
});
expect(result).toEqual(mockDisplay);
});
test("should create a display and new contact when contact does not exist", async () => {
vi.mocked(getContactByUserId).mockResolvedValue(null);
vi.mocked(prisma.contact.create).mockResolvedValue(mockContact);
vi.mocked(prisma.display.create).mockResolvedValue(mockDisplay);
const result = await createDisplay(displayInput);
expect(validateInputs).toHaveBeenCalledWith([displayInput, expect.any(Object)]);
expect(getContactByUserId).toHaveBeenCalledWith(environmentId, userId);
expect(prisma.contact.create).toHaveBeenCalledWith({
data: {
environment: { connect: { id: environmentId } },
attributes: {
create: {
attributeKey: {
connect: { key_environmentId: { key: "userId", environmentId } },
},
value: userId,
},
},
},
});
expect(prisma.display.create).toHaveBeenCalledWith({
data: {
survey: { connect: { id: surveyId } },
contact: { connect: { id: contactId } },
},
select: { id: true, contactId: true, surveyId: true },
});
expect(result).toEqual(mockDisplay);
});
test("should create a display without contact when userId is not provided", async () => {
vi.mocked(prisma.display.create).mockResolvedValue(mockDisplayWithoutContact);
const result = await createDisplay(displayInputWithoutUserId);
expect(validateInputs).toHaveBeenCalledWith([displayInputWithoutUserId, expect.any(Object)]);
expect(getContactByUserId).not.toHaveBeenCalled();
expect(prisma.contact.create).not.toHaveBeenCalled();
expect(prisma.display.create).toHaveBeenCalledWith({
data: {
survey: { connect: { id: surveyId } },
},
select: { id: true, contactId: true, surveyId: true },
});
expect(result).toEqual(mockDisplayWithoutContact);
});
test("should throw ValidationError if validation fails", async () => {
const validationError = new ValidationError("Validation failed");
vi.mocked(validateInputs).mockImplementation(() => {
throw validationError;
});
await expect(createDisplay(displayInput)).rejects.toThrow(ValidationError);
expect(getContactByUserId).not.toHaveBeenCalled();
expect(prisma.display.create).not.toHaveBeenCalled();
});
test("should throw InvalidInputError when survey does not exist (RelatedRecordDoesNotExist)", async () => {
vi.mocked(getContactByUserId).mockResolvedValue(mockContact);
vi.mocked(prisma.survey.findUnique).mockResolvedValue(null);
await expect(createDisplay(displayInput)).rejects.toThrow(new ResourceNotFoundError("Survey", surveyId));
expect(getContactByUserId).toHaveBeenCalledWith(environmentId, userId);
expect(prisma.survey.findUnique).toHaveBeenCalledWith({
where: { id: surveyId, environmentId },
});
expect(prisma.display.create).not.toHaveBeenCalled();
});
test("should throw DatabaseError on other Prisma known request errors", async () => {
const prismaError = new Prisma.PrismaClientKnownRequestError("Database error", {
code: "P2002",
clientVersion: "2.0.0",
});
vi.mocked(getContactByUserId).mockResolvedValue(mockContact);
vi.mocked(prisma.display.create).mockRejectedValue(prismaError);
await expect(createDisplay(displayInput)).rejects.toThrow(DatabaseError);
expect(getContactByUserId).toHaveBeenCalledWith(environmentId, userId);
expect(prisma.display.create).toHaveBeenCalled();
});
test("should throw original error on other errors during display creation", async () => {
const genericError = new Error("Something went wrong");
vi.mocked(getContactByUserId).mockResolvedValue(mockContact);
vi.mocked(prisma.display.create).mockRejectedValue(genericError);
await expect(createDisplay(displayInput)).rejects.toThrow(genericError);
expect(getContactByUserId).toHaveBeenCalledWith(environmentId, userId);
expect(prisma.display.create).toHaveBeenCalled();
});
test("should throw error if getContactByUserId fails", async () => {
const contactError = new Error("Failed to get contact");
vi.mocked(getContactByUserId).mockRejectedValue(contactError);
await expect(createDisplay(displayInput)).rejects.toThrow(contactError);
expect(getContactByUserId).toHaveBeenCalledWith(environmentId, userId);
expect(prisma.display.create).not.toHaveBeenCalled();
});
test("should throw error if contact creation fails", async () => {
const contactCreateError = new Error("Failed to create contact");
vi.mocked(getContactByUserId).mockResolvedValue(null);
vi.mocked(prisma.contact.create).mockRejectedValue(contactCreateError);
await expect(createDisplay(displayInput)).rejects.toThrow(contactCreateError);
expect(getContactByUserId).toHaveBeenCalledWith(environmentId, userId);
expect(prisma.contact.create).toHaveBeenCalled();
expect(prisma.display.create).not.toHaveBeenCalled();
});
});
@@ -2,7 +2,7 @@ import { validateInputs } from "@/lib/utils/validate";
import { Prisma } from "@prisma/client";
import { prisma } from "@formbricks/database";
import { TDisplayCreateInput, ZDisplayCreateInput } from "@formbricks/types/displays";
import { DatabaseError } from "@formbricks/types/errors";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { getContactByUserId } from "./contact";
export const createDisplay = async (displayInput: TDisplayCreateInput): Promise<{ id: string }> => {
@@ -31,6 +31,16 @@ export const createDisplay = async (displayInput: TDisplayCreateInput): Promise<
}
}
const survey = await prisma.survey.findUnique({
where: {
id: surveyId,
environmentId,
},
});
if (!survey) {
throw new ResourceNotFoundError("Survey", surveyId);
}
const display = await prisma.display.create({
data: {
survey: {
@@ -4,7 +4,7 @@ import { capturePosthogEnvironmentEvent } from "@/lib/posthogServer";
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
import { logger } from "@formbricks/logger";
import { ZDisplayCreateInput } from "@formbricks/types/displays";
import { InvalidInputError } from "@formbricks/types/errors";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { createDisplay } from "./lib/display";
interface Context {
@@ -52,11 +52,11 @@ export const POST = async (request: Request, context: Context): Promise<Response
await capturePosthogEnvironmentEvent(inputValidation.data.environmentId, "display created");
return responses.successResponse(response, true);
} catch (error) {
if (error instanceof InvalidInputError) {
return responses.badRequestResponse(error.message);
if (error instanceof ResourceNotFoundError) {
return responses.notFoundResponse("Survey", inputValidation.data.surveyId);
} else {
logger.error({ error, url: request.url }, "Error in POST /api/v1/client/[environmentId]/displays");
return responses.internalServerErrorResponse(error.message);
return responses.internalServerErrorResponse("Something went wrong. Please try again.");
}
}
};
@@ -2,7 +2,7 @@ import { validateInputs } from "@/lib/utils/validate";
import { Prisma } from "@prisma/client";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { DatabaseError, ValidationError } from "@formbricks/types/errors";
import { DatabaseError, ResourceNotFoundError, ValidationError } from "@formbricks/types/errors";
import { TDisplayCreateInputV2 } from "../types/display";
import { doesContactExist } from "./contact";
import { createDisplay } from "./display";
@@ -16,6 +16,9 @@ vi.mock("@formbricks/database", () => ({
display: {
create: vi.fn(),
},
survey: {
findUnique: vi.fn(),
},
},
}));
@@ -43,17 +46,32 @@ const mockDisplay = {
id: displayId,
contactId,
surveyId,
responseId: null,
status: null,
createdAt: new Date(),
updatedAt: new Date(),
};
const mockDisplayWithoutContact = {
id: displayId,
contactId: null,
surveyId,
responseId: null,
status: null,
createdAt: new Date(),
updatedAt: new Date(),
};
const mockSurvey = {
id: surveyId,
name: "Test Survey",
environmentId,
} as any;
describe("createDisplay", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(prisma.survey.findUnique).mockResolvedValue(mockSurvey);
});
test("should create a display with contactId successfully", async () => {
@@ -119,7 +137,19 @@ describe("createDisplay", () => {
expect(prisma.display.create).not.toHaveBeenCalled();
});
test("should throw DatabaseError on Prisma known request error", async () => {
test("should throw InvalidInputError when survey does not exist (P2025)", async () => {
vi.mocked(doesContactExist).mockResolvedValue(true);
vi.mocked(prisma.survey.findUnique).mockResolvedValue(null);
await expect(createDisplay(displayInput)).rejects.toThrow(new ResourceNotFoundError("Survey", surveyId));
expect(doesContactExist).toHaveBeenCalledWith(contactId);
expect(prisma.survey.findUnique).toHaveBeenCalledWith({
where: { id: surveyId, environmentId },
});
expect(prisma.display.create).not.toHaveBeenCalled();
});
test("should throw DatabaseError on other Prisma known request errors", async () => {
const prismaError = new Prisma.PrismaClientKnownRequestError("DB error", {
code: "P2002",
clientVersion: "2.0.0",
@@ -5,17 +5,27 @@ import {
import { validateInputs } from "@/lib/utils/validate";
import { Prisma } from "@prisma/client";
import { prisma } from "@formbricks/database";
import { DatabaseError } from "@formbricks/types/errors";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { doesContactExist } from "./contact";
export const createDisplay = async (displayInput: TDisplayCreateInputV2): Promise<{ id: string }> => {
validateInputs([displayInput, ZDisplayCreateInputV2]);
const { contactId, surveyId } = displayInput;
const { contactId, surveyId, environmentId } = displayInput;
try {
const contactExists = contactId ? await doesContactExist(contactId) : false;
const survey = await prisma.survey.findUnique({
where: {
id: surveyId,
environmentId,
},
});
if (!survey) {
throw new ResourceNotFoundError("Survey", surveyId);
}
const display = await prisma.display.create({
data: {
survey: {
@@ -4,7 +4,7 @@ import { transformErrorToDetails } from "@/app/lib/api/validator";
import { capturePosthogEnvironmentEvent } from "@/lib/posthogServer";
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
import { logger } from "@formbricks/logger";
import { InvalidInputError } from "@formbricks/types/errors";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { createDisplay } from "./lib/display";
interface Context {
@@ -52,11 +52,11 @@ export const POST = async (request: Request, context: Context): Promise<Response
await capturePosthogEnvironmentEvent(inputValidation.data.environmentId, "display created");
return responses.successResponse(response, true);
} catch (error) {
if (error instanceof InvalidInputError) {
return responses.badRequestResponse(error.message);
if (error instanceof ResourceNotFoundError) {
return responses.notFoundResponse("Survey", inputValidation.data.surveyId);
} else {
logger.error({ error, url: request.url }, "Error creating display");
return responses.internalServerErrorResponse(error.message);
return responses.internalServerErrorResponse("Something went wrong. Please try again.");
}
}
};
+12 -5
View File
@@ -5,6 +5,8 @@ import {
mockDisplayInputWithUserId,
mockDisplayWithPersonId,
mockEnvironment,
mockEnvironmentId,
mockSurveyId,
} from "./__mocks__/data.mock";
import { prisma } from "@/lib/__mocks__/database";
import { createDisplay } from "@/app/api/v1/client/[environmentId]/displays/lib/display";
@@ -26,20 +28,25 @@ afterEach(() => {
beforeEach(() => {
prisma.contact.findFirst.mockResolvedValue(mockContact);
prisma.survey.findUnique.mockResolvedValue({
id: mockSurveyId,
name: "Test Survey",
environmentId: mockEnvironmentId,
} as any);
});
describe("Tests for createDisplay service", () => {
describe("Happy Path", () => {
test("Creates a new display when a userId exists", async () => {
prisma.environment.findUnique.mockResolvedValue(mockEnvironment);
prisma.display.create.mockResolvedValue(mockDisplayWithPersonId);
prisma.environment.findUnique.mockResolvedValue(mockEnvironment as any);
prisma.display.create.mockResolvedValue(mockDisplayWithPersonId as any);
const display = await createDisplay(mockDisplayInputWithUserId);
expect(display).toEqual(mockDisplayWithPersonId);
});
test("Creates a new display when a userId does not exists", async () => {
prisma.display.create.mockResolvedValue(mockDisplay);
prisma.display.create.mockResolvedValue(mockDisplay as any);
const display = await createDisplay(mockDisplayInput);
expect(display).toEqual(mockDisplay);
@@ -51,7 +58,7 @@ describe("Tests for createDisplay service", () => {
test("Throws DatabaseError on PrismaClientKnownRequestError occurrence", async () => {
const mockErrorMessage = "Mock error message";
prisma.environment.findUnique.mockResolvedValue(mockEnvironment);
prisma.environment.findUnique.mockResolvedValue(mockEnvironment as any);
const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, {
code: PrismaErrorType.UniqueConstraintViolation,
clientVersion: "0.0.1",
@@ -74,7 +81,7 @@ describe("Tests for createDisplay service", () => {
describe("Tests for delete display service", () => {
describe("Happy Path", () => {
test("Deletes a display", async () => {
prisma.display.delete.mockResolvedValue(mockDisplay);
prisma.display.delete.mockResolvedValue(mockDisplay as any);
const display = await deleteDisplay(mockDisplay.id);
expect(display).toEqual(mockDisplay);