fix: Allow zero routingFormResponseId in API v2 available endpoint (#21301)
* Allow zero routingFormResponseId * fix: conditionally validate routingFormResponseId to allow 0 only for dry run operations Co-Authored-By: hariom@cal.com <hariom@cal.com> * fix: pass _isDryRun parameter to API v2 available endpoint Co-Authored-By: hariom@cal.com <hariom@cal.com> * fix runtime error and improve message * Add e2e test to verify the constraint works --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: hariom@cal.com <hariom@cal.com>
This commit is contained in:
co-authored by
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
hariom@cal.com <hariom@cal.com>
parent
5bc9d71a88
commit
fab364f084
+80
@@ -685,6 +685,86 @@ describe("Slots 2024-04-15 Endpoints", () => {
|
||||
await bookingsRepositoryFixture.deleteById(booking.id);
|
||||
});
|
||||
|
||||
describe("routingFormResponseId and _isDryRun validation", () => {
|
||||
const baseUrl = "/api/v2/slots/available";
|
||||
const baseParams = {
|
||||
startTime: "2050-09-05",
|
||||
endTime: "2050-09-10",
|
||||
};
|
||||
|
||||
it("should allow routingFormResponseId=0 in dry-run mode", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(baseUrl)
|
||||
.query({
|
||||
...baseParams,
|
||||
eventTypeId,
|
||||
routingFormResponseId: 0,
|
||||
_isDryRun: true,
|
||||
})
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body.status).toEqual(SUCCESS_STATUS);
|
||||
});
|
||||
});
|
||||
|
||||
it("should reject routingFormResponseId=1 in dry-run mode", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(baseUrl)
|
||||
.query({
|
||||
...baseParams,
|
||||
eventTypeId,
|
||||
routingFormResponseId: 1,
|
||||
_isDryRun: true,
|
||||
})
|
||||
.expect(400)
|
||||
.expect((res) => {
|
||||
expect(res.body.error.details.errors[0].constraints.routingFormResponseIdValidator).toContain("routingFormResponseId must be 0 for dry run");
|
||||
});
|
||||
});
|
||||
|
||||
it("should allow routingFormResponseId=1 in non-dry-run mode", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(baseUrl)
|
||||
.query({
|
||||
...baseParams,
|
||||
eventTypeId,
|
||||
routingFormResponseId: 1,
|
||||
})
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body.status).toEqual(SUCCESS_STATUS);
|
||||
});
|
||||
});
|
||||
|
||||
it("should reject routingFormResponseId=0 in non-dry-run mode", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(baseUrl)
|
||||
.query({
|
||||
...baseParams,
|
||||
eventTypeId,
|
||||
routingFormResponseId: 0,
|
||||
})
|
||||
.expect(400)
|
||||
.expect((res) => {
|
||||
expect(res.body.error.details.errors[0].constraints.routingFormResponseIdValidator).toContain("routingFormResponseId must be a positive number");
|
||||
});
|
||||
});
|
||||
|
||||
it("should reject routingFormResponseId=-1 in non-dry-run mode", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(baseUrl)
|
||||
.query({
|
||||
...baseParams,
|
||||
eventTypeId,
|
||||
routingFormResponseId: -1,
|
||||
})
|
||||
.expect(400)
|
||||
.expect((res) => {
|
||||
expect(res.body.error.details.errors[0].constraints.routingFormResponseIdValidator).toContain("routingFormResponseId must be a positive number");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await selectedSlotsRepositoryFixture.deleteByUId(reservedSlotUid);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useSearchParams } from "next/navigation";
|
||||
|
||||
import { updateEmbedBookerState } from "@calcom/embed-core/src/embed-iframe";
|
||||
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
|
||||
import { isBookingDryRun } from "@calcom/features/bookings/Booker/utils/isBookingDryRun";
|
||||
import { useTimesForSchedule } from "@calcom/features/schedules/lib/use-schedule/useTimesForSchedule";
|
||||
import { getRoutedTeamMemberIdsFromSearchParams } from "@calcom/lib/bookings/getRoutedTeamMemberIdsFromSearchParams";
|
||||
import { PUBLIC_QUERY_AVAILABLE_SLOTS_INTERVAL_SECONDS } from "@calcom/lib/constants";
|
||||
@@ -97,6 +98,7 @@ export const useSchedule = ({
|
||||
email,
|
||||
// Ensures that connectVersion causes a refresh of the data
|
||||
...(embedConnectVersion ? { embedConnectVersion } : {}),
|
||||
_isDryRun: searchParams ? isBookingDryRun(searchParams) : false,
|
||||
};
|
||||
|
||||
const options = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ApiProperty, ApiPropertyOptional, ApiHideProperty } from "@nestjs/swagger";
|
||||
import { Transform } from "class-transformer";
|
||||
import type { ValidationArguments, ValidatorConstraintInterface } from "class-validator";
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
@@ -10,10 +11,35 @@ import {
|
||||
IsString,
|
||||
Min,
|
||||
IsEnum,
|
||||
ValidatorConstraint,
|
||||
Validate,
|
||||
} from "class-validator";
|
||||
|
||||
import { SlotFormat } from "@calcom/platform-enums";
|
||||
|
||||
@ValidatorConstraint({ name: "routingFormResponseIdValidator", async: false })
|
||||
class RoutingFormResponseIdValidator implements ValidatorConstraintInterface {
|
||||
validate(routingFormResponseId: number, args: ValidationArguments) {
|
||||
if (routingFormResponseId === undefined) return true;
|
||||
|
||||
const payload = args.object as GetAvailableSlotsInput_2024_04_15;
|
||||
|
||||
if (payload._isDryRun) {
|
||||
return routingFormResponseId === 0;
|
||||
}
|
||||
|
||||
return routingFormResponseId >= 1;
|
||||
}
|
||||
|
||||
defaultMessage(args: ValidationArguments) {
|
||||
const payload = args.object as GetAvailableSlotsInput_2024_04_15;
|
||||
if (payload._isDryRun) {
|
||||
return "routingFormResponseId must be 0 for dry run";
|
||||
}
|
||||
return "routingFormResponseId must be a positive number";
|
||||
}
|
||||
}
|
||||
|
||||
export class GetAvailableSlotsInput_2024_04_15 {
|
||||
@IsDateString({ strict: true })
|
||||
@ApiProperty({
|
||||
@@ -145,7 +171,7 @@ export class GetAvailableSlotsInput_2024_04_15 {
|
||||
@Transform(({ value }: { value: string }) => value && parseInt(value))
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Min(1, { message: "routingFormResponseId must be a positive number" })
|
||||
@Validate(RoutingFormResponseIdValidator)
|
||||
@ApiPropertyOptional()
|
||||
@ApiHideProperty()
|
||||
routingFormResponseId?: number;
|
||||
@@ -156,6 +182,12 @@ export class GetAvailableSlotsInput_2024_04_15 {
|
||||
@ApiHideProperty()
|
||||
_shouldServeCache?: boolean;
|
||||
|
||||
@Transform(({ value }) => value && value.toLowerCase() === "true")
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiHideProperty()
|
||||
_isDryRun?: boolean;
|
||||
|
||||
@Transform(({ value }) => value && value.toLowerCase() === "true")
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
|
||||
Reference in New Issue
Block a user