717a26f223
* fix: prevent bulk update of locked locations in child managed event types - Filter out child managed event types with locked locations in getBulkUserEventTypes - Add validation in bulkUpdateEventsToDefaultLocation to prevent updating locked fields - Implements defense in depth with validation at multiple layers Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * Abstract filtering logic * test: add comprehensive tests for bulk location update filtering - Add unit tests for filterEventTypesWhereLocationUpdateIsAllowed - Add unit tests for bulkUpdateEventsToDefaultLocation - Add integration tests for getBulkUserEventTypes - Fix bug: change unlockedFields?.locations check from !== undefined to === true This ensures that locations: false is properly treated as locked, addressing the security issue identified in PR review comments Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: filter locked managed event types on app installation page - Add parentId to eventTypeSelect in getEventTypes function - Apply filterEventTypesWhereLocationUpdateIsAllowed to both team and user event types - Only filter when isConferencing is true to avoid affecting other app types - Fixes issue where locked managed event types were showing in the event type selection list on /apps/installation/event-types page Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix(embed-react): remove obsolete availabilityLoaded event listener The availabilityLoaded event does not exist in the EventDataMap type system in embed-core. This code was causing 5 TypeScript errors in CI: - Type 'availabilityLoaded' does not satisfy constraint 'keyof EventDataMap' - 'data' is of type 'unknown' (2 occurrences) - Type 'availabilityLoaded' is not assignable to action union (2 occurrences) Since this is an example file and the event is not defined in the type system, removing this obsolete code resolves the type errors. Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: correct Prisma type for metadata in test helper function Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: use flexible PrismaLike type for better test compatibility Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: properly type mock Prisma objects in test files Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: properly mock Prisma methods in test file Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * Filter out metadata * Undo change in embed file * Address feedback --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
349 lines
8.1 KiB
TypeScript
349 lines
8.1 KiB
TypeScript
import { describe, expect, it, beforeEach, vi } from "vitest";
|
|
|
|
import type { PrismaClient } from "@calcom/prisma";
|
|
|
|
import { bulkUpdateEventsToDefaultLocation } from "./bulkUpdateEventsToDefaultLocation";
|
|
|
|
vi.mock("../utils", () => ({
|
|
getAppFromSlug: vi.fn((slug: string) => {
|
|
if (slug === "google-meet") {
|
|
return {
|
|
slug: "google-meet",
|
|
appData: {
|
|
location: {
|
|
type: "integrations:google:meet",
|
|
},
|
|
},
|
|
};
|
|
}
|
|
if (slug === "zoom") {
|
|
return {
|
|
slug: "zoom",
|
|
appData: {
|
|
location: {
|
|
type: "integrations:zoom",
|
|
},
|
|
},
|
|
};
|
|
}
|
|
return null;
|
|
}),
|
|
}));
|
|
|
|
type MockPrisma = {
|
|
credential: {
|
|
findFirst: ReturnType<typeof vi.fn>;
|
|
};
|
|
eventType: {
|
|
findMany: ReturnType<typeof vi.fn>;
|
|
updateMany: ReturnType<typeof vi.fn>;
|
|
};
|
|
};
|
|
|
|
const createMockPrisma = (): MockPrisma => ({
|
|
credential: {
|
|
findFirst: vi.fn(),
|
|
},
|
|
eventType: {
|
|
findMany: vi.fn(),
|
|
updateMany: vi.fn(),
|
|
},
|
|
});
|
|
|
|
describe("bulkUpdateEventsToDefaultLocation", () => {
|
|
let mockPrisma: MockPrisma;
|
|
|
|
beforeEach(() => {
|
|
mockPrisma = createMockPrisma();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("should throw error when default conferencing app is not set", async () => {
|
|
const user = {
|
|
id: 1,
|
|
metadata: {},
|
|
};
|
|
|
|
await expect(
|
|
bulkUpdateEventsToDefaultLocation({
|
|
eventTypeIds: [1, 2],
|
|
user,
|
|
prisma: mockPrisma as unknown as PrismaClient,
|
|
})
|
|
).rejects.toThrow("Default conferencing app not set");
|
|
});
|
|
|
|
it("should throw error when default conferencing app doesn't exist", async () => {
|
|
const user = {
|
|
id: 1,
|
|
metadata: {
|
|
defaultConferencingApp: {
|
|
appSlug: "non-existent-app",
|
|
appLink: "https://example.com",
|
|
},
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
bulkUpdateEventsToDefaultLocation({
|
|
eventTypeIds: [1, 2],
|
|
user,
|
|
prisma: mockPrisma as unknown as PrismaClient,
|
|
})
|
|
).rejects.toThrow("Default conferencing app 'non-existent-app' doesnt exist.");
|
|
});
|
|
|
|
it("should update event types that are not child managed events", async () => {
|
|
const user = {
|
|
id: 1,
|
|
metadata: {
|
|
defaultConferencingApp: {
|
|
appSlug: "google-meet",
|
|
appLink: "https://meet.google.com",
|
|
},
|
|
},
|
|
};
|
|
|
|
const credential = { id: 100 };
|
|
const eventTypes = [
|
|
{ id: 1, parentId: null, metadata: {} },
|
|
{ id: 2, parentId: null, metadata: {} },
|
|
];
|
|
|
|
mockPrisma.credential.findFirst.mockResolvedValue(credential);
|
|
mockPrisma.eventType.findMany.mockResolvedValue(eventTypes);
|
|
mockPrisma.eventType.updateMany.mockResolvedValue({ count: 2 });
|
|
|
|
const result = await bulkUpdateEventsToDefaultLocation({
|
|
eventTypeIds: [1, 2],
|
|
user,
|
|
prisma: mockPrisma as unknown as PrismaClient,
|
|
});
|
|
|
|
expect(result.count).toBe(2);
|
|
expect(mockPrisma.eventType.updateMany).toHaveBeenCalledWith({
|
|
where: {
|
|
id: {
|
|
in: [1, 2],
|
|
},
|
|
userId: 1,
|
|
},
|
|
data: {
|
|
locations: [
|
|
{
|
|
type: "integrations:google:meet",
|
|
link: "https://meet.google.com",
|
|
credentialId: 100,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
});
|
|
|
|
it("should filter out child managed event types with locked locations", async () => {
|
|
const user = {
|
|
id: 1,
|
|
metadata: {
|
|
defaultConferencingApp: {
|
|
appSlug: "zoom",
|
|
appLink: "https://zoom.us",
|
|
},
|
|
},
|
|
};
|
|
|
|
const credential = { id: 200 };
|
|
const eventTypes = [
|
|
{ id: 1, parentId: null, metadata: {} },
|
|
{
|
|
id: 2,
|
|
parentId: 100,
|
|
metadata: {
|
|
managedEventConfig: {
|
|
unlockedFields: {
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: 3,
|
|
parentId: 100,
|
|
metadata: {
|
|
managedEventConfig: {
|
|
unlockedFields: {
|
|
locations: true, // unlocked
|
|
},
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
mockPrisma.credential.findFirst.mockResolvedValue(credential);
|
|
mockPrisma.eventType.findMany.mockResolvedValue(eventTypes);
|
|
mockPrisma.eventType.updateMany.mockResolvedValue({ count: 2 });
|
|
|
|
const result = await bulkUpdateEventsToDefaultLocation({
|
|
eventTypeIds: [1, 2, 3],
|
|
user,
|
|
prisma: mockPrisma as unknown as PrismaClient,
|
|
});
|
|
|
|
expect(result.count).toBe(2);
|
|
expect(mockPrisma.eventType.updateMany).toHaveBeenCalledWith({
|
|
where: {
|
|
id: {
|
|
in: [1, 3],
|
|
},
|
|
userId: 1,
|
|
},
|
|
data: {
|
|
locations: [
|
|
{
|
|
type: "integrations:zoom",
|
|
link: "https://zoom.us",
|
|
credentialId: 200,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
});
|
|
|
|
it("should return count 0 when all event types have locked locations", async () => {
|
|
const user = {
|
|
id: 1,
|
|
metadata: {
|
|
defaultConferencingApp: {
|
|
appSlug: "google-meet",
|
|
appLink: "https://meet.google.com",
|
|
},
|
|
},
|
|
};
|
|
|
|
const credential = { id: 300 };
|
|
const eventTypes = [
|
|
{
|
|
id: 1,
|
|
parentId: 100,
|
|
metadata: {
|
|
managedEventConfig: {
|
|
unlockedFields: {
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: 2,
|
|
parentId: 100,
|
|
metadata: {
|
|
managedEventConfig: {
|
|
unlockedFields: {
|
|
locations: false, // explicitly locked
|
|
},
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
mockPrisma.credential.findFirst.mockResolvedValue(credential);
|
|
mockPrisma.eventType.findMany.mockResolvedValue(eventTypes);
|
|
|
|
const result = await bulkUpdateEventsToDefaultLocation({
|
|
eventTypeIds: [1, 2],
|
|
user,
|
|
prisma: mockPrisma as unknown as PrismaClient,
|
|
});
|
|
|
|
expect(result.count).toBe(0);
|
|
expect(mockPrisma.eventType.updateMany).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should work without credential when credential is not found", async () => {
|
|
const user = {
|
|
id: 1,
|
|
metadata: {
|
|
defaultConferencingApp: {
|
|
appSlug: "google-meet",
|
|
appLink: "https://meet.google.com",
|
|
},
|
|
},
|
|
};
|
|
|
|
const eventTypes = [{ id: 1, parentId: null, metadata: {} }];
|
|
|
|
mockPrisma.credential.findFirst.mockResolvedValue(null);
|
|
mockPrisma.eventType.findMany.mockResolvedValue(eventTypes);
|
|
mockPrisma.eventType.updateMany.mockResolvedValue({ count: 1 });
|
|
|
|
const result = await bulkUpdateEventsToDefaultLocation({
|
|
eventTypeIds: [1],
|
|
user,
|
|
prisma: mockPrisma as unknown as PrismaClient,
|
|
});
|
|
|
|
expect(result.count).toBe(1);
|
|
expect(mockPrisma.eventType.updateMany).toHaveBeenCalledWith({
|
|
where: {
|
|
id: {
|
|
in: [1],
|
|
},
|
|
userId: 1,
|
|
},
|
|
data: {
|
|
locations: [
|
|
{
|
|
type: "integrations:google:meet",
|
|
link: "https://meet.google.com",
|
|
credentialId: undefined,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
});
|
|
|
|
it("should only query event types belonging to the user", async () => {
|
|
const user = {
|
|
id: 5,
|
|
metadata: {
|
|
defaultConferencingApp: {
|
|
appSlug: "zoom",
|
|
appLink: "https://zoom.us",
|
|
},
|
|
},
|
|
};
|
|
|
|
const credential = { id: 400 };
|
|
const eventTypes = [{ id: 10, parentId: null, metadata: {} }];
|
|
|
|
mockPrisma.credential.findFirst.mockResolvedValue(credential);
|
|
mockPrisma.eventType.findMany.mockResolvedValue(eventTypes);
|
|
mockPrisma.eventType.updateMany.mockResolvedValue({ count: 1 });
|
|
|
|
await bulkUpdateEventsToDefaultLocation({
|
|
eventTypeIds: [10, 11, 12],
|
|
user,
|
|
prisma: mockPrisma as unknown as PrismaClient,
|
|
});
|
|
|
|
expect(mockPrisma.eventType.findMany).toHaveBeenCalledWith({
|
|
where: {
|
|
id: {
|
|
in: [10, 11, 12],
|
|
},
|
|
userId: 5,
|
|
},
|
|
select: {
|
|
id: true,
|
|
metadata: true,
|
|
parentId: true,
|
|
},
|
|
});
|
|
|
|
expect(mockPrisma.eventType.updateMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: expect.objectContaining({
|
|
userId: 5,
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
});
|