Files
sean-brydonGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
be4bb3fcde feat: TOUTOC in handleSeats (#27389)
* feat: TOUTOC in handleSeats

* fix type check

* refactor: replace Prisma include with select in integration tests

Address Cubic AI review feedback (confidence 9/10) to use select instead
of include in Prisma queries to limit returned fields and avoid pulling
full related records.

Co-Authored-By: unknown <>

* fix seat count

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-30 13:38:31 +00:00

502 lines
13 KiB
TypeScript

import { describe, it, expect, beforeAll, afterEach } from "vitest";
import { prisma } from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { addSeatToBooking } from "./createNewSeat";
// Track resources to clean up
const createdBookingIds: number[] = [];
let testUserId: number;
let testEventTypeId: number;
async function clearTestData() {
if (createdBookingIds.length > 0) {
await prisma.bookingSeat.deleteMany({
where: { bookingId: { in: createdBookingIds } },
});
await prisma.attendee.deleteMany({
where: { bookingId: { in: createdBookingIds } },
});
await prisma.booking.deleteMany({
where: { id: { in: createdBookingIds } },
});
createdBookingIds.length = 0;
}
}
describe("createNewSeat Race Condition Prevention (Integration)", () => {
beforeAll(async () => {
// Use existing seed user
const testUser = await prisma.user.findFirstOrThrow({
where: { email: "[email protected]" },
});
testUserId = testUser.id;
// Find or create a test event type with seats
let eventType = await prisma.eventType.findFirst({
where: {
userId: testUserId,
seatsPerTimeSlot: { not: null },
},
});
if (!eventType) {
eventType = await prisma.eventType.create({
data: {
title: "Seated Test Event",
slug: `seated-test-event-${Date.now()}`,
length: 30,
userId: testUserId,
seatsPerTimeSlot: 2,
seatsShowAttendees: false,
},
});
}
testEventTypeId = eventType.id;
});
afterEach(async () => {
await clearTestData();
});
it("should prevent overbooking when concurrent requests race for the last seat", async () => {
const bookingUid = `race-test-${Date.now()}`;
const startTime = new Date(Date.now() + 24 * 60 * 60 * 1000);
const endTime = new Date(startTime.getTime() + 30 * 60 * 1000);
const seatsPerTimeSlot = 2;
// Create a booking first
const booking = await prisma.booking.create({
data: {
uid: bookingUid,
userId: testUserId,
eventTypeId: testEventTypeId,
status: BookingStatus.ACCEPTED,
startTime,
endTime,
title: "Race Condition Test Booking",
},
});
createdBookingIds.push(booking.id);
// Create 1 existing seat (out of 2)
const attendee = await prisma.attendee.create({
data: {
email: "[email protected]",
name: "Existing Seat",
timeZone: "UTC",
locale: "en",
bookingId: booking.id,
},
});
await prisma.bookingSeat.create({
data: {
referenceUid: `existing-seat-${Date.now()}`,
data: {},
bookingId: booking.id,
attendeeId: attendee.id,
},
});
// Fire 2 concurrent requests for the last seat using the actual addSeatToBooking function
const results = await Promise.allSettled([
addSeatToBooking({
bookingUid,
bookingId: booking.id,
bookingStatus: BookingStatus.ACCEPTED,
seatsPerTimeSlot,
attendee: {
email: "[email protected]",
name: "Racer 1",
timeZone: "UTC",
locale: "en",
},
seatData: {},
}),
addSeatToBooking({
bookingUid,
bookingId: booking.id,
bookingStatus: BookingStatus.ACCEPTED,
seatsPerTimeSlot,
attendee: {
email: "[email protected]",
name: "Racer 2",
timeZone: "UTC",
locale: "en",
},
seatData: {},
}),
]);
// Count successes and failures
const successes = results.filter((r) => r.status === "fulfilled");
const failures = results.filter(
(r) => r.status === "rejected" && (r.reason as Error).message === ErrorCode.BookingSeatsFull
);
// Exactly 1 should succeed, 1 should fail with BookingSeatsFull
expect(successes.length).toBe(1);
expect(failures.length).toBe(1);
// Verify final seat count is exactly 2 (1 existing + 1 new)
const finalBooking = await prisma.booking.findUnique({
where: { uid: bookingUid },
select: {
attendees: {
select: {
id: true,
bookingSeat: {
select: {
id: true,
},
},
},
},
},
});
const finalSeatCount = finalBooking?.attendees.filter((a) => !!a.bookingSeat).length;
expect(finalSeatCount).toBe(2);
});
it("should allow both bookings when enough seats are available", async () => {
const bookingUid = `multi-seat-test-${Date.now()}`;
const startTime = new Date(Date.now() + 24 * 60 * 60 * 1000);
const endTime = new Date(startTime.getTime() + 30 * 60 * 1000);
const seatsPerTimeSlot = 3; // 3 seats available
// Create a booking with no seats taken yet
const booking = await prisma.booking.create({
data: {
uid: bookingUid,
userId: testUserId,
eventTypeId: testEventTypeId,
status: BookingStatus.ACCEPTED,
startTime,
endTime,
title: "Multi Seat Test Booking",
},
});
createdBookingIds.push(booking.id);
// Fire 2 concurrent requests when there are 3 seats available
const results = await Promise.allSettled([
addSeatToBooking({
bookingUid,
bookingId: booking.id,
bookingStatus: BookingStatus.ACCEPTED,
seatsPerTimeSlot,
attendee: {
email: "[email protected]",
name: "Booker 1",
timeZone: "UTC",
locale: "en",
},
seatData: {},
}),
addSeatToBooking({
bookingUid,
bookingId: booking.id,
bookingStatus: BookingStatus.ACCEPTED,
seatsPerTimeSlot,
attendee: {
email: "[email protected]",
name: "Booker 2",
timeZone: "UTC",
locale: "en",
},
seatData: {},
}),
]);
// Both should succeed
const successes = results.filter((r) => r.status === "fulfilled");
expect(successes.length).toBe(2);
// Verify final seat count is 2
const finalBooking = await prisma.booking.findUnique({
where: { uid: bookingUid },
select: {
attendees: {
select: {
id: true,
bookingSeat: {
select: {
id: true,
},
},
},
},
},
});
const finalSeatCount = finalBooking?.attendees.filter((a) => !!a.bookingSeat).length;
expect(finalSeatCount).toBe(2);
});
it("should reject booking when event is already full", async () => {
const bookingUid = `full-event-test-${Date.now()}`;
const startTime = new Date(Date.now() + 24 * 60 * 60 * 1000);
const endTime = new Date(startTime.getTime() + 30 * 60 * 1000);
const seatsPerTimeSlot = 2;
// Create a booking first
const booking = await prisma.booking.create({
data: {
uid: bookingUid,
userId: testUserId,
eventTypeId: testEventTypeId,
status: BookingStatus.ACCEPTED,
startTime,
endTime,
title: "Full Event Test Booking",
},
});
createdBookingIds.push(booking.id);
// Create 2 seats (filling all available seats)
const attendee1 = await prisma.attendee.create({
data: {
email: "[email protected]",
name: "Seat 1",
timeZone: "UTC",
locale: "en",
bookingId: booking.id,
},
});
await prisma.bookingSeat.create({
data: {
referenceUid: `seat1-${Date.now()}`,
data: {},
bookingId: booking.id,
attendeeId: attendee1.id,
},
});
const attendee2 = await prisma.attendee.create({
data: {
email: "[email protected]",
name: "Seat 2",
timeZone: "UTC",
locale: "en",
bookingId: booking.id,
},
});
await prisma.bookingSeat.create({
data: {
referenceUid: `seat2-${Date.now()}`,
data: {},
bookingId: booking.id,
attendeeId: attendee2.id,
},
});
// Try to add another seat - should fail
await expect(
addSeatToBooking({
bookingUid,
bookingId: booking.id,
bookingStatus: BookingStatus.ACCEPTED,
seatsPerTimeSlot,
attendee: {
email: "[email protected]",
name: "Late Comer",
timeZone: "UTC",
locale: "en",
},
seatData: {},
})
).rejects.toThrow(ErrorCode.BookingSeatsFull);
// Verify seat count is still 2
const finalBooking = await prisma.booking.findUnique({
where: { uid: bookingUid },
select: {
attendees: {
select: {
id: true,
bookingSeat: {
select: {
id: true,
},
},
},
},
},
});
const finalSeatCount = finalBooking?.attendees.filter((a) => !!a.bookingSeat).length;
expect(finalSeatCount).toBe(2);
});
it("should allow booking when seatsPerTimeSlot is 0 (falsy)", async () => {
// This tests the fix for the bug where null/undefined seatsPerTimeSlot
// would fallback to 0 and incorrectly reject all bookings because
// 0 <= currentSeatCount was always true
const bookingUid = `zero-seats-test-${Date.now()}`;
const startTime = new Date(Date.now() + 24 * 60 * 60 * 1000);
const endTime = new Date(startTime.getTime() + 30 * 60 * 1000);
const seatsPerTimeSlot = 0; // Simulates null/undefined fallback
// Create a booking
const booking = await prisma.booking.create({
data: {
uid: bookingUid,
userId: testUserId,
eventTypeId: testEventTypeId,
status: BookingStatus.ACCEPTED,
startTime,
endTime,
title: "Zero Seats Test Booking",
},
});
createdBookingIds.push(booking.id);
// Add a seat with seatsPerTimeSlot = 0 - should succeed
const result = await addSeatToBooking({
bookingUid,
bookingId: booking.id,
bookingStatus: BookingStatus.ACCEPTED,
seatsPerTimeSlot,
attendee: {
email: "[email protected]",
name: "Booker",
timeZone: "UTC",
locale: "en",
},
seatData: {},
});
expect(result).toBeDefined();
expect(result?.referenceUid).toBeDefined();
// Verify the seat was created
const finalBooking = await prisma.booking.findUnique({
where: { uid: bookingUid },
select: {
attendees: {
select: {
id: true,
bookingSeat: {
select: {
id: true,
},
},
},
},
},
});
const finalSeatCount = finalBooking?.attendees.filter((a) => !!a.bookingSeat).length;
expect(finalSeatCount).toBe(1);
});
it("should allow multiple bookings when seatsPerTimeSlot is 0 (no limit)", async () => {
const bookingUid = `zero-seats-multi-test-${Date.now()}`;
const startTime = new Date(Date.now() + 24 * 60 * 60 * 1000);
const endTime = new Date(startTime.getTime() + 30 * 60 * 1000);
const seatsPerTimeSlot = 0; // No limit enforced
// Create a booking
const booking = await prisma.booking.create({
data: {
uid: bookingUid,
userId: testUserId,
eventTypeId: testEventTypeId,
status: BookingStatus.ACCEPTED,
startTime,
endTime,
title: "Zero Seats Multi Test Booking",
},
});
createdBookingIds.push(booking.id);
// Create existing seats
const attendee1 = await prisma.attendee.create({
data: {
email: "[email protected]",
name: "Existing 1",
timeZone: "UTC",
locale: "en",
bookingId: booking.id,
},
});
await prisma.bookingSeat.create({
data: {
referenceUid: `existing1-${Date.now()}`,
data: {},
bookingId: booking.id,
attendeeId: attendee1.id,
},
});
const attendee2 = await prisma.attendee.create({
data: {
email: "[email protected]",
name: "Existing 2",
timeZone: "UTC",
locale: "en",
bookingId: booking.id,
},
});
await prisma.bookingSeat.create({
data: {
referenceUid: `existing2-${Date.now()}`,
data: {},
bookingId: booking.id,
attendeeId: attendee2.id,
},
});
// Add another seat with seatsPerTimeSlot = 0 - should succeed even with existing seats
const result = await addSeatToBooking({
bookingUid,
bookingId: booking.id,
bookingStatus: BookingStatus.ACCEPTED,
seatsPerTimeSlot,
attendee: {
email: "[email protected]",
name: "New Booker",
timeZone: "UTC",
locale: "en",
},
seatData: {},
});
expect(result).toBeDefined();
expect(result?.referenceUid).toBeDefined();
// Verify total seat count is 3 (2 existing + 1 new)
const finalBooking = await prisma.booking.findUnique({
where: { uid: bookingUid },
select: {
attendees: {
select: {
id: true,
bookingSeat: {
select: {
id: true,
},
},
},
},
},
});
const finalSeatCount = finalBooking?.attendees.filter((a) => !!a.bookingSeat).length;
expect(finalSeatCount).toBe(3);
});
});