Files
calendar/packages/lib/server/service/__tests__/InsightsBookingService.integration-test.ts
T
Eunjae LeeGitHubeunjae@cal.com <hey@eunjae.dev>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
cd576f5378 refactor: use dependency injection for InsightsBookingService (#22790)
* refactor: use dependency injection for InsightsBookingService

- Add DI tokens for InsightsBookingService and module
- Rename InsightsBookingService to InsightsBookingBaseService
- Create InsightsBookingService DI interface with create method
- Add DI module and container for InsightsBookingService
- Update tRPC router to use getInsightsBookingService DI container
- Update test file to use InsightsBookingBaseService
- Update documentation to reflect new DI pattern

Follows the same dependency injection pattern established for InsightsRoutingService in PR #22677

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: restore createInsightsBookingService helper using DI internally

- Keep createInsightsBookingService helper function for cleaner API
- Use getInsightsBookingService DI container internally
- Maintain same function signature and behavior
- All existing calls continue to work unchanged

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: rename service files with proper capitalization

- Rename insightsBookingBase.ts to InsightsBookingBaseService.ts
- Rename insightsBookingDI.ts to InsightsBookingDIService.ts
- Update all import statements to use new file names
- Maintain existing handler functionality using createInsightsBookingService(ctx, input)

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* revert some changes

* rename

* update doc

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-08-05 11:07:07 +02:00

577 lines
16 KiB
TypeScript

import type { Team, User, Membership } from "@prisma/client";
import { Prisma } from "@prisma/client";
import { describe, expect, it } from "vitest";
import { ColumnFilterType } from "@calcom/features/data-table/lib/types";
import prisma from "@calcom/prisma";
import { BookingStatus, MembershipRole } from "@calcom/prisma/enums";
import { InsightsBookingBaseService as InsightsBookingService } from "../InsightsBookingBaseService";
const NOTHING_CONDITION = Prisma.sql`1=0`;
// Helper function to create unique test data
async function createTestData({
teamRole = MembershipRole.MEMBER,
orgRole = MembershipRole.MEMBER,
moreTeamsAndUsers = [],
}: {
teamRole?: MembershipRole;
orgRole?: MembershipRole;
moreTeamsAndUsers?: MembershipRole[][]; // Array of teams, each team has array of user roles
} = {}) {
const timestamp = Date.now();
const randomSuffix = Math.random().toString(36).substring(7);
// Create test user
const user = await prisma.user.create({
data: {
email: `test-user-${timestamp}-${randomSuffix}@example.com`,
username: `testuser-${timestamp}-${randomSuffix}`,
name: "Test User",
},
});
// Create test organization
const org = await prisma.team.create({
data: {
name: `Test Org ${timestamp}-${randomSuffix}`,
slug: `test-org-${timestamp}-${randomSuffix}`,
isOrganization: true,
},
});
// Create test team under the organization
const team = await prisma.team.create({
data: {
name: `Test Team ${timestamp}-${randomSuffix}`,
slug: `test-team-${timestamp}-${randomSuffix}`,
isOrganization: false,
parentId: org.id,
},
});
// Create test event type
const eventType = await prisma.eventType.create({
data: {
title: "Test Event Type",
slug: "test-event-type",
length: 60,
userId: user.id,
teamId: team.id,
},
});
// Create test booking
const booking = await prisma.booking.create({
data: {
uid: `test-booking-${timestamp}-${randomSuffix}`,
title: "Test Booking",
startTime: new Date(),
endTime: new Date(Date.now() + 60 * 60 * 1000),
userId: user.id,
eventTypeId: eventType.id,
status: BookingStatus.PENDING,
},
});
const membership = await prisma.membership.create({
data: {
userId: user.id,
teamId: team.id,
role: teamRole,
accepted: true,
},
});
const orgMembership = await prisma.membership.create({
data: {
userId: user.id,
teamId: org.id,
role: orgRole,
accepted: true,
},
});
// Create additional teams and users based on moreTeamsAndUsers
const additionalTeams: Team[] = [];
const additionalUsers: User[] = [];
const additionalMemberships: Membership[] = [];
for (let teamIndex = 0; teamIndex < moreTeamsAndUsers.length; teamIndex++) {
const teamUserRoles = moreTeamsAndUsers[teamIndex];
// Create the team
const additionalTeam = await prisma.team.create({
data: {
name: `Test Team ${teamIndex + 2} ${timestamp}-${randomSuffix}`,
slug: `test-team-${teamIndex + 2}-${timestamp}-${randomSuffix}`,
isOrganization: false,
parentId: org.id,
},
});
additionalTeams.push(additionalTeam);
// Create users for this team
for (let userIndex = 0; userIndex < teamUserRoles.length; userIndex++) {
const userRole = teamUserRoles[userIndex];
// Create the user
const additionalUser = await prisma.user.create({
data: {
email: `test-user-team${teamIndex + 2}-${userIndex + 1}-${timestamp}-${randomSuffix}@example.com`,
username: `testuser-team${teamIndex + 2}-${userIndex + 1}-${timestamp}-${randomSuffix}`,
name: `Test User Team${teamIndex + 2}-${userIndex + 1}`,
},
});
additionalUsers.push(additionalUser);
// Create the membership
const additionalMembership = await prisma.membership.create({
data: {
userId: additionalUser.id,
teamId: additionalTeam.id,
role: userRole,
accepted: true,
},
});
additionalMemberships.push(additionalMembership);
}
}
// Return test data and cleanup function
return {
user,
org,
orgMembership,
team,
eventType,
booking,
membership,
additionalTeams,
additionalUsers,
additionalMemberships,
cleanup: async () => {
// Clean up additional memberships
for (const membership of additionalMemberships) {
await prisma.membership.delete({
where: { id: membership.id },
});
}
// Clean up additional users
for (const user of additionalUsers) {
await prisma.user.delete({
where: { id: user.id },
});
}
// Clean up additional teams
for (const team of additionalTeams) {
await prisma.team.delete({
where: { id: team.id },
});
}
// Clean up main test data
await prisma.booking.deleteMany({
where: { id: booking.id },
});
await prisma.eventType.deleteMany({
where: { id: eventType.id },
});
await prisma.membership.deleteMany({
where: { id: membership.id },
});
await prisma.membership.delete({
where: { id: orgMembership.id },
});
await prisma.team.deleteMany({
where: { id: team.id },
});
await prisma.team.deleteMany({
where: { id: org.id },
});
await prisma.user.deleteMany({
where: { id: user.id },
});
},
};
}
describe("InsightsBookingService Integration Tests", () => {
describe("Authorization Conditions", () => {
it("should return NOTHING for invalid options", async () => {
const service = new InsightsBookingService({
prisma,
options: null as any,
});
const conditions = await service.getAuthorizationConditions();
expect(conditions).toEqual(NOTHING_CONDITION);
});
it("should return NOTHING for non-owner/admin user", async () => {
const testData = await createTestData();
// Create a regular user who is not an owner/admin
const regularUser = await prisma.user.create({
data: {
email: `regular-user-${Date.now()}-${Math.random().toString(36).substring(7)}@example.com`,
username: `regularuser-${Date.now()}-${Math.random().toString(36).substring(7)}`,
name: "Regular User",
},
});
// Add the user as a regular member (not owner/admin) to the organization
const membership = await prisma.membership.create({
data: {
userId: regularUser.id,
teamId: testData.org.id,
role: MembershipRole.MEMBER,
accepted: true,
},
});
const service = new InsightsBookingService({
prisma,
options: {
scope: "org",
userId: regularUser.id,
orgId: testData.org.id,
},
});
const conditions = await service.getAuthorizationConditions();
expect(conditions).toEqual(NOTHING_CONDITION);
// Clean up
await prisma.membership.delete({
where: { id: membership.id },
});
await prisma.user.delete({
where: { id: regularUser.id },
});
await testData.cleanup();
});
it("should build user scope 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,
},
});
const conditions = await service.getAuthorizationConditions();
expect(conditions).toEqual(Prisma.sql`("userId" = ${testData.user.id}) AND ("teamId" IS NULL)`);
await testData.cleanup();
});
it("should build team scope conditions", async () => {
const testData = await createTestData({
teamRole: MembershipRole.OWNER,
orgRole: MembershipRole.OWNER,
});
const service = new InsightsBookingService({
prisma,
options: {
scope: "team",
userId: testData.user.id,
orgId: testData.org.id,
teamId: testData.team.id,
},
});
const conditions = await service.getAuthorizationConditions();
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();
});
it("should build org scope conditions", async () => {
const testData = await createTestData({
teamRole: MembershipRole.OWNER,
orgRole: MembershipRole.OWNER,
moreTeamsAndUsers: [
[MembershipRole.MEMBER], // Team 2: 1 user with MEMBER role
[MembershipRole.MEMBER], // Team 3: 1 user with MEMBER role
],
});
// Get references to additional data
const user2 = testData.additionalUsers[0]; // First user from first team
const user3 = testData.additionalUsers[1]; // First user from second team
const team2 = testData.additionalTeams[0]; // First additional team
const team3 = testData.additionalTeams[1]; // Second additional team
const service = new InsightsBookingService({
prisma,
options: {
scope: "org",
userId: testData.user.id,
orgId: testData.org.id,
},
});
const conditions = await service.getAuthorizationConditions();
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();
});
});
describe("Filter Conditions", () => {
it("should return null when no filters are provided", async () => {
const testData = await createTestData();
const service = new InsightsBookingService({
prisma,
options: {
scope: "user",
userId: testData.user.id,
orgId: testData.org.id,
},
});
const conditions = await service.getFilterConditions();
expect(conditions).toBeNull();
await testData.cleanup();
});
it("should build eventTypeId filter conditions", async () => {
const testData = await createTestData();
const service = new InsightsBookingService({
prisma,
options: {
scope: "user",
userId: testData.user.id,
orgId: testData.org.id,
},
filters: {
columnFilters: [
{
id: "eventTypeId",
value: {
type: ColumnFilterType.SINGLE_SELECT,
data: testData.eventType.id,
},
},
],
},
});
const conditions = await service.getFilterConditions();
expect(conditions).toEqual(
Prisma.sql`("eventTypeId" = ${testData.eventType.id}) OR ("eventParentId" = ${testData.eventType.id})`
);
await testData.cleanup();
});
it("should build userId filter conditions", async () => {
const testData = await createTestData();
const service = new InsightsBookingService({
prisma,
options: {
scope: "user",
userId: testData.user.id,
orgId: testData.org.id,
},
filters: {
columnFilters: [
{
id: "userId",
value: {
type: ColumnFilterType.SINGLE_SELECT,
data: testData.user.id,
},
},
],
},
});
const conditions = await service.getFilterConditions();
expect(conditions).toEqual(Prisma.sql`"userId" = ${testData.user.id}`);
await testData.cleanup();
});
it("should combine multiple filter conditions", async () => {
const testData = await createTestData();
const service = new InsightsBookingService({
prisma,
options: {
scope: "user",
userId: testData.user.id,
orgId: testData.org.id,
},
filters: {
columnFilters: [
{
id: "eventTypeId",
value: {
type: ColumnFilterType.SINGLE_SELECT,
data: testData.eventType.id,
},
},
{
id: "userId",
value: {
type: ColumnFilterType.SINGLE_SELECT,
data: testData.user.id,
},
},
],
},
});
const conditions = await service.getFilterConditions();
expect(conditions).toEqual(
Prisma.sql`(("eventTypeId" = ${testData.eventType.id}) OR ("eventParentId" = ${testData.eventType.id})) AND ("userId" = ${testData.user.id})`
);
await testData.cleanup();
});
it("should build status filter conditions", async () => {
const testData = await createTestData();
const service = new InsightsBookingService({
prisma,
options: {
scope: "user",
userId: testData.user.id,
orgId: testData.org.id,
},
filters: {
columnFilters: [
{
id: "status",
value: {
type: ColumnFilterType.MULTI_SELECT,
data: ["pending", "accepted"],
},
},
],
},
});
const conditions = await service.getFilterConditions();
expect(conditions).toEqual(
Prisma.sql`"status" IN (${Prisma.join([
Prisma.sql`${"pending"}::"BookingStatus"`,
Prisma.sql`${"accepted"}::"BookingStatus"`,
])})`
);
await testData.cleanup();
});
});
describe("getBaseConditions", () => {
it("should combine authorization and filter conditions", async () => {
const testData = await createTestData({
teamRole: MembershipRole.OWNER,
orgRole: MembershipRole.OWNER,
});
// Create a user-scoped event type (not team-scoped)
const userEventType = await prisma.eventType.create({
data: {
title: "User Event Type",
slug: "user-event-type",
length: 60,
userId: testData.user.id,
teamId: null, // User-scoped, not team-scoped
},
});
// Create a booking for the user-scoped event type
const userBooking = await prisma.booking.create({
data: {
uid: `user-booking-${Date.now()}-${Math.random().toString(36).substring(7)}`,
title: "User Booking",
startTime: new Date(),
endTime: new Date(Date.now() + 60 * 60 * 1000),
userId: testData.user.id,
eventTypeId: userEventType.id,
status: BookingStatus.PENDING,
},
});
const service = new InsightsBookingService({
prisma,
options: {
scope: "user",
userId: testData.user.id,
orgId: testData.org.id,
},
filters: {
columnFilters: [
{
id: "eventTypeId",
value: {
type: ColumnFilterType.SINGLE_SELECT,
data: userEventType.id,
},
},
],
},
});
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).toEqual([
{
id: userBooking.id,
},
]);
// Clean up
await prisma.booking.delete({
where: { id: userBooking.id },
});
await prisma.eventType.delete({
where: { id: userEventType.id },
});
await testData.cleanup();
});
});
});