Files
formbricks/apps/web/app/api/v3/surveys/reference-validation.test.ts
T
2026-05-20 16:33:33 +00:00

270 lines
7.4 KiB
TypeScript

import { describe, expect, test } from "vitest";
import { validateV3SurveyReferences } from "./reference-validation";
import { ZV3CreateSurveyBody } from "./schemas";
const validSurvey = ZV3CreateSurveyBody.parse({
workspaceId: "clxx1234567890123456789012",
name: "Product Feedback",
hiddenFields: {
enabled: true,
fieldIds: ["account_id"],
},
variables: [
{
id: "clvar123456789012345678901",
name: "score",
type: "number",
value: 0,
},
],
endings: [
{
id: "clend123456789012345678901",
type: "endScreen",
headline: { "en-US": "Thanks" },
},
],
blocks: [
{
id: "clbk1234567890123456789012",
name: "Main Block",
logicFallback: "clend123456789012345678901",
elements: [
{
id: "satisfaction",
type: "openText",
headline: { "en-US": "What should we improve?" },
required: true,
},
],
logic: [
{
id: "cllog123456789012345678901",
conditions: {
id: "clgrp123456789012345678901",
connector: "and",
conditions: [
{
id: "clcon123456789012345678901",
leftOperand: { type: "element", value: "satisfaction" },
operator: "isSubmitted",
},
],
},
actions: [
{
id: "clact123456789012345678901",
objective: "calculate",
variableId: "clvar123456789012345678901",
operator: "add",
value: { type: "static", value: 1 },
},
],
},
],
},
],
});
describe("validateV3SurveyReferences", () => {
test("accepts a survey with consistent stable identifiers", () => {
expect(
validateV3SurveyReferences({
blocks: validSurvey.blocks,
endings: validSurvey.endings,
hiddenFields: validSurvey.hiddenFields,
variables: validSurvey.variables,
})
).toEqual({ ok: true, invalidParams: [] });
});
test("rejects duplicate block, element, variable, and hidden field identifiers", () => {
const survey = {
...validSurvey,
hiddenFields: { enabled: true, fieldIds: ["account_id", "account_id"] },
variables: [
...validSurvey.variables,
{
id: "clvar123456789012345678901",
name: "score",
type: "number" as const,
value: 0,
},
],
blocks: [
...validSurvey.blocks,
{
...validSurvey.blocks[0],
elements: [{ ...validSurvey.blocks[0].elements[0] }],
},
],
};
const result = validateV3SurveyReferences({
blocks: survey.blocks,
endings: survey.endings,
hiddenFields: survey.hiddenFields,
variables: survey.variables,
});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.invalidParams).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: "blocks.1.id" }),
expect.objectContaining({ name: "blocks.1.elements.0.id" }),
expect.objectContaining({ name: "variables.1.id" }),
expect.objectContaining({ name: "hiddenFields.fieldIds.1" }),
])
);
}
});
test("rejects cross-namespace identifier collisions", () => {
const result = validateV3SurveyReferences({
blocks: validSurvey.blocks,
endings: validSurvey.endings,
hiddenFields: { enabled: true, fieldIds: ["account_id", "satisfaction"] },
variables: [
{
id: "satisfaction",
name: "account_id",
type: "number",
value: 0,
},
],
});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.invalidParams).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: "hiddenFields.fieldIds.1" }),
expect.objectContaining({ name: "variables.0.id" }),
expect.objectContaining({ name: "variables.0.name" }),
])
);
}
});
test("reports dangling logic references with actionable paths", () => {
const survey = {
...validSurvey,
blocks: [
{
...validSurvey.blocks[0],
logicFallback: "clmiss12345678901234567890",
logic: [
{
...validSurvey.blocks[0].logic![0],
actions: [
{
...validSurvey.blocks[0].logic![0].actions[0],
variableId: "clmiss12345678901234567890",
},
{
id: "cljmp123456789012345678901",
objective: "jumpToBlock" as const,
target: "clmiss12345678901234567890",
},
],
},
],
},
],
};
const result = validateV3SurveyReferences({
blocks: survey.blocks,
endings: survey.endings,
hiddenFields: survey.hiddenFields,
variables: survey.variables,
});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.invalidParams).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: "blocks.0.logicFallback" }),
expect.objectContaining({ name: "blocks.0.logic.0.actions.0.variableId" }),
expect.objectContaining({ name: "blocks.0.logic.0.actions.1.target" }),
])
);
}
});
test("reports dangling recall references with actionable paths", () => {
const survey = {
...validSurvey,
blocks: [
{
...validSurvey.blocks[0],
elements: [
{
...validSurvey.blocks[0].elements[0],
headline: {
default: "Please explain #recall:missing_id/fallback:your answer#",
},
},
],
},
],
};
const result = validateV3SurveyReferences({
blocks: survey.blocks,
endings: survey.endings,
hiddenFields: survey.hiddenFields,
variables: survey.variables,
});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.invalidParams).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "blocks.0.elements.0.headline.default",
reason: expect.stringContaining("missing_id"),
}),
])
);
}
});
test("reports dangling recall references in survey-level translatable fields", () => {
const result = validateV3SurveyReferences({
blocks: validSurvey.blocks,
endings: validSurvey.endings,
hiddenFields: validSurvey.hiddenFields,
metadata: {
title: {
default: "Metadata #recall:missing_metadata_reference/fallback:value#",
},
},
variables: validSurvey.variables,
welcomeCard: {
enabled: true,
headline: {
default: "Welcome #recall:missing_welcome_reference/fallback:there#",
},
},
});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.invalidParams).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "welcomeCard.headline.default",
reason: expect.stringContaining("missing_welcome_reference"),
}),
expect.objectContaining({
name: "metadata.title.default",
reason: expect.stringContaining("missing_metadata_reference"),
}),
])
);
}
});
});