Files
calendar/packages/lib/server/getServerErrorFromUnknown.test.ts
T
Benny JooGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
e625308881 fix: Improve the error handling in Stripe collectCard method (#27249)
* fix: improve error handling in Stripe collectCard method

- Add CollectCardFailure error code for better error categorization
- Replace generic error with ErrorWithCode for consistent error handling
- Add error mappings for common Stripe errors (customer not found, invalid API key, rate limit, etc.)
- Include Stripe error message when available for better debugging
- Follow the same pattern used in chargeCard method

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: add CollectCardFailure to getHttpStatusCode mapping

This ensures the error returns 400 instead of 500 when collectCard fails.

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* docs: add comments explaining ChargeCardFailure vs CollectCardFailure

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* refactor: simplify collectCard error handling

Remove complex error mapping logic per code review feedback.

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* test: add CollectCardFailure to HTTP status code tests

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-27 06:47:11 +00:00

240 lines
8.2 KiB
TypeScript

import { describe, expect, test } from "vitest";
import { ZodError } from "zod";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { ErrorWithCode } from "@calcom/lib/errors";
import { Prisma } from "@calcom/prisma/client";
import { HttpError } from "../http-error";
import { TracedError } from "../tracing/error";
import { getServerErrorFromUnknown } from "./getServerErrorFromUnknown";
const test400Codes = [
ErrorCode.RequestBodyWithouEnd,
ErrorCode.MissingPaymentCredential,
ErrorCode.MissingPaymentAppId,
ErrorCode.AvailabilityNotFoundInSchedule,
ErrorCode.CancelledBookingsCannotBeRescheduled,
ErrorCode.BookingTimeOutOfBounds,
ErrorCode.BookingNotAllowedByRestrictionSchedule,
ErrorCode.BookerLimitExceeded,
ErrorCode.BookerLimitExceededReschedule,
ErrorCode.ChargeCardFailure,
ErrorCode.CollectCardFailure,
];
const test404Codes = [
ErrorCode.EventTypeNotFound,
ErrorCode.BookingNotFound,
ErrorCode.RestrictionScheduleNotFound,
];
const test409Codes = [
ErrorCode.NoAvailableUsersFound,
ErrorCode.FixedHostsUnavailableForBooking,
ErrorCode.RoundRobinHostsUnavailableForBooking,
ErrorCode.AlreadySignedUpForBooking,
ErrorCode.BookingSeatsFull,
ErrorCode.NotEnoughAvailableSeats,
ErrorCode.BookingConflict,
ErrorCode.PaymentCreationFailure,
];
describe("getServerErrorFromUnknown", () => {
test("should handle a StripeInvalidRequestError", () => {
const stripeError = {
name: "StripeInvalidRequestError",
type: "StripeInvalidRequestError",
rawType: "invalid_request_error",
message: "No such customer: cus_12345; a valid customer ID is required.",
code: "resource_missing",
doc_url: "https://stripe.com/docs/error-codes/resource-missing",
statusCode: 400,
raw: {
error: { type: "invalid_request_error", message: "No such customer" },
},
headers: {
"content-type": "application/json",
"request-id": "req_abc123xyz",
},
requestId: "req_abc123xyz",
param: "customer",
};
Object.setPrototypeOf(stripeError, Error.prototype);
const result = getServerErrorFromUnknown(stripeError);
expect(result).toBeInstanceOf(HttpError);
expect(result.statusCode).toBe(400);
expect(result.message).toBe(stripeError.message);
// Check that cause contains the essential properties (Vitest 4.0 uses stricter equality)
expect(result.cause).toMatchObject({
name: stripeError.name,
message: stripeError.message,
});
expect(result.name).toBe("HttpError");
});
test("ErrorWithCode should preserve data property", () => {
const rescheduleData = {
rescheduleUid: "abc123",
startTime: new Date("2024-01-15T10:00:00Z"),
attendees: [{ name: "John Doe", email: "john@example.com" }],
};
const error = new ErrorWithCode(
ErrorCode.BookerLimitExceededReschedule,
"Maximum booking limit reached, please reschedule an existing booking",
rescheduleData
);
const result = getServerErrorFromUnknown(error);
expect(result.statusCode).toBe(400);
expect(result.message).toBe("Maximum booking limit reached, please reschedule an existing booking");
expect(result.data).toEqual(rescheduleData);
});
test("Plain Error with ErrorCode message should work the same as ErrorWithCode", () => {
const error = new Error(ErrorCode.NoAvailableUsersFound);
const result = getServerErrorFromUnknown(error);
expect(result.statusCode).toBe(409);
expect(result.message).toBe(ErrorCode.NoAvailableUsersFound);
});
test("Generic Error without ErrorCode should return 500", () => {
const error = new Error("Some unexpected server error");
const result = getServerErrorFromUnknown(error);
expect(result.statusCode).toBe(500);
expect(result.message).toBe("Some unexpected server error");
});
test("Server-side ErrorCode should return 500", () => {
const error = new ErrorWithCode(ErrorCode.CreatingOauthClientError, "OAuth client creation failed");
const result = getServerErrorFromUnknown(error);
expect(result.statusCode).toBe(500);
expect(result.message).toBe("OAuth client creation failed");
});
});
describe("TracedError handling", () => {
test("should extract traceId and tracedData from TracedError", () => {
const originalError = new Error("Original error message");
const tracedData = { userId: "123", operation: "booking" };
const traceContext = {
traceId: "trace_abc123",
spanId: "span_123",
operation: "test_operation",
};
const tracedError = new TracedError(originalError, traceContext, tracedData);
const result = getServerErrorFromUnknown(tracedError);
expect(result.statusCode).toBe(500);
expect(result.message).toBe("Original error message");
expect(result.data).toEqual({ ...tracedData, traceId: traceContext.traceId });
});
test("should handle TracedError wrapping ErrorWithCode", () => {
const originalError = new ErrorWithCode(ErrorCode.BookingNotFound, "Booking not found");
const tracedData = { bookingId: "456" };
const traceContext = {
traceId: "trace_def456",
spanId: "span_456",
operation: "booking_lookup",
};
const tracedError = new TracedError(originalError, traceContext, tracedData);
const result = getServerErrorFromUnknown(tracedError);
expect(result.statusCode).toBe(404);
expect(result.message).toBe("Booking not found");
expect(result.data).toEqual({ ...tracedData, traceId: traceContext.traceId });
});
});
describe("ZodError handling", () => {
test("should handle ZodError with validation issues", () => {
const zodError = new ZodError([
{
code: "invalid_type",
expected: "string",
received: "number",
path: ["name"],
message: "Expected string, received number",
},
]);
const result = getServerErrorFromUnknown(zodError);
expect(result.statusCode).toBe(400);
expect(result.message).toBe("invalid_type in 'name': Expected string, received number");
expect(result.cause).toBe(zodError);
});
});
describe("Prisma error handling", () => {
test("should handle Prisma P2025 error (record not found)", () => {
const prismaError = new Error("Record to delete does not exist.") as any;
prismaError.code = "P2025";
prismaError.clientVersion = "5.0.0";
Object.setPrototypeOf(prismaError, Prisma.PrismaClientKnownRequestError.prototype);
const result = getServerErrorFromUnknown(prismaError);
expect(result.statusCode).toBe(404);
expect(result.message).toBe("Record to delete does not exist.");
expect(result.cause).toBe(prismaError);
});
test("should handle other Prisma errors as 400", () => {
const prismaError = new Error("Foreign key constraint failed") as any;
prismaError.code = "P2003";
prismaError.clientVersion = "5.0.0";
Object.setPrototypeOf(prismaError, Prisma.PrismaClientKnownRequestError.prototype);
const result = getServerErrorFromUnknown(prismaError);
expect(result.statusCode).toBe(400);
expect(result.message).toBe("Foreign key constraint failed");
});
});
test("should handle unknown type fallback", () => {
const result = getServerErrorFromUnknown({ someProperty: "value" });
expect(result.statusCode).toBe(500);
expect(result.message).toBe("Unhandled error of type 'object'. Please reach out for our customer support.");
expect(result.data).toBeUndefined();
});
test400Codes.forEach((errorCode) => {
test(`${errorCode} should return 400 Bad Request`, () => {
const error = new ErrorWithCode(errorCode, `Test message for ${errorCode}`);
const result = getServerErrorFromUnknown(error);
expect(result.statusCode).toBe(400);
});
});
test404Codes.forEach((errorCode) => {
test(`${errorCode} should return 404 Not Found`, () => {
const error = new ErrorWithCode(errorCode, `Test message for ${errorCode}`);
const result = getServerErrorFromUnknown(error);
expect(result.statusCode).toBe(404);
});
});
test409Codes.forEach((errorCode) => {
test(`${errorCode} should return 409 Conflict`, () => {
const error = new ErrorWithCode(errorCode, `Test message for ${errorCode}`);
const result = getServerErrorFromUnknown(error);
expect(result.statusCode).toBe(409);
});
});