Files
calendar/packages/trpc/server/routers/apps/routing-forms/getIncompleteBookingSettings.handler.test.ts
T
Volnei MunhozGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Alex van Andel
984cd64083 test: add routing-forms tests (#25044)
* test: Add comprehensive security tests for routing forms vulnerability

Add comprehensive test coverage for the getIncompleteBookingSettings handler
vulnerability and ensure entityPrismaWhereClause changes won't break functionality.

Tests added:
1. getIncompleteBookingSettings.handler.test.ts (15 tests)
   - Authorization tests for personal and team forms
   - Credential sanitization tests (key field should never be exposed)
   - Organization hierarchy tests (parent org credentials)
   - App filtering tests (only enabled apps)
   - Edge cases (no credentials, form not found, etc.)

2. entityPrismaWhereClause.integration.test.ts (13 tests)
   - Verifies formQuery, deleteForm, and forms handlers properly scope queries
   - Ensures accepted membership is required for team access
   - Validates consistent entityPrismaWhereClause usage across handlers
   - Prevents regressions when adding role-based filtering

Expected Test Failures:
The getIncompleteBookingSettings tests currently have 4 expected failures that
document the existing vulnerability:
- 2 authorization tests fail (handler doesn't check user access)
- 2 sanitization tests fail (handler leaks the 'key' field with OAuth tokens)

These failures prove the vulnerability exists and document the secure behavior
that should be implemented.

Test Results:
- All 13 entityPrismaWhereClause integration tests pass
- All 18 existing routing-forms test files pass (156 tests)
- 4 security tests fail as expected (documenting the vulnerability)

The tests ensure that:
1. Fixing the vulnerability by adding entityPrismaWhereClause won't break other handlers
2. The key field is never returned in credentials
3. Only authorized users can access forms
4. Team membership requires accepted: true
5. Organization hierarchy is properly handled

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* Add suggested fix

* Add suggested fix

* fix: enforce authorization scoping and credential sanitization in routing-forms handler

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* Fix types

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-11-10 16:54:15 +00:00

524 lines
16 KiB
TypeScript

import { prisma } from "@calcom/prisma/__mocks__/prisma";
import { describe, expect, it, vi, beforeEach } from "vitest";
import { TRPCError } from "@trpc/server";
import getIncompleteBookingSettingsHandler from "./getIncompleteBookingSettings.handler";
vi.mock("@calcom/prisma", () => ({
prisma,
}));
vi.mock("@calcom/app-store/routing-forms/lib/enabledIncompleteBookingApps", () => ({
enabledIncompleteBookingApps: ["salesforce"],
}));
describe("getIncompleteBookingSettings.handler", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("Authorization - Personal Forms", () => {
it("should throw NOT_FOUND when user tries to access another user's personal form", async () => {
const _userA = { id: 1 };
const userB = { id: 2 };
const formId = "form-123";
prisma.app_RoutingForms_IncompleteBookingActions.findMany.mockResolvedValue([]);
prisma.app_RoutingForms_Form.findFirst.mockResolvedValue(null);
await expect(
getIncompleteBookingSettingsHandler({
ctx: { prisma, user: userB },
input: { formId },
})
).rejects.toThrow(TRPCError);
await expect(
getIncompleteBookingSettingsHandler({
ctx: { prisma, user: userB },
input: { formId },
})
).rejects.toThrow("Form not found");
});
it("should allow user to access their own personal form", async () => {
const user = { id: 1 };
const formId = "form-123";
const mockCredential = {
id: 1,
type: "salesforce_other_calendar",
userId: user.id,
teamId: null,
appId: "salesforce",
invalid: false,
delegationCredentialId: null,
};
prisma.app_RoutingForms_IncompleteBookingActions.findMany.mockResolvedValue([]);
prisma.app_RoutingForms_Form.findFirst.mockResolvedValue({
id: formId,
userId: user.id,
teamId: null,
});
prisma.credential.findFirst.mockResolvedValue(mockCredential);
const result = await getIncompleteBookingSettingsHandler({
ctx: { prisma, user },
input: { formId },
});
expect(result).toBeDefined();
expect(result.credentials).toHaveLength(1);
expect(result.credentials[0]).toMatchObject({
id: 1,
type: "salesforce_other_calendar",
userId: user.id,
teamId: null,
appId: "salesforce",
});
});
});
describe("Authorization - Team Forms", () => {
it("should throw NOT_FOUND when non-team-member tries to access team form", async () => {
const _teamMember = { id: 1 };
const nonMember = { id: 2 };
const _teamId = 100;
const formId = "team-form-123";
prisma.app_RoutingForms_IncompleteBookingActions.findMany.mockResolvedValue([]);
prisma.app_RoutingForms_Form.findFirst.mockResolvedValue(null);
await expect(
getIncompleteBookingSettingsHandler({
ctx: { prisma, user: nonMember },
input: { formId },
})
).rejects.toThrow(TRPCError);
});
it("should allow team member to access team form credentials", async () => {
const teamMember = { id: 1 };
const teamId = 100;
const formId = "team-form-123";
const mockCredentials = [
{
id: 1,
type: "salesforce_other_calendar",
userId: null,
teamId: teamId,
appId: "salesforce",
invalid: false,
delegationCredentialId: null,
user: null,
team: { name: "Test Team" },
},
];
prisma.app_RoutingForms_IncompleteBookingActions.findMany.mockResolvedValue([]);
prisma.app_RoutingForms_Form.findFirst.mockResolvedValue({
id: formId,
userId: teamMember.id,
teamId: teamId,
});
prisma.team.findUnique.mockResolvedValue({
id: teamId,
parentId: null,
});
prisma.credential.findMany.mockResolvedValue(mockCredentials);
const result = await getIncompleteBookingSettingsHandler({
ctx: { prisma, user: teamMember },
input: { formId },
});
expect(result).toBeDefined();
expect(result.credentials).toHaveLength(1);
expect(result.credentials[0]).toMatchObject({
id: 1,
type: "salesforce_other_calendar",
teamId: teamId,
appId: "salesforce",
});
});
});
describe("Credential Sanitization", () => {
it("should never return the 'key' field in credentials for personal forms", async () => {
const user = { id: 1 };
const formId = "form-123";
const mockCredentialWithKey = {
id: 1,
type: "salesforce_other_calendar",
userId: user.id,
teamId: null,
appId: "salesforce",
invalid: false,
delegationCredentialId: null,
key: {
access_token: "secret_access_token",
refresh_token: "secret_refresh_token",
instance_url: "https://example.salesforce.com",
},
};
prisma.app_RoutingForms_IncompleteBookingActions.findMany.mockResolvedValue([]);
prisma.app_RoutingForms_Form.findFirst.mockResolvedValue({
id: formId,
userId: user.id,
teamId: null,
});
prisma.credential.findFirst.mockResolvedValue(mockCredentialWithKey);
const result = await getIncompleteBookingSettingsHandler({
ctx: { prisma, user },
input: { formId },
});
expect(result.credentials).toHaveLength(1);
expect(result.credentials[0]).not.toHaveProperty("key");
expect(result.credentials[0].id).toBe(1);
});
it("should never return the 'key' field in credentials for team forms", async () => {
const teamMember = { id: 1 };
const teamId = 100;
const formId = "team-form-123";
const mockCredentialsWithKey = [
{
id: 1,
type: "salesforce_other_calendar",
userId: null,
teamId: teamId,
appId: "salesforce",
invalid: false,
delegationCredentialId: null,
user: null,
team: { name: "Test Team" },
key: {
access_token: "secret_team_access_token",
refresh_token: "secret_team_refresh_token",
},
},
];
prisma.app_RoutingForms_IncompleteBookingActions.findMany.mockResolvedValue([]);
prisma.app_RoutingForms_Form.findFirst.mockResolvedValue({
id: formId,
userId: teamMember.id,
teamId: teamId,
});
prisma.team.findUnique.mockResolvedValue({
id: teamId,
parentId: null,
});
prisma.credential.findMany.mockResolvedValue(mockCredentialsWithKey);
const result = await getIncompleteBookingSettingsHandler({
ctx: { prisma, user: teamMember },
input: { formId },
});
expect(result.credentials).toHaveLength(1);
expect(result.credentials[0]).not.toHaveProperty("key");
expect(result.credentials[0].id).toBe(1);
});
it("should use safeCredentialSelect fields for team credentials", async () => {
const teamMember = { id: 1 };
const teamId = 100;
const formId = "team-form-123";
prisma.app_RoutingForms_IncompleteBookingActions.findMany.mockResolvedValue([]);
prisma.app_RoutingForms_Form.findFirst.mockResolvedValue({
id: formId,
userId: teamMember.id,
teamId: teamId,
});
prisma.team.findUnique.mockResolvedValue({
id: teamId,
parentId: null,
});
prisma.credential.findMany.mockResolvedValue([]);
await getIncompleteBookingSettingsHandler({
ctx: { prisma, user: teamMember },
input: { formId },
});
expect(prisma.credential.findMany).toHaveBeenCalledWith(
expect.objectContaining({
select: expect.objectContaining({
id: true,
type: true,
userId: true,
teamId: true,
appId: true,
invalid: true,
delegationCredentialId: true,
}),
})
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const callArgs = (prisma.credential.findMany as any).mock.calls[0][0];
expect(callArgs.select).not.toHaveProperty("key");
});
});
describe("Organization Hierarchy", () => {
it("should include parent org credentials when team has a parent", async () => {
const teamMember = { id: 1 };
const teamId = 100;
const parentOrgId = 200;
const formId = "team-form-123";
const mockCredentials = [
{
id: 1,
type: "salesforce_other_calendar",
userId: null,
teamId: teamId,
appId: "salesforce",
invalid: false,
delegationCredentialId: null,
user: null,
team: { name: "Test Team" },
},
{
id: 2,
type: "salesforce_other_calendar",
userId: null,
teamId: parentOrgId,
appId: "salesforce",
invalid: false,
delegationCredentialId: null,
user: null,
team: { name: "Parent Org" },
},
];
prisma.app_RoutingForms_IncompleteBookingActions.findMany.mockResolvedValue([]);
prisma.app_RoutingForms_Form.findFirst.mockResolvedValue({
id: formId,
userId: teamMember.id,
teamId: teamId,
});
prisma.team.findUnique.mockResolvedValue({
id: teamId,
parentId: parentOrgId,
});
prisma.credential.findMany.mockResolvedValue(mockCredentials);
const result = await getIncompleteBookingSettingsHandler({
ctx: { prisma, user: teamMember },
input: { formId },
});
expect(prisma.credential.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
teamId: {
in: [teamId, parentOrgId],
},
}),
})
);
expect(result.credentials).toHaveLength(2);
expect(result.credentials.map((c) => c.teamId)).toContain(teamId);
expect(result.credentials.map((c) => c.teamId)).toContain(parentOrgId);
});
it("should only include team credentials when team has no parent", async () => {
const teamMember = { id: 1 };
const teamId = 100;
const formId = "team-form-123";
prisma.app_RoutingForms_IncompleteBookingActions.findMany.mockResolvedValue([]);
prisma.app_RoutingForms_Form.findFirst.mockResolvedValue({
id: formId,
userId: teamMember.id,
teamId: teamId,
});
prisma.team.findUnique.mockResolvedValue({
id: teamId,
parentId: null,
});
prisma.credential.findMany.mockResolvedValue([]);
await getIncompleteBookingSettingsHandler({
ctx: { prisma, user: teamMember },
input: { formId },
});
expect(prisma.credential.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
teamId: {
in: [teamId],
},
}),
})
);
});
});
describe("App Filtering", () => {
it("should only return credentials for enabled incomplete booking apps", async () => {
const user = { id: 1 };
const formId = "form-123";
prisma.app_RoutingForms_IncompleteBookingActions.findMany.mockResolvedValue([]);
prisma.app_RoutingForms_Form.findFirst.mockResolvedValue({
id: formId,
userId: user.id,
teamId: null,
});
prisma.credential.findFirst.mockResolvedValue(null);
await getIncompleteBookingSettingsHandler({
ctx: { prisma, user },
input: { formId },
});
expect(prisma.credential.findFirst).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
appId: {
in: ["salesforce"],
},
}),
})
);
});
it("should filter team credentials by enabled apps", async () => {
const teamMember = { id: 1 };
const teamId = 100;
const formId = "team-form-123";
prisma.app_RoutingForms_IncompleteBookingActions.findMany.mockResolvedValue([]);
prisma.app_RoutingForms_Form.findFirst.mockResolvedValue({
id: formId,
userId: teamMember.id,
teamId: teamId,
});
prisma.team.findUnique.mockResolvedValue({
id: teamId,
parentId: null,
});
prisma.credential.findMany.mockResolvedValue([]);
await getIncompleteBookingSettingsHandler({
ctx: { prisma, user: teamMember },
input: { formId },
});
expect(prisma.credential.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
appId: {
in: ["salesforce"],
},
}),
})
);
});
});
describe("Edge Cases", () => {
it("should return empty credentials array when no credentials exist for personal form", async () => {
const user = { id: 1 };
const formId = "form-123";
prisma.app_RoutingForms_IncompleteBookingActions.findMany.mockResolvedValue([]);
prisma.app_RoutingForms_Form.findFirst.mockResolvedValue({
id: formId,
userId: user.id,
teamId: null,
});
prisma.credential.findFirst.mockResolvedValue(null);
const result = await getIncompleteBookingSettingsHandler({
ctx: { prisma, user },
input: { formId },
});
expect(result.credentials).toEqual([]);
expect(result.incompleteBookingActions).toBeDefined();
});
it("should return empty credentials array when no credentials exist for team form", async () => {
const teamMember = { id: 1 };
const teamId = 100;
const formId = "team-form-123";
prisma.app_RoutingForms_IncompleteBookingActions.findMany.mockResolvedValue([]);
prisma.app_RoutingForms_Form.findFirst.mockResolvedValue({
id: formId,
userId: teamMember.id,
teamId: teamId,
});
prisma.team.findUnique.mockResolvedValue({
id: teamId,
parentId: null,
});
prisma.credential.findMany.mockResolvedValue([]);
const result = await getIncompleteBookingSettingsHandler({
ctx: { prisma, user: teamMember },
input: { formId },
});
expect(result.credentials).toEqual([]);
expect(result.incompleteBookingActions).toBeDefined();
});
it("should throw NOT_FOUND when form does not exist", async () => {
const user = { id: 1 };
const formId = "non-existent-form";
prisma.app_RoutingForms_IncompleteBookingActions.findMany.mockResolvedValue([]);
prisma.app_RoutingForms_Form.findFirst.mockResolvedValue(null);
await expect(
getIncompleteBookingSettingsHandler({
ctx: { prisma, user },
input: { formId },
})
).rejects.toThrow(TRPCError);
await expect(
getIncompleteBookingSettingsHandler({
ctx: { prisma, user },
input: { formId },
})
).rejects.toThrow("Form not found");
});
it("should return incompleteBookingActions along with credentials", async () => {
const user = { id: 1 };
const formId = "form-123";
const mockActions = [
{ id: 1, formId, action: "create_contact" },
{ id: 2, formId, action: "update_lead" },
];
prisma.app_RoutingForms_IncompleteBookingActions.findMany.mockResolvedValue(mockActions);
prisma.app_RoutingForms_Form.findFirst.mockResolvedValue({
id: formId,
userId: user.id,
teamId: null,
});
prisma.credential.findFirst.mockResolvedValue(null);
const result = await getIncompleteBookingSettingsHandler({
ctx: { prisma, user },
input: { formId },
});
expect(result.incompleteBookingActions).toEqual(mockActions);
expect(result.incompleteBookingActions).toHaveLength(2);
});
});
});