mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-21 18:18:48 -06:00
fix: recaptcha feature bugs (#5599)
This commit is contained in:
@@ -125,7 +125,7 @@ export const updateSurveyAction = authenticatedActionClient
|
||||
const { followUps } = parsedInput;
|
||||
|
||||
if (parsedInput.recaptcha?.enabled) {
|
||||
await checkSpamProtectionPermission();
|
||||
await checkSpamProtectionPermission(organizationId);
|
||||
}
|
||||
|
||||
if (followUps?.length) {
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
capturePosthogEnvironmentEvent,
|
||||
sendPlanLimitsReachedEventToPosthogWeekly,
|
||||
} from "@/lib/posthogServer";
|
||||
import { getIsSpamProtectionEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { logger } from "@formbricks/logger";
|
||||
@@ -48,6 +47,8 @@ vi.mock("./survey");
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: true, // Default to false, override in specific tests
|
||||
RECAPTCHA_SITE_KEY: "mock_recaptcha_site_key",
|
||||
RECAPTCHA_SECRET_KEY: "mock_recaptcha_secret_key",
|
||||
IS_RECAPTCHA_CONFIGURED: true,
|
||||
IS_PRODUCTION: true,
|
||||
IS_POSTHOG_CONFIGURED: false,
|
||||
ENTERPRISE_LICENSE_KEY: "mock_enterprise_license_key",
|
||||
@@ -255,7 +256,6 @@ describe("getEnvironmentState", () => {
|
||||
vi.mocked(getProjectForEnvironmentState).mockResolvedValue(mockProject);
|
||||
vi.mocked(getSurveysForEnvironmentState).mockResolvedValue(mockSurveys);
|
||||
vi.mocked(getActionClassesForEnvironmentState).mockResolvedValue(mockActionClasses);
|
||||
vi.mocked(getIsSpamProtectionEnabled).mockResolvedValue(false); // Default to false
|
||||
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(50); // Default below limit
|
||||
});
|
||||
|
||||
@@ -267,6 +267,7 @@ describe("getEnvironmentState", () => {
|
||||
const result = await getEnvironmentState(environmentId);
|
||||
|
||||
const expectedData: TJsEnvironmentState["data"] = {
|
||||
recaptchaSiteKey: "mock_recaptcha_site_key",
|
||||
surveys: [mockSurveys[0]], // Only app, inProgress survey
|
||||
actionClasses: mockActionClasses,
|
||||
project: mockProject,
|
||||
@@ -279,7 +280,6 @@ describe("getEnvironmentState", () => {
|
||||
expect(getProjectForEnvironmentState).toHaveBeenCalledWith(environmentId);
|
||||
expect(getSurveysForEnvironmentState).toHaveBeenCalledWith(environmentId);
|
||||
expect(getActionClassesForEnvironmentState).toHaveBeenCalledWith(environmentId);
|
||||
expect(getIsSpamProtectionEnabled).toHaveBeenCalled();
|
||||
expect(prisma.environment.update).not.toHaveBeenCalled();
|
||||
expect(capturePosthogEnvironmentEvent).not.toHaveBeenCalled();
|
||||
expect(getMonthlyOrganizationResponseCount).toHaveBeenCalled(); // Not cloud
|
||||
@@ -358,22 +358,12 @@ describe("getEnvironmentState", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("should include recaptchaSiteKey if spam protection is enabled", async () => {
|
||||
vi.mocked(getIsSpamProtectionEnabled).mockResolvedValue(true);
|
||||
|
||||
test("should include recaptchaSiteKey if recaptcha variables are set", async () => {
|
||||
const result = await getEnvironmentState(environmentId);
|
||||
|
||||
expect(result.data.recaptchaSiteKey).toBe("mock_recaptcha_site_key");
|
||||
});
|
||||
|
||||
test("should not include recaptchaSiteKey if spam protection is disabled", async () => {
|
||||
vi.mocked(getIsSpamProtectionEnabled).mockResolvedValue(false);
|
||||
|
||||
const result = await getEnvironmentState(environmentId);
|
||||
|
||||
expect(result.data.recaptchaSiteKey).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should filter surveys correctly (only app type and inProgress status)", async () => {
|
||||
const result = await getEnvironmentState(environmentId);
|
||||
expect(result.data.surveys).toHaveLength(1);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { actionClassCache } from "@/lib/actionClass/cache";
|
||||
import { cache } from "@/lib/cache";
|
||||
import { IS_FORMBRICKS_CLOUD, RECAPTCHA_SITE_KEY } from "@/lib/constants";
|
||||
import { IS_FORMBRICKS_CLOUD, IS_RECAPTCHA_CONFIGURED, RECAPTCHA_SITE_KEY } from "@/lib/constants";
|
||||
import { environmentCache } from "@/lib/environment/cache";
|
||||
import { getEnvironment } from "@/lib/environment/service";
|
||||
import { organizationCache } from "@/lib/organization/cache";
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
} from "@/lib/posthogServer";
|
||||
import { projectCache } from "@/lib/project/cache";
|
||||
import { surveyCache } from "@/lib/survey/cache";
|
||||
import { getIsSpamProtectionEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
@@ -99,7 +98,6 @@ export const getEnvironmentState = async (
|
||||
getSurveysForEnvironmentState(environmentId),
|
||||
getActionClassesForEnvironmentState(environmentId),
|
||||
]);
|
||||
const isSpamProtectionEnabled = await getIsSpamProtectionEnabled();
|
||||
|
||||
const filteredSurveys = surveys.filter(
|
||||
(survey) => survey.type === "app" && survey.status === "inProgress"
|
||||
@@ -109,7 +107,7 @@ export const getEnvironmentState = async (
|
||||
surveys: !isMonthlyResponsesLimitReached ? filteredSurveys : [],
|
||||
actionClasses,
|
||||
project: project,
|
||||
...(isSpamProtectionEnabled ? { recaptchaSiteKey: RECAPTCHA_SITE_KEY } : {}),
|
||||
...(IS_RECAPTCHA_CONFIGURED ? { recaptchaSiteKey: RECAPTCHA_SITE_KEY } : {}),
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -9,7 +9,7 @@ export const checkFeaturePermissions = async (
|
||||
organization: TOrganization
|
||||
): Promise<Response | null> => {
|
||||
if (surveyData.recaptcha?.enabled) {
|
||||
const isSpamProtectionEnabled = await getIsSpamProtectionEnabled();
|
||||
const isSpamProtectionEnabled = await getIsSpamProtectionEnabled(organization.billing.plan);
|
||||
if (!isSpamProtectionEnabled) {
|
||||
return responses.forbiddenResponse("Spam protection is not enabled for this organization");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Organization } from "@prisma/client";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { getOrganizationBillingByEnvironmentId } from "./organization";
|
||||
|
||||
vi.mock("@formbricks/database", () => ({
|
||||
prisma: {
|
||||
organization: {
|
||||
findFirst: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
vi.mock("@formbricks/logger", () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
vi.mock("@/lib/cache", () => ({
|
||||
cache: (fn: any) => fn,
|
||||
}));
|
||||
vi.mock("@/lib/organization/cache", () => ({
|
||||
organizationCache: {
|
||||
tag: {
|
||||
byEnvironmentId: (id: string) => `tag-${id}`,
|
||||
},
|
||||
},
|
||||
}));
|
||||
vi.mock("react", () => ({
|
||||
cache: (fn: any) => fn,
|
||||
}));
|
||||
|
||||
describe("getOrganizationBillingByEnvironmentId", () => {
|
||||
const environmentId = "env-123";
|
||||
const mockBillingData: Organization["billing"] = {
|
||||
limits: {
|
||||
monthly: { miu: 0, responses: 0 },
|
||||
projects: 3,
|
||||
},
|
||||
period: "monthly",
|
||||
periodStart: new Date(),
|
||||
plan: "scale",
|
||||
stripeCustomerId: "mock-stripe-customer-id",
|
||||
};
|
||||
|
||||
test("returns billing when organization is found", async () => {
|
||||
vi.mocked(prisma.organization.findFirst).mockResolvedValue({ billing: mockBillingData });
|
||||
const result = await getOrganizationBillingByEnvironmentId(environmentId);
|
||||
expect(result).toEqual(mockBillingData);
|
||||
expect(prisma.organization.findFirst).toHaveBeenCalledWith({
|
||||
where: {
|
||||
projects: {
|
||||
some: {
|
||||
environments: {
|
||||
some: {
|
||||
id: environmentId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
billing: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("returns null when organization is not found", async () => {
|
||||
vi.mocked(prisma.organization.findFirst).mockResolvedValueOnce(null);
|
||||
const result = await getOrganizationBillingByEnvironmentId(environmentId);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test("logs error and returns null on exception", async () => {
|
||||
const error = new Error("db error");
|
||||
vi.mocked(prisma.organization.findFirst).mockRejectedValueOnce(error);
|
||||
const result = await getOrganizationBillingByEnvironmentId(environmentId);
|
||||
expect(result).toBeNull();
|
||||
expect(logger.error).toHaveBeenCalledWith(error, "Failed to get organization billing by environment ID");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { cache } from "@/lib/cache";
|
||||
import { organizationCache } from "@/lib/organization/cache";
|
||||
import { Organization } from "@prisma/client";
|
||||
import { cache as reactCache } from "react";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { logger } from "@formbricks/logger";
|
||||
|
||||
export const getOrganizationBillingByEnvironmentId = reactCache(
|
||||
async (environmentId: string): Promise<Organization["billing"] | null> =>
|
||||
cache(
|
||||
async () => {
|
||||
try {
|
||||
const organization = await prisma.organization.findFirst({
|
||||
where: {
|
||||
projects: {
|
||||
some: {
|
||||
environments: {
|
||||
some: {
|
||||
id: environmentId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
billing: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!organization) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return organization.billing;
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to get organization billing by environment ID");
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[`api-v2-client-getOrganizationBillingByEnvironmentId-${environmentId}`],
|
||||
{
|
||||
tags: [organizationCache.tag.byEnvironmentId(environmentId)],
|
||||
}
|
||||
)()
|
||||
);
|
||||
@@ -1,9 +1,11 @@
|
||||
import { getOrganizationBillingByEnvironmentId } from "@/app/api/v2/client/[environmentId]/responses/lib/organization";
|
||||
import { verifyRecaptchaToken } from "@/app/api/v2/client/[environmentId]/responses/lib/recaptcha";
|
||||
import { checkSurveyValidity } from "@/app/api/v2/client/[environmentId]/responses/lib/utils";
|
||||
import { TResponseInputV2 } from "@/app/api/v2/client/[environmentId]/responses/types/response";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { getIsSpamProtectionEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { Organization } from "@prisma/client";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
@@ -14,6 +16,7 @@ vi.mock("@/app/api/v2/client/[environmentId]/responses/lib/recaptcha", () => ({
|
||||
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 })),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -21,6 +24,10 @@ vi.mock("@/modules/ee/license-check/lib/utils", () => ({
|
||||
getIsSpamProtectionEnabled: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/api/v2/client/[environmentId]/responses/lib/organization", () => ({
|
||||
getOrganizationBillingByEnvironmentId: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@formbricks/logger", () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
@@ -76,7 +83,22 @@ const mockResponseInput: TResponseInputV2 = {
|
||||
meta: {},
|
||||
};
|
||||
|
||||
const mockBillingData: Organization["billing"] = {
|
||||
limits: {
|
||||
monthly: { miu: 0, responses: 0 },
|
||||
projects: 3,
|
||||
},
|
||||
period: "monthly",
|
||||
periodStart: new Date(),
|
||||
plan: "scale",
|
||||
stripeCustomerId: "mock-stripe-customer-id",
|
||||
};
|
||||
|
||||
describe("checkSurveyValidity", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
test("should return badRequestResponse if survey environmentId does not match", async () => {
|
||||
const survey = { ...mockSurvey, environmentId: "env-2" };
|
||||
const result = await checkSurveyValidity(survey, "env-1", mockResponseInput);
|
||||
@@ -98,18 +120,6 @@ describe("checkSurveyValidity", () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test("should return null if recaptcha is enabled but spam protection is disabled", async () => {
|
||||
const survey = { ...mockSurvey, recaptcha: { enabled: true, threshold: 0.5 } };
|
||||
vi.mocked(getIsSpamProtectionEnabled).mockResolvedValue(false);
|
||||
vi.mocked(verifyRecaptchaToken).mockResolvedValue(true);
|
||||
const result = await checkSurveyValidity(survey, "env-1", {
|
||||
...mockResponseInput,
|
||||
recaptchaToken: "test-token",
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
expect(logger.error).toHaveBeenCalledWith("Spam protection is not enabled for this organization");
|
||||
});
|
||||
|
||||
test("should return badRequestResponse if recaptcha enabled, spam protection enabled, but token is missing", async () => {
|
||||
const survey = { ...mockSurvey, recaptcha: { enabled: true, threshold: 0.5 } };
|
||||
vi.mocked(getIsSpamProtectionEnabled).mockResolvedValue(true);
|
||||
@@ -127,11 +137,40 @@ describe("checkSurveyValidity", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("should return not found response if billing data is not found", async () => {
|
||||
const survey = { ...mockSurvey, recaptcha: { enabled: true, threshold: 0.5 } };
|
||||
vi.mocked(getIsSpamProtectionEnabled).mockResolvedValue(true);
|
||||
vi.mocked(getOrganizationBillingByEnvironmentId).mockResolvedValue(null);
|
||||
|
||||
const result = await checkSurveyValidity(survey, "env-1", {
|
||||
...mockResponseInput,
|
||||
recaptchaToken: "test-token",
|
||||
});
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
expect(result?.status).toBe(404);
|
||||
expect(responses.notFoundResponse).toHaveBeenCalledWith("Organization", null);
|
||||
expect(getOrganizationBillingByEnvironmentId).toHaveBeenCalledWith("env-1");
|
||||
});
|
||||
|
||||
test("should return null if recaptcha is enabled but spam protection is disabled", async () => {
|
||||
const survey = { ...mockSurvey, recaptcha: { enabled: true, threshold: 0.5 } };
|
||||
vi.mocked(getIsSpamProtectionEnabled).mockResolvedValue(false);
|
||||
vi.mocked(verifyRecaptchaToken).mockResolvedValue(true);
|
||||
vi.mocked(getOrganizationBillingByEnvironmentId).mockResolvedValue(mockBillingData);
|
||||
const result = await checkSurveyValidity(survey, "env-1", {
|
||||
...mockResponseInput,
|
||||
recaptchaToken: "test-token",
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
expect(logger.error).toHaveBeenCalledWith("Spam protection is not enabled for this organization");
|
||||
});
|
||||
|
||||
test("should return badRequestResponse if recaptcha verification fails", async () => {
|
||||
const survey = { ...mockSurvey, recaptcha: { enabled: true, threshold: 0.5 } };
|
||||
const responseInputWithToken = { ...mockResponseInput, recaptchaToken: "test-token" };
|
||||
vi.mocked(getIsSpamProtectionEnabled).mockResolvedValue(true);
|
||||
vi.mocked(verifyRecaptchaToken).mockResolvedValue(false);
|
||||
vi.mocked(getOrganizationBillingByEnvironmentId).mockResolvedValue(mockBillingData);
|
||||
|
||||
const result = await checkSurveyValidity(survey, "env-1", responseInputWithToken);
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
@@ -149,6 +188,7 @@ describe("checkSurveyValidity", () => {
|
||||
const responseInputWithToken = { ...mockResponseInput, recaptchaToken: "test-token" };
|
||||
vi.mocked(getIsSpamProtectionEnabled).mockResolvedValue(true);
|
||||
vi.mocked(verifyRecaptchaToken).mockResolvedValue(true);
|
||||
vi.mocked(getOrganizationBillingByEnvironmentId).mockResolvedValue(mockBillingData);
|
||||
|
||||
const result = await checkSurveyValidity(survey, "env-1", responseInputWithToken);
|
||||
expect(result).toBeNull();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getOrganizationBillingByEnvironmentId } from "@/app/api/v2/client/[environmentId]/responses/lib/organization";
|
||||
import { verifyRecaptchaToken } from "@/app/api/v2/client/[environmentId]/responses/lib/recaptcha";
|
||||
import { TResponseInputV2 } from "@/app/api/v2/client/[environmentId]/responses/types/response";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
@@ -34,7 +35,13 @@ export const checkSurveyValidity = async (
|
||||
true
|
||||
);
|
||||
}
|
||||
const isSpamProtectionEnabled = await getIsSpamProtectionEnabled();
|
||||
const billing = await getOrganizationBillingByEnvironmentId(environmentId);
|
||||
|
||||
if (!billing) {
|
||||
return responses.notFoundResponse("Organization", null);
|
||||
}
|
||||
|
||||
const isSpamProtectionEnabled = await getIsSpamProtectionEnabled(billing.plan);
|
||||
|
||||
if (!isSpamProtectionEnabled) {
|
||||
logger.error("Spam protection is not enabled for this organization");
|
||||
|
||||
@@ -391,13 +391,18 @@ export const getIsSamlSsoEnabled = async (): Promise<boolean> => {
|
||||
return licenseFeatures.sso && licenseFeatures.saml;
|
||||
};
|
||||
|
||||
export const getIsSpamProtectionEnabled = async (): Promise<boolean> => {
|
||||
export const getIsSpamProtectionEnabled = async (
|
||||
billingPlan: Organization["billing"]["plan"]
|
||||
): Promise<boolean> => {
|
||||
if (!IS_RECAPTCHA_CONFIGURED) return false;
|
||||
|
||||
if (E2E_TESTING) {
|
||||
const previousResult = await fetchLicenseForE2ETesting();
|
||||
return previousResult?.features ? previousResult.features.spamProtection : false;
|
||||
}
|
||||
if (IS_FORMBRICKS_CLOUD)
|
||||
return billingPlan === PROJECT_FEATURE_KEYS.SCALE || billingPlan === PROJECT_FEATURE_KEYS.ENTERPRISE;
|
||||
|
||||
const licenseFeatures = await getLicenseFeatures();
|
||||
if (!licenseFeatures) return false;
|
||||
return licenseFeatures.spamProtection;
|
||||
|
||||
@@ -58,7 +58,7 @@ export const createSurveyAction = authenticatedActionClient
|
||||
});
|
||||
|
||||
if (parsedInput.surveyBody.recaptcha?.enabled) {
|
||||
await checkSpamProtectionPermission();
|
||||
await checkSpamProtectionPermission(organizationId);
|
||||
}
|
||||
|
||||
if (parsedInput.surveyBody.followUps?.length) {
|
||||
|
||||
@@ -63,7 +63,7 @@ export const updateSurveyAction = authenticatedActionClient
|
||||
});
|
||||
|
||||
if (parsedInput.recaptcha?.enabled) {
|
||||
await checkSpamProtectionPermission();
|
||||
await checkSpamProtectionPermission(organizationId);
|
||||
}
|
||||
|
||||
if (parsedInput.followUps?.length) {
|
||||
|
||||
@@ -67,7 +67,7 @@ export const SurveyEditorPage = async (props) => {
|
||||
const isUserTargetingAllowed = await getIsContactsEnabled();
|
||||
const isMultiLanguageAllowed = await getMultiLanguagePermission(organizationBilling.plan);
|
||||
const isSurveyFollowUpsAllowed = await getSurveyFollowUpsPermission(organizationBilling.plan);
|
||||
const isSpamProtectionAllowed = await getIsSpamProtectionEnabled();
|
||||
const isSpamProtectionAllowed = await getIsSpamProtectionEnabled(organizationBilling.plan);
|
||||
|
||||
const userEmail = await getUserEmail(session.user.id);
|
||||
const projectLanguages = await getProjectLanguages(projectWithTeamIds.id);
|
||||
|
||||
@@ -1,27 +1,52 @@
|
||||
import { getIsSpamProtectionEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getOrganizationBilling } from "@/modules/survey/lib/survey";
|
||||
import { Organization } from "@prisma/client";
|
||||
import { cleanup } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
||||
import { OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { checkSpamProtectionPermission } from "./permission";
|
||||
|
||||
vi.mock("@/modules/ee/license-check/lib/utils", () => ({
|
||||
getIsSpamProtectionEnabled: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/survey/lib/survey", () => ({
|
||||
getOrganizationBilling: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("checkSpamProtectionPermission", () => {
|
||||
const mockOrganizationId = "mock-organization-id";
|
||||
const mockBillingData: Organization["billing"] = {
|
||||
limits: {
|
||||
monthly: { miu: 0, responses: 0 },
|
||||
projects: 3,
|
||||
},
|
||||
period: "monthly",
|
||||
periodStart: new Date(),
|
||||
plan: "scale",
|
||||
stripeCustomerId: "mock-stripe-customer-id",
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("throws ResourceNotFoundError if organization is not found", async () => {
|
||||
vi.mocked(getOrganizationBilling).mockResolvedValue(null);
|
||||
await expect(checkSpamProtectionPermission(mockOrganizationId)).rejects.toThrow(ResourceNotFoundError);
|
||||
});
|
||||
|
||||
test("resolves if spam protection is enabled", async () => {
|
||||
vi.mocked(getOrganizationBilling).mockResolvedValue(mockBillingData);
|
||||
vi.mocked(getIsSpamProtectionEnabled).mockResolvedValue(true);
|
||||
await expect(checkSpamProtectionPermission()).resolves.toBeUndefined();
|
||||
await expect(checkSpamProtectionPermission(mockOrganizationId)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
test("throws OperationNotAllowedError if spam protection is not enabled", async () => {
|
||||
vi.mocked(getOrganizationBilling).mockResolvedValue(mockBillingData);
|
||||
vi.mocked(getIsSpamProtectionEnabled).mockResolvedValue(false);
|
||||
await expect(checkSpamProtectionPermission()).rejects.toThrow(OperationNotAllowedError);
|
||||
await expect(checkSpamProtectionPermission()).rejects.toThrow(
|
||||
await expect(checkSpamProtectionPermission(mockOrganizationId)).rejects.toThrow(OperationNotAllowedError);
|
||||
await expect(checkSpamProtectionPermission(mockOrganizationId)).rejects.toThrow(
|
||||
"Spam protection is not enabled for this organization"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import { getIsSpamProtectionEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
||||
import { getOrganizationBilling } from "@/modules/survey/lib/survey";
|
||||
import { OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
|
||||
/**
|
||||
* Checks if the organization has spam protection enabled.
|
||||
*
|
||||
* @param {string} organizationId - The ID of the organization to check.
|
||||
* @returns {Promise<void>} A promise that resolves if spam protection is enabled.
|
||||
* @throws {ResourceNotFoundError} If the organization is not found.
|
||||
* @throws {OperationNotAllowedError} If spam protection is not enabled for the organization.
|
||||
*/
|
||||
export const checkSpamProtectionPermission = async (): Promise<void> => {
|
||||
const isSpamProtectionEnabled = await getIsSpamProtectionEnabled();
|
||||
export const checkSpamProtectionPermission = async (organizationId: string): Promise<void> => {
|
||||
const organizationBilling = await getOrganizationBilling(organizationId);
|
||||
if (!organizationBilling) {
|
||||
throw new ResourceNotFoundError("Organization", organizationId);
|
||||
}
|
||||
|
||||
const isSpamProtectionEnabled = await getIsSpamProtectionEnabled(organizationBilling.plan);
|
||||
if (!isSpamProtectionEnabled) {
|
||||
throw new OperationNotAllowedError("Spam protection is not enabled for this organization");
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import {
|
||||
IMPRINT_URL,
|
||||
IS_FORMBRICKS_CLOUD,
|
||||
IS_RECAPTCHA_CONFIGURED,
|
||||
PRIVACY_URL,
|
||||
RECAPTCHA_SITE_KEY,
|
||||
WEBAPP_URL,
|
||||
} from "@/lib/constants";
|
||||
import { getSurveyDomain } from "@/lib/getSurveyUrl";
|
||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||
import { getIsSpamProtectionEnabled, getMultiLanguagePermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getMultiLanguagePermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@/modules/survey/lib/organization";
|
||||
import { getResponseCountBySurveyId } from "@/modules/survey/lib/response";
|
||||
import { getOrganizationBilling } from "@/modules/survey/lib/survey";
|
||||
@@ -57,8 +58,7 @@ export const renderSurvey = async ({
|
||||
}
|
||||
const isMultiLanguageAllowed = await getMultiLanguagePermission(organizationBilling.plan);
|
||||
|
||||
const isSpamProtectionAllowed = await getIsSpamProtectionEnabled();
|
||||
const isSpamProtectionEnabled = Boolean(isSpamProtectionAllowed && survey.recaptcha?.enabled);
|
||||
const isSpamProtectionEnabled = Boolean(IS_RECAPTCHA_CONFIGURED && survey.recaptcha?.enabled);
|
||||
|
||||
if (survey.status !== "inProgress" && !isPreview) {
|
||||
return (
|
||||
|
||||
@@ -38,6 +38,7 @@ export default defineConfig({
|
||||
"modules/ui/components/environmentId-base-layout/*.tsx",
|
||||
"modules/ui/components/survey/recaptcha.ts",
|
||||
"app/api/v2/client/**/responses/lib/recaptcha.ts",
|
||||
"app/api/v2/client/**/responses/lib/organization.ts",
|
||||
"app/api/v2/client/**/responses/lib/utils.ts",
|
||||
"modules/ui/components/progress-bar/index.tsx",
|
||||
"app/(app)/environments/**/layout.tsx",
|
||||
|
||||
Reference in New Issue
Block a user