fix: fixes displays and responses api about survey status (#8007)

This commit is contained in:
Anshuman Pandey
2026-05-14 18:31:27 +05:30
committed by GitHub
parent 96b08fbe23
commit efe259d484
10 changed files with 89 additions and 6 deletions
@@ -2,7 +2,12 @@ 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 {
DatabaseError,
InvalidInputError,
ResourceNotFoundError,
ValidationError,
} from "@formbricks/types/errors";
import { validateInputs } from "@/lib/utils/validate";
import { getContactByUserId } from "./contact";
import { createDisplay } from "./display";
@@ -78,6 +83,7 @@ const mockSurvey = {
id: surveyId,
name: "Test Survey",
environmentId,
status: "inProgress",
} as any;
describe("createDisplay", () => {
@@ -177,6 +183,17 @@ describe("createDisplay", () => {
expect(prisma.display.create).not.toHaveBeenCalled();
});
test.each(["draft", "paused", "completed"])(
"should throw InvalidInputError when survey status is %s",
async (status) => {
vi.mocked(getContactByUserId).mockResolvedValue(mockContact);
vi.mocked(prisma.survey.findUnique).mockResolvedValue({ ...mockSurvey, status } as any);
await expect(createDisplay(displayInput)).rejects.toThrow(InvalidInputError);
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",
@@ -1,7 +1,7 @@
import { Prisma } from "@prisma/client";
import { prisma } from "@formbricks/database";
import { TDisplayCreateInput, ZDisplayCreateInput } from "@formbricks/types/displays";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
import { validateInputs } from "@/lib/utils/validate";
import { getContactByUserId } from "./contact";
@@ -41,6 +41,10 @@ export const createDisplay = async (displayInput: TDisplayCreateInput): Promise<
throw new ResourceNotFoundError("Survey", surveyId);
}
if (survey.status !== "inProgress") {
throw new InvalidInputError("Survey is not accepting submissions");
}
const display = await prisma.display.create({
data: {
survey: {
@@ -1,6 +1,6 @@
import { logger } from "@formbricks/logger";
import { ZDisplayCreateInput } from "@formbricks/types/displays";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { THandlerParams, withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
@@ -61,6 +61,12 @@ export const POST = withV1ApiWrapper({
return {
response: responses.notFoundResponse("Survey", inputValidation.data.surveyId),
};
} else if (error instanceof InvalidInputError) {
return {
response: responses.forbiddenResponse(error.message, true, {
surveyId: inputValidation.data.surveyId,
}),
};
} else {
logger.error({ error, url: req.url }, "Error in POST /api/v1/client/[environmentId]/displays");
return {
@@ -128,6 +128,14 @@ export const POST = withV1ApiWrapper({
};
}
if (survey.status !== "inProgress") {
return {
response: responses.forbiddenResponse("Survey is not accepting submissions", true, {
surveyId: survey.id,
}),
};
}
const singleUseValidationResult = validateSingleUseResponseInput(
survey,
environmentId,
@@ -1,7 +1,12 @@
import { Prisma } from "@prisma/client";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { DatabaseError, ResourceNotFoundError, ValidationError } from "@formbricks/types/errors";
import {
DatabaseError,
InvalidInputError,
ResourceNotFoundError,
ValidationError,
} from "@formbricks/types/errors";
import { validateInputs } from "@/lib/utils/validate";
import { TDisplayCreateInputV2 } from "../types/display";
import { doesContactExist } from "./contact";
@@ -66,6 +71,7 @@ const mockSurvey = {
id: surveyId,
name: "Test Survey",
environmentId,
status: "inProgress",
} as any;
describe("createDisplay", () => {
@@ -149,6 +155,17 @@ describe("createDisplay", () => {
expect(prisma.display.create).not.toHaveBeenCalled();
});
test.each(["draft", "paused", "completed"])(
"should throw InvalidInputError when survey status is %s",
async (status) => {
vi.mocked(doesContactExist).mockResolvedValue(true);
vi.mocked(prisma.survey.findUnique).mockResolvedValue({ ...mockSurvey, status } as any);
await expect(createDisplay(displayInput)).rejects.toThrow(InvalidInputError);
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",
@@ -1,6 +1,6 @@
import { Prisma } from "@prisma/client";
import { prisma } from "@formbricks/database";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
import {
TDisplayCreateInputV2,
ZDisplayCreateInputV2,
@@ -26,6 +26,10 @@ export const createDisplay = async (displayInput: TDisplayCreateInputV2): Promis
throw new ResourceNotFoundError("Survey", surveyId);
}
if (survey.status !== "inProgress") {
throw new InvalidInputError("Survey is not accepting submissions");
}
const display = await prisma.display.create({
data: {
survey: {
@@ -1,4 +1,4 @@
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
import {
TDisplayCreateInputV2,
ZDisplayCreateInputV2,
@@ -79,6 +79,12 @@ export const POST = async (request: Request, context: Context): Promise<Response
return responses.notFoundResponse("Survey", displayInputData.surveyId, true);
}
if (error instanceof InvalidInputError) {
return responses.forbiddenResponse(error.message, true, {
surveyId: displayInputData.surveyId,
});
}
const response = responses.internalServerErrorResponse("Something went wrong. Please try again.", true);
reportApiError({
request,
@@ -26,6 +26,7 @@ vi.mock("@/app/lib/api/response", () => ({
responses: {
badRequestResponse: vi.fn((message) => new Response(message, { status: 400 })),
notFoundResponse: vi.fn((message) => new Response(message, { status: 404 })),
forbiddenResponse: vi.fn((message) => new Response(message, { status: 403 })),
},
}));
@@ -138,6 +139,19 @@ describe("checkSurveyValidity", () => {
);
});
test.each(["draft", "paused", "completed"] as const)(
"should return forbiddenResponse when survey status is %s",
async (status) => {
const survey = { ...mockSurvey, status } as TSurvey;
const result = await checkSurveyValidity(survey, "env-1", mockResponseInput);
expect(result).toBeInstanceOf(Response);
expect(result?.status).toBe(403);
expect(responses.forbiddenResponse).toHaveBeenCalledWith("Survey is not accepting submissions", true, {
surveyId: mockSurvey.id,
});
}
);
test("should return null if recaptcha is not enabled", async () => {
const survey = { ...mockSurvey, recaptcha: { enabled: false, threshold: 0.5 } };
const result = await checkSurveyValidity(survey, "env-1", mockResponseInput);
@@ -19,6 +19,12 @@ export const checkSurveyValidity = async (
return responses.badRequestResponse("Survey does not belong to this environment", undefined, true);
}
if (survey.status !== "inProgress") {
return responses.forbiddenResponse("Survey is not accepting submissions", true, {
surveyId: survey.id,
});
}
const singleUseValidationResult = validateSingleUseResponseInput(survey, environmentId, responseInput);
if (singleUseValidationResult) {
if ("response" in singleUseValidationResult) {
@@ -32,6 +32,7 @@ beforeEach(() => {
id: mockSurveyId,
name: "Test Survey",
environmentId: mockEnvironmentId,
status: "inProgress",
} as any);
});