mirror of
https://github.com/hanzoai/calendar.git
synced 2026-09-24 22:42:24 +00:00
* refactor: apply biome formatting to apps/web (batch 1) Formats apps/web non-modules directories: - apps/web/app - apps/web/lib - apps/web/pages - apps/web/styles - apps/web/server - apps/web/test - Root-level .ts and .mjs files Co-Authored-By: Claude Opus 4.5 <[email protected]> * refactor: apply biome formatting to apps/web/modules (batch 2) Formats smaller apps/web/modules directories: - onboarding, data-table, users, apps, auth - integration-attribute-sync, shell, availability - feature-flags, team, webhooks, getting-started - feature-opt-in, troubleshooter, schedules, notifications - signup-view.tsx Co-Authored-By: Claude Opus 4.5 <[email protected]> * refactor: apply biome formatting to apps/web/modules (batch 3) Formats medium apps/web/modules directories: - bookings - event-types - insights - embed Co-Authored-By: Claude Opus 4.5 <[email protected]> * refactor: apply biome formatting to apps/web/modules/ee (batch 4) Formats apps/web/modules/ee directory. Co-Authored-By: Claude Opus 4.5 <[email protected]> * refactor: apply biome formatting to apps/web (batch 5) Formats remaining apps/web directories: - apps/web/modules/settings - apps/web/modules/booking-audit - apps/web/playwright Co-Authored-By: Claude Opus 4.5 <[email protected]> * refactor: apply biome formatting to apps/web/components (batch 6) Formats remaining apps/web/components files. Co-Authored-By: Claude Opus 4.5 <[email protected]> * refactor: apply biome formatting to new apps/web files from main Formats newly added/modified files from main merge: - WorkflowStepContainer.tsx (resolved merge conflicts) - Newly moved files (blocklist, form-builder, schedules, etc.) - Other files modified in main Co-Authored-By: Claude Opus 4.5 <[email protected]> * refactor: apply biome formatting to new apps/web files from main Co-Authored-By: Claude Opus 4.5 <[email protected]> --------- Co-authored-by: Claude Opus 4.5 <[email protected]> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
192 lines
5.3 KiB
TypeScript
192 lines
5.3 KiB
TypeScript
import { expect } from "@playwright/test";
|
|
import { v4 as uuidv4 } from "uuid";
|
|
|
|
import { prisma } from "@calcom/prisma";
|
|
import { BookingStatus } from "@calcom/prisma/enums";
|
|
|
|
import { test } from "./lib/fixtures";
|
|
|
|
/**
|
|
* Helper to retry network requests that may fail with transient errors like ECONNRESET
|
|
*/
|
|
async function retryOnNetworkError<T>(fn: () => Promise<T>, maxRetries = 3, delayMs = 500): Promise<T> {
|
|
let lastError: Error | undefined;
|
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
try {
|
|
return await fn();
|
|
} catch (error) {
|
|
lastError = error as Error;
|
|
const errorMessage = lastError.message || "";
|
|
// Only retry on transient network errors
|
|
const isRetryable =
|
|
errorMessage.includes("ECONNRESET") ||
|
|
errorMessage.includes("ECONNREFUSED") ||
|
|
errorMessage.includes("ETIMEDOUT") ||
|
|
errorMessage.includes("socket hang up");
|
|
|
|
if (!isRetryable || attempt === maxRetries) {
|
|
throw lastError;
|
|
}
|
|
// Wait before retrying with exponential backoff
|
|
await new Promise((resolve) => setTimeout(resolve, delayMs * attempt));
|
|
}
|
|
}
|
|
throw lastError;
|
|
}
|
|
|
|
test.describe("Booking Confirmation and Rejection via API", () => {
|
|
test.afterEach(async ({ users }) => {
|
|
await users.deleteAll();
|
|
});
|
|
|
|
test("should successfully confirm a booking via GET /api/verify-booking-token", async ({ page, users }) => {
|
|
const organizer = await users.create({
|
|
name: "Test Organizer",
|
|
overrideDefaultEventTypes: true,
|
|
eventTypes: [
|
|
{
|
|
title: "30 Min Meeting",
|
|
slug: "30-min",
|
|
length: 30,
|
|
requiresConfirmation: true,
|
|
},
|
|
],
|
|
});
|
|
|
|
const eventType = organizer.eventTypes[0];
|
|
if (!eventType) {
|
|
throw new Error("Event type not found");
|
|
}
|
|
|
|
const oneTimePassword = uuidv4();
|
|
const startTime = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
|
const endTime = new Date(startTime.getTime() + 30 * 60 * 1000);
|
|
|
|
const booking = await prisma.booking.create({
|
|
data: {
|
|
uid: `test-booking-${Date.now()}`,
|
|
title: "Test Booking",
|
|
startTime,
|
|
endTime,
|
|
status: BookingStatus.PENDING,
|
|
oneTimePassword,
|
|
user: {
|
|
connect: {
|
|
id: organizer.id,
|
|
},
|
|
},
|
|
eventType: {
|
|
connect: {
|
|
id: eventType.id,
|
|
},
|
|
},
|
|
attendees: {
|
|
create: {
|
|
email: "[email protected]",
|
|
name: "Test Attendee",
|
|
timeZone: "Europe/London",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const url = `/api/verify-booking-token?action=accept&token=${oneTimePassword}&bookingUid=${booking.uid}&userId=${organizer.id}`;
|
|
|
|
const response = await retryOnNetworkError(
|
|
() =>
|
|
page.request.get(url, {
|
|
maxRedirects: 0,
|
|
}),
|
|
3,
|
|
500
|
|
);
|
|
|
|
expect(response.status()).toBe(303);
|
|
const location = response.headers()["location"];
|
|
expect(location).toContain(`/booking/${booking.uid}`);
|
|
|
|
const updatedBooking = await prisma.booking.findUnique({
|
|
where: { id: booking.id },
|
|
});
|
|
|
|
expect(updatedBooking?.status).toBe(BookingStatus.ACCEPTED);
|
|
expect(updatedBooking?.oneTimePassword).toBeNull();
|
|
});
|
|
|
|
test("should successfully reject a booking via POST /api/verify-booking-token", async ({ page, users }) => {
|
|
const organizer = await users.create({
|
|
name: "Test Organizer",
|
|
overrideDefaultEventTypes: true,
|
|
eventTypes: [
|
|
{
|
|
title: "30 Min Meeting",
|
|
slug: "30-min",
|
|
length: 30,
|
|
requiresConfirmation: true,
|
|
},
|
|
],
|
|
});
|
|
|
|
const eventType = organizer.eventTypes[0];
|
|
if (!eventType) {
|
|
throw new Error("Event type not found");
|
|
}
|
|
|
|
const oneTimePassword = uuidv4();
|
|
const startTime = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
|
const endTime = new Date(startTime.getTime() + 30 * 60 * 1000);
|
|
|
|
const booking = await prisma.booking.create({
|
|
data: {
|
|
uid: `test-booking-${Date.now()}`,
|
|
title: "Test Booking",
|
|
startTime,
|
|
endTime,
|
|
status: BookingStatus.PENDING,
|
|
oneTimePassword,
|
|
user: {
|
|
connect: {
|
|
id: organizer.id,
|
|
},
|
|
},
|
|
eventType: {
|
|
connect: {
|
|
id: eventType.id,
|
|
},
|
|
},
|
|
attendees: {
|
|
create: {
|
|
email: "[email protected]",
|
|
name: "Test Attendee",
|
|
timeZone: "Europe/London",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const url = `/api/verify-booking-token?action=reject&token=${oneTimePassword}&bookingUid=${booking.uid}&userId=${organizer.id}`;
|
|
|
|
const response = await retryOnNetworkError(
|
|
() =>
|
|
page.request.post(url, {
|
|
data: { reason: "Not available at this time" },
|
|
maxRedirects: 0,
|
|
}),
|
|
3,
|
|
500
|
|
);
|
|
|
|
expect(response.status()).toBe(303);
|
|
const location = response.headers()["location"];
|
|
expect(location).toContain(`/booking/${booking.uid}`);
|
|
|
|
const updatedBooking = await prisma.booking.findUnique({
|
|
where: { id: booking.id },
|
|
});
|
|
|
|
expect(updatedBooking?.status).toBe(BookingStatus.REJECTED);
|
|
expect(updatedBooking?.rejectionReason).toBe("Not available at this time");
|
|
expect(updatedBooking?.oneTimePassword).toBeNull();
|
|
});
|
|
});
|