528 lines
15 KiB
TypeScript
528 lines
15 KiB
TypeScript
import { describe, it, expect, beforeAll, afterAll, afterEach } from "vitest";
|
|
|
|
import handleDeleteCredential from "@calcom/features/credentials/handleDeleteCredential";
|
|
import prisma from "@calcom/prisma";
|
|
import type { Credential, EventType, User } from "@calcom/prisma/client";
|
|
import { BookingStatus } from "@calcom/prisma/enums";
|
|
|
|
describe("handleDeleteCredential Integration Tests - BookingReference Soft Delete", () => {
|
|
let testUser: User;
|
|
let testCredential: Credential;
|
|
let testEventType: EventType;
|
|
const createdBookingReferenceIds: number[] = [];
|
|
const createdBookingIds: number[] = [];
|
|
|
|
async function deleteCredentialWithMetadata(credentialId: number, userId: number) {
|
|
const user = await prisma.user.findUnique({
|
|
where: { id: userId },
|
|
select: { metadata: true },
|
|
});
|
|
|
|
return handleDeleteCredential({
|
|
userId,
|
|
userMetadata: user?.metadata ?? {},
|
|
credentialId,
|
|
});
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
testUser = await prisma.user.create({
|
|
data: {
|
|
email: "delete-credential-test@example.com",
|
|
username: "delete-credential-test",
|
|
},
|
|
});
|
|
|
|
testCredential = await prisma.credential.create({
|
|
data: {
|
|
type: "stripe_payment",
|
|
key: { test_key: "test_value" },
|
|
userId: testUser.id,
|
|
appId: "stripe",
|
|
},
|
|
});
|
|
|
|
testEventType = await prisma.eventType.create({
|
|
data: {
|
|
title: "Paid Event",
|
|
slug: "paid-event-test",
|
|
length: 30,
|
|
userId: testUser.id,
|
|
price: 1000, // $10
|
|
currency: "usd",
|
|
},
|
|
});
|
|
});
|
|
|
|
afterEach(async () => {
|
|
if (createdBookingReferenceIds.length > 0) {
|
|
await prisma.bookingReference.deleteMany({
|
|
where: {
|
|
id: {
|
|
in: createdBookingReferenceIds,
|
|
},
|
|
},
|
|
});
|
|
createdBookingReferenceIds.splice(0, createdBookingReferenceIds.length);
|
|
}
|
|
|
|
if (createdBookingIds.length > 0) {
|
|
await prisma.attendee.deleteMany({
|
|
where: {
|
|
bookingId: {
|
|
in: createdBookingIds,
|
|
},
|
|
},
|
|
});
|
|
|
|
await prisma.booking.deleteMany({
|
|
where: {
|
|
id: {
|
|
in: createdBookingIds,
|
|
},
|
|
},
|
|
});
|
|
createdBookingIds.splice(0, createdBookingIds.length);
|
|
}
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await prisma.eventType.delete({
|
|
where: {
|
|
id: testEventType.id,
|
|
},
|
|
});
|
|
|
|
try {
|
|
await prisma.credential.delete({
|
|
where: {
|
|
id: testCredential.id,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
// Credential already deleted by test
|
|
console.log("Credential deletion error:", error);
|
|
}
|
|
|
|
await prisma.user.delete({
|
|
where: {
|
|
id: testUser.id,
|
|
},
|
|
});
|
|
});
|
|
|
|
describe("Soft-delete bookingReferences when deleting payment credential", () => {
|
|
it("should soft-delete booking references for unpaid bookings when credential is deleted", async () => {
|
|
const unpaidBooking = await prisma.booking.create({
|
|
data: {
|
|
uid: "unpaid-booking-uid-123",
|
|
title: "Unpaid Booking",
|
|
startTime: new Date(Date.now() + 86400000), // Tomorrow
|
|
endTime: new Date(Date.now() + 90000000),
|
|
userId: testUser.id,
|
|
eventTypeId: testEventType.id,
|
|
status: BookingStatus.PENDING,
|
|
paid: false,
|
|
attendees: {
|
|
create: [
|
|
{
|
|
email: "attendee@test.com",
|
|
name: "Test Attendee",
|
|
timeZone: "America/New_York",
|
|
},
|
|
],
|
|
},
|
|
payment: {
|
|
create: {
|
|
uid: "payment-uid-123",
|
|
appId: "stripe",
|
|
amount: 1000,
|
|
fee: 0,
|
|
currency: "usd",
|
|
success: false,
|
|
refunded: false,
|
|
data: {},
|
|
externalId: "ext-payment-123",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
createdBookingIds.push(unpaidBooking.id);
|
|
|
|
const bookingRef1 = await prisma.bookingReference.create({
|
|
data: {
|
|
type: "stripe_payment",
|
|
uid: "stripe-ref-123",
|
|
meetingId: "stripe-meeting-123",
|
|
credentialId: testCredential.id,
|
|
bookingId: unpaidBooking.id,
|
|
},
|
|
});
|
|
createdBookingReferenceIds.push(bookingRef1.id);
|
|
|
|
const bookingRef2 = await prisma.bookingReference.create({
|
|
data: {
|
|
type: "google_calendar",
|
|
uid: "gcal-ref-123",
|
|
meetingId: "gcal-meeting-123",
|
|
credentialId: testCredential.id,
|
|
bookingId: unpaidBooking.id,
|
|
},
|
|
});
|
|
createdBookingReferenceIds.push(bookingRef2.id);
|
|
|
|
const refsBeforeDeletion = await prisma.bookingReference.findMany({
|
|
where: {
|
|
bookingId: unpaidBooking.id,
|
|
},
|
|
});
|
|
expect(refsBeforeDeletion).toHaveLength(2);
|
|
expect(refsBeforeDeletion.every((ref) => ref.deleted === null)).toBe(true);
|
|
|
|
await deleteCredentialWithMetadata(testCredential.id, testUser.id);
|
|
|
|
const refsAfterDeletion = await prisma.bookingReference.findMany({
|
|
where: {
|
|
bookingId: unpaidBooking.id,
|
|
},
|
|
});
|
|
|
|
expect(refsAfterDeletion).toHaveLength(2);
|
|
expect(refsAfterDeletion.every((ref) => ref.deleted === true)).toBe(true);
|
|
|
|
const cancelledBooking = await prisma.booking.findUnique({
|
|
where: { id: unpaidBooking.id },
|
|
});
|
|
expect(cancelledBooking?.status).toBe(BookingStatus.CANCELLED);
|
|
});
|
|
|
|
it("should persist soft-deleted references in database", async () => {
|
|
const newCredential = await prisma.credential.create({
|
|
data: {
|
|
type: "stripe_payment",
|
|
key: { test_key: "test_value" },
|
|
userId: testUser.id,
|
|
appId: "stripe",
|
|
},
|
|
});
|
|
|
|
const unpaidBooking = await prisma.booking.create({
|
|
data: {
|
|
uid: "unpaid-booking-uid-456",
|
|
title: "Another Unpaid Booking",
|
|
startTime: new Date(Date.now() + 86400000),
|
|
endTime: new Date(Date.now() + 90000000),
|
|
userId: testUser.id,
|
|
eventTypeId: testEventType.id,
|
|
status: BookingStatus.PENDING,
|
|
paid: false,
|
|
attendees: {
|
|
create: [
|
|
{
|
|
email: "attendee2@test.com",
|
|
name: "Test Attendee 2",
|
|
timeZone: "America/New_York",
|
|
},
|
|
],
|
|
},
|
|
payment: {
|
|
create: {
|
|
uid: "payment-uid-456",
|
|
appId: "stripe",
|
|
amount: 1000,
|
|
fee: 0,
|
|
currency: "usd",
|
|
success: false,
|
|
refunded: false,
|
|
data: {},
|
|
externalId: "ext-payment-456",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
createdBookingIds.push(unpaidBooking.id);
|
|
|
|
const bookingRef = await prisma.bookingReference.create({
|
|
data: {
|
|
type: "stripe_payment",
|
|
uid: "stripe-ref-456",
|
|
meetingId: "stripe-meeting-456",
|
|
meetingUrl: "https://example.com/meeting",
|
|
credentialId: newCredential.id,
|
|
bookingId: unpaidBooking.id,
|
|
},
|
|
});
|
|
createdBookingReferenceIds.push(bookingRef.id);
|
|
|
|
await deleteCredentialWithMetadata(newCredential.id, testUser.id);
|
|
|
|
const deletedRef = await prisma.bookingReference.findUnique({
|
|
where: { id: bookingRef.id },
|
|
});
|
|
|
|
expect(deletedRef).toBeDefined();
|
|
expect(deletedRef?.deleted).toBe(true);
|
|
expect(deletedRef?.uid).toBe("stripe-ref-456");
|
|
expect(deletedRef?.meetingUrl).toBe("https://example.com/meeting");
|
|
expect(deletedRef?.type).toBe("stripe_payment");
|
|
});
|
|
|
|
it("should soft-delete references for multiple unpaid bookings when credential is deleted", async () => {
|
|
const newCredential = await prisma.credential.create({
|
|
data: {
|
|
type: "stripe_payment",
|
|
key: { test_key: "test_value" },
|
|
userId: testUser.id,
|
|
appId: "stripe",
|
|
},
|
|
});
|
|
|
|
const booking1 = await prisma.booking.create({
|
|
data: {
|
|
uid: "booking-1-uid",
|
|
title: "Booking 1",
|
|
startTime: new Date(Date.now() + 86400000),
|
|
endTime: new Date(Date.now() + 90000000),
|
|
userId: testUser.id,
|
|
eventTypeId: testEventType.id,
|
|
status: BookingStatus.PENDING,
|
|
paid: false,
|
|
attendees: {
|
|
create: [
|
|
{
|
|
email: "attendee-b1@test.com",
|
|
name: "Attendee B1",
|
|
timeZone: "America/New_York",
|
|
},
|
|
],
|
|
},
|
|
payment: {
|
|
create: {
|
|
uid: "payment-uid-b1",
|
|
appId: "stripe",
|
|
amount: 1000,
|
|
fee: 0,
|
|
currency: "usd",
|
|
success: false,
|
|
refunded: false,
|
|
data: {},
|
|
externalId: "ext-payment-b1",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
createdBookingIds.push(booking1.id);
|
|
|
|
const booking2 = await prisma.booking.create({
|
|
data: {
|
|
uid: "booking-2-uid",
|
|
title: "Booking 2",
|
|
startTime: new Date(Date.now() + 172800000), // 2 days from now
|
|
endTime: new Date(Date.now() + 176400000),
|
|
userId: testUser.id,
|
|
eventTypeId: testEventType.id,
|
|
status: BookingStatus.PENDING,
|
|
paid: false,
|
|
attendees: {
|
|
create: [
|
|
{
|
|
email: "attendee-b2@test.com",
|
|
name: "Attendee B2",
|
|
timeZone: "America/New_York",
|
|
},
|
|
],
|
|
},
|
|
payment: {
|
|
create: {
|
|
uid: "payment-uid-b2",
|
|
appId: "stripe",
|
|
amount: 1000,
|
|
fee: 0,
|
|
currency: "usd",
|
|
success: false,
|
|
refunded: false,
|
|
data: {},
|
|
externalId: "ext-payment-b2",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
createdBookingIds.push(booking2.id);
|
|
|
|
const ref1 = await prisma.bookingReference.create({
|
|
data: {
|
|
type: "stripe_payment",
|
|
uid: "stripe-ref-b1",
|
|
meetingId: "meeting-b1",
|
|
credentialId: newCredential.id,
|
|
bookingId: booking1.id,
|
|
},
|
|
});
|
|
createdBookingReferenceIds.push(ref1.id);
|
|
|
|
const ref2 = await prisma.bookingReference.create({
|
|
data: {
|
|
type: "stripe_payment",
|
|
uid: "stripe-ref-b2",
|
|
meetingId: "meeting-b2",
|
|
credentialId: newCredential.id,
|
|
bookingId: booking2.id,
|
|
},
|
|
});
|
|
createdBookingReferenceIds.push(ref2.id);
|
|
|
|
await deleteCredentialWithMetadata(newCredential.id, testUser.id);
|
|
|
|
const allRefsAfter = await prisma.bookingReference.findMany({
|
|
where: {
|
|
id: {
|
|
in: [ref1.id, ref2.id],
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(allRefsAfter).toHaveLength(2);
|
|
expect(allRefsAfter.every((ref) => ref.deleted === true)).toBe(true);
|
|
});
|
|
|
|
it("should not affect references of paid bookings when credential is deleted", async () => {
|
|
const newCredential = await prisma.credential.create({
|
|
data: {
|
|
type: "stripe_payment",
|
|
key: { test_key: "test_value" },
|
|
userId: testUser.id,
|
|
appId: "stripe",
|
|
},
|
|
});
|
|
|
|
const paidBooking = await prisma.booking.create({
|
|
data: {
|
|
uid: "paid-booking-uid",
|
|
title: "Paid Booking",
|
|
startTime: new Date(Date.now() + 86400000),
|
|
endTime: new Date(Date.now() + 90000000),
|
|
userId: testUser.id,
|
|
eventTypeId: testEventType.id,
|
|
status: BookingStatus.ACCEPTED,
|
|
paid: true,
|
|
attendees: {
|
|
create: [
|
|
{
|
|
email: "paid-attendee@test.com",
|
|
name: "Paid Attendee",
|
|
timeZone: "America/New_York",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
});
|
|
createdBookingIds.push(paidBooking.id);
|
|
|
|
const paidBookingRef = await prisma.bookingReference.create({
|
|
data: {
|
|
type: "stripe_payment",
|
|
uid: "stripe-paid-ref",
|
|
meetingId: "stripe-paid-meeting",
|
|
credentialId: newCredential.id,
|
|
bookingId: paidBooking.id,
|
|
},
|
|
});
|
|
createdBookingReferenceIds.push(paidBookingRef.id);
|
|
|
|
await deleteCredentialWithMetadata(newCredential.id, testUser.id);
|
|
|
|
const refAfter = await prisma.bookingReference.findUnique({
|
|
where: { id: paidBookingRef.id },
|
|
});
|
|
|
|
expect(refAfter).toBeDefined();
|
|
expect(refAfter?.deleted).toBe(null);
|
|
|
|
const bookingAfter = await prisma.booking.findUnique({
|
|
where: { id: paidBooking.id },
|
|
});
|
|
expect(bookingAfter?.status).toBe(BookingStatus.ACCEPTED);
|
|
});
|
|
});
|
|
|
|
describe("Soft-delete behavior with queries after credential deletion", () => {
|
|
it("should exclude soft-deleted references from regular queries", async () => {
|
|
const newCredential = await prisma.credential.create({
|
|
data: {
|
|
type: "stripe_payment",
|
|
key: { test_key: "test_value" },
|
|
userId: testUser.id,
|
|
appId: "stripe",
|
|
},
|
|
});
|
|
|
|
const unpaidBooking = await prisma.booking.create({
|
|
data: {
|
|
uid: "query-test-booking-uid",
|
|
title: "Query Test Booking",
|
|
startTime: new Date(Date.now() + 86400000),
|
|
endTime: new Date(Date.now() + 90000000),
|
|
userId: testUser.id,
|
|
eventTypeId: testEventType.id,
|
|
status: BookingStatus.PENDING,
|
|
paid: false,
|
|
attendees: {
|
|
create: [
|
|
{
|
|
email: "query-test@test.com",
|
|
name: "Query Test",
|
|
timeZone: "America/New_York",
|
|
},
|
|
],
|
|
},
|
|
payment: {
|
|
create: {
|
|
uid: "payment-uid-query-test",
|
|
appId: "stripe",
|
|
amount: 1000,
|
|
fee: 0,
|
|
currency: "usd",
|
|
success: false,
|
|
refunded: false,
|
|
data: {},
|
|
externalId: "ext-payment-query-test",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
createdBookingIds.push(unpaidBooking.id);
|
|
|
|
const ref = await prisma.bookingReference.create({
|
|
data: {
|
|
type: "stripe_payment",
|
|
uid: "query-test-ref",
|
|
meetingId: "query-test-meeting",
|
|
credentialId: newCredential.id,
|
|
bookingId: unpaidBooking.id,
|
|
},
|
|
});
|
|
createdBookingReferenceIds.push(ref.id);
|
|
|
|
await deleteCredentialWithMetadata(newCredential.id, testUser.id);
|
|
|
|
const activeRefs = await prisma.bookingReference.findMany({
|
|
where: {
|
|
bookingId: unpaidBooking.id,
|
|
deleted: null,
|
|
},
|
|
});
|
|
|
|
expect(activeRefs).toHaveLength(0);
|
|
|
|
const allRefs = await prisma.bookingReference.findMany({
|
|
where: {
|
|
bookingId: unpaidBooking.id,
|
|
},
|
|
});
|
|
|
|
expect(allRefs).toHaveLength(1);
|
|
expect(allRefs[0].deleted).toBe(true);
|
|
});
|
|
});
|
|
});
|