Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import type { Team, User, Membership } from "@prisma/client";
import { Prisma } from "@prisma/client";
import { describe, expect, it } from "vitest";

import prisma from "@calcom/prisma";
import { BookingStatus, MembershipRole } from "@calcom/prisma/enums";

import { InsightsBookingService } from "../../service/insightsBooking";

const NOTHING_CONDITION = Prisma.sql`1=0`;

// Helper function to create unique test data
async function createTestData({
teamRole = MembershipRole.MEMBER,
Expand Down Expand Up @@ -204,7 +207,7 @@ describe("InsightsBookingService Integration Tests", () => {
});

const conditions = await service.getAuthorizationConditions();
expect(conditions).toEqual({ id: -1 });
expect(conditions).toEqual(NOTHING_CONDITION);
});

it("should return NOTHING for non-owner/admin user", async () => {
Expand Down Expand Up @@ -239,7 +242,7 @@ describe("InsightsBookingService Integration Tests", () => {
});

const conditions = await service.getAuthorizationConditions();
expect(conditions).toEqual({ id: -1 });
expect(conditions).toEqual(NOTHING_CONDITION);

// Clean up
await prisma.membership.delete({
Expand Down Expand Up @@ -267,14 +270,7 @@ describe("InsightsBookingService Integration Tests", () => {
});

const conditions = await service.getAuthorizationConditions();
expect(conditions).toEqual({
AND: [
{
userId: testData.user.id,
teamId: null,
},
],
});
expect(conditions).toEqual(Prisma.sql`("userId" = ${testData.user.id}) AND ("teamId" IS NULL)`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 AI 代码审查发现问题

📋 问题概述

使用 Prisma.sql 拼接 SQL 条件,但未验证输入是否经过参数化处理,存在潜在 SQL 注入风险

📍 问题详情

🔴 问题 1 | 严重程度: HIGH | 行号: 273

💬 详细说明:

  • 攻击者可通过恶意输入构造任意 SQL 语句,可能导致数据泄露或数据库被篡改

📝 问题代码:

expect(conditions).toEqual(Prisma.sql`("userId" = ${testData.user.id}) AND ("teamId" IS NULL)`);

💡 修复建议:

确保所有用户输入在拼接前已通过 Prisma 参数化机制处理,并对动态条件进行严格类型校验和白名单过滤

✅ 修复示例:



🔗 参考链接


await testData.cleanup();
});
Expand All @@ -296,24 +292,11 @@ describe("InsightsBookingService Integration Tests", () => {
});

const conditions = await service.getAuthorizationConditions();
expect(conditions).toEqual({
AND: [
{
OR: [
{
teamId: testData.team.id,
isTeamBooking: true,
},
{
userId: {
in: [testData.user.id],
},
isTeamBooking: false,
},
],
},
],
});
expect(conditions).toEqual(
Prisma.sql`(("teamId" = ${testData.team.id}) AND ("isTeamBooking" = true)) OR (("userId" = ANY(${[
testData.user.id,
]})) AND ("isTeamBooking" = false))`
);

// Clean up
await testData.cleanup();
Expand Down Expand Up @@ -346,26 +329,18 @@ describe("InsightsBookingService Integration Tests", () => {

const conditions = await service.getAuthorizationConditions();

expect(conditions).toEqual({
AND: [
{
OR: [
{
teamId: {
in: [testData.org.id, testData.team.id, team2.id, team3.id],
},
isTeamBooking: true,
},
{
userId: {
in: [testData.user.id, user2.id, user3.id],
},
isTeamBooking: false,
},
],
},
],
});
expect(conditions).toEqual(
Prisma.sql`(("teamId" = ANY(${[
testData.org.id,
testData.team.id,
team2.id,
team3.id,
]})) AND ("isTeamBooking" = true)) OR (("userId" = ANY(${[
testData.user.id,
user2.id,
user3.id,
]})) AND ("isTeamBooking" = false))`
);

await testData.cleanup();
});
Expand Down Expand Up @@ -406,13 +381,9 @@ describe("InsightsBookingService Integration Tests", () => {
});

const conditions = await service.getFilterConditions();
expect(conditions).toEqual({
AND: [
{
OR: [{ eventTypeId: testData.eventType.id }, { eventParentId: testData.eventType.id }],
},
],
});
expect(conditions).toEqual(
Prisma.sql`("eventTypeId" = ${testData.eventType.id}) OR ("eventParentId" = ${testData.eventType.id})`
);

await testData.cleanup();
});
Expand All @@ -433,13 +404,7 @@ describe("InsightsBookingService Integration Tests", () => {
});

const conditions = await service.getFilterConditions();
expect(conditions).toEqual({
AND: [
{
userId: testData.user.id,
},
],
});
expect(conditions).toEqual(Prisma.sql`"userId" = ${testData.user.id}`);

await testData.cleanup();
});
Expand All @@ -461,90 +426,15 @@ describe("InsightsBookingService Integration Tests", () => {
});

const conditions = await service.getFilterConditions();
expect(conditions).toEqual({
AND: [
{
OR: [{ eventTypeId: testData.eventType.id }, { eventParentId: testData.eventType.id }],
},
{
userId: testData.user.id,
},
],
});

await testData.cleanup();
});
});

describe("Caching", () => {
it("should cache authorization conditions", async () => {
const testData = await createTestData({
teamRole: MembershipRole.OWNER,
orgRole: MembershipRole.OWNER,
});

const service = new InsightsBookingService({
prisma,
options: {
scope: "user",
userId: testData.user.id,
orgId: testData.org.id,
},
});

// First call should build conditions
const conditions1 = await service.getAuthorizationConditions();
expect(conditions1).toEqual({
AND: [
{
userId: testData.user.id,
teamId: null,
},
],
});

// Second call should use cached conditions
const conditions2 = await service.getAuthorizationConditions();
expect(conditions2).toEqual(conditions1);

// Clean up
await testData.cleanup();
});

it("should cache filter conditions", async () => {
const testData = await createTestData();

const service = new InsightsBookingService({
prisma,
options: {
scope: "user",
userId: testData.user.id,
orgId: testData.org.id,
},
filters: {
eventTypeId: testData.eventType.id,
},
});

// First call should build conditions
const conditions1 = await service.getFilterConditions();
expect(conditions1).toEqual({
AND: [
{
OR: [{ eventTypeId: testData.eventType.id }, { eventParentId: testData.eventType.id }],
},
],
});

// Second call should use cached conditions
const conditions2 = await service.getFilterConditions();
expect(conditions2).toEqual(conditions1);
expect(conditions).toEqual(
Prisma.sql`(("eventTypeId" = ${testData.eventType.id}) OR ("eventParentId" = ${testData.eventType.id})) AND ("userId" = ${testData.user.id})`
);

await testData.cleanup();
});
});

describe("findMany", () => {
describe("getBaseConditions", () => {
it("should combine authorization and filter conditions", async () => {
const testData = await createTestData({
teamRole: MembershipRole.OWNER,
Expand Down Expand Up @@ -587,16 +477,18 @@ describe("InsightsBookingService Integration Tests", () => {
},
});

const results = await service.findMany({
select: {
id: true,
title: true,
},
});
const baseConditions = await service.getBaseConditions();
const results = await prisma.$queryRaw<{ id: number }[]>`
SELECT id FROM "BookingTimeStatusDenormalized"
WHERE ${baseConditions}
`;

// Should return the user booking since it matches both conditions
expect(results).toHaveLength(1);
expect(results[0]?.id).toBe(userBooking.id);
expect(results).toEqual([
{
id: userBooking.id,
},
]);

// Clean up
await prisma.booking.delete({
Expand Down
Loading