Skip to content

Commit d70ae5d

Browse files
committed
update parsing schemas
1 parent c6a435f commit d70ae5d

File tree

3 files changed

+95
-55
lines changed

3 files changed

+95
-55
lines changed

src/common/types/roomRequest.ts

Lines changed: 46 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -152,58 +152,54 @@ export const roomRequestBaseSchema = z.object({
152152
.string()
153153
.regex(/^(fa|sp|su|wi)\d{2}$/, "Invalid semester provided"),
154154
});
155-
156-
export const roomRequestSchema = roomRequestBaseSchema
157-
.extend({
158-
eventStart: z.coerce.date({
159-
required_error: "Event start date and time is required",
160-
invalid_type_error: "Event start must be a valid date and time",
161-
}),
162-
eventEnd: z.coerce.date({
163-
required_error: "Event end date and time is required",
164-
invalid_type_error: "Event end must be a valid date and time",
165-
}),
166-
theme: z.enum(eventThemeOptions, {
167-
required_error: "Event theme must be provided",
168-
invalid_type_error: "Event theme must be provided",
155+
export const roomRequestDataSchema = roomRequestBaseSchema.extend({
156+
eventStart: z.coerce.date({
157+
required_error: "Event start date and time is required",
158+
invalid_type_error: "Event start must be a valid date and time",
159+
}),
160+
eventEnd: z.coerce.date({
161+
required_error: "Event end date and time is required",
162+
invalid_type_error: "Event end must be a valid date and time",
163+
}),
164+
theme: z.enum(eventThemeOptions, {
165+
required_error: "Event theme must be provided",
166+
invalid_type_error: "Event theme must be provided",
167+
}),
168+
description: z
169+
.string()
170+
.min(10, "Description must have at least 10 words")
171+
.max(1000, "Description cannot exceed 1000 characters")
172+
.refine((val) => val.split(/\s+/).filter(Boolean).length >= 10, {
173+
message: "Description must have at least 10 words",
169174
}),
170-
description: z
171-
.string()
172-
.min(10, "Description must have at least 10 words")
173-
.max(1000, "Description cannot exceed 1000 characters")
174-
.refine((val) => val.split(/\s+/).filter(Boolean).length >= 10, {
175-
message: "Description must have at least 10 words",
176-
}),
177-
// Recurring event fields
178-
isRecurring: z.boolean().default(false),
179-
recurrencePattern: z.enum(["weekly", "biweekly", "monthly"]).optional(),
180-
recurrenceEndDate: z.coerce.date().optional(),
181-
// Setup time fields
182-
setupNeeded: z.boolean().default(false),
183-
setupMinutesBefore: z.number().min(5).max(60).optional(),
184-
// Existing fields
185-
hostingMinors: z.boolean(),
186-
locationType: z.enum(["in-person", "virtual", "both"]),
187-
spaceType: z.optional(z.string().min(1)),
188-
specificRoom: z.optional(z.string().min(1)),
189-
estimatedAttendees: z.optional(z.number().positive()),
190-
seatsNeeded: z.optional(z.number().positive()),
191-
setupDetails: z.string().min(1).nullable().optional(),
192-
onCampusPartners: z.string().min(1).nullable(),
193-
offCampusPartners: z.string().min(1).nullable(),
194-
nonIllinoisSpeaker: z.string().min(1).nullable(),
195-
nonIllinoisAttendees: z.number().min(1).nullable(),
196-
foodOrDrink: z.boolean(),
197-
crafting: z.boolean(),
198-
comments: z.string().optional(),
199-
})
175+
// Recurring event fields
176+
isRecurring: z.boolean().default(false),
177+
recurrencePattern: z.enum(["weekly", "biweekly", "monthly"]).optional(),
178+
recurrenceEndDate: z.coerce.date().optional(),
179+
// Setup time fields
180+
setupNeeded: z.boolean().default(false),
181+
setupMinutesBefore: z.number().min(5).max(60).optional(),
182+
// Existing fields
183+
hostingMinors: z.boolean(),
184+
locationType: z.enum(["in-person", "virtual", "both"]),
185+
spaceType: z.optional(z.string().min(1)),
186+
specificRoom: z.optional(z.string().min(1)),
187+
estimatedAttendees: z.optional(z.number().positive()),
188+
seatsNeeded: z.optional(z.number().positive()),
189+
setupDetails: z.string().min(1).nullable().optional(),
190+
onCampusPartners: z.string().min(1).nullable(),
191+
offCampusPartners: z.string().min(1).nullable(),
192+
nonIllinoisSpeaker: z.string().min(1).nullable(),
193+
nonIllinoisAttendees: z.number().min(1).nullable(),
194+
foodOrDrink: z.boolean(),
195+
crafting: z.boolean(),
196+
comments: z.string().optional(),
197+
})
198+
199+
export const roomRequestSchema = roomRequestDataSchema
200200
.refine(
201201
(data) => {
202-
// Check if end time is after start time
203-
if (data.eventStart && data.eventEnd) {
204-
return data.eventEnd > data.eventStart;
205-
}
206-
return true;
202+
return data.eventEnd.getTime() > data.eventStart.getTime();
207203
},
208204
{
209205
message: "End date/time must be after start date/time",
@@ -241,7 +237,7 @@ export const roomRequestSchema = roomRequestBaseSchema
241237
if (data.isRecurring && data.recurrenceEndDate && data.eventStart) {
242238
const endDateWithTime = new Date(data.recurrenceEndDate);
243239
endDateWithTime.setHours(23, 59, 59, 999);
244-
return endDateWithTime >= data.eventStart;
240+
return endDateWithTime.getTime() >= data.eventStart.getTime();
245241
}
246242
return true;
247243
},

src/ui/pages/roomRequest/ViewRoomRequest.page.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
RoomRequestStatusUpdatePostBody,
2525
roomRequestStatusUpdateRequest,
2626
formatStatus,
27+
roomRequestDataSchema,
2728
} from "@common/types/roomRequest";
2829
import { useParams } from "react-router-dom";
2930
import { getStatusColor, getStatusIcon } from "./roomRequestUtils";
@@ -55,11 +56,23 @@ export const ViewRoomRequest: React.FC = () => {
5556
const response = await api.get(
5657
`/api/v1/roomRequests/${semesterId}/${requestId}`,
5758
);
58-
const parsed = {
59-
data: await roomRequestSchema.parseAsync(response.data.data),
60-
updates: response.data.updates,
61-
};
62-
setData(parsed);
59+
try {
60+
const parsed = {
61+
data: await roomRequestSchema.parseAsync(response.data.data),
62+
updates: response.data.updates,
63+
};
64+
setData(parsed);
65+
} catch (e) {
66+
notifications.show({
67+
title: "Failed to validate room reservation",
68+
message: "Data may not render correctly or may be invalid.",
69+
color: "red",
70+
});
71+
setData({
72+
data: await roomRequestDataSchema.parseAsync(response.data.data),
73+
updates: response.data.updates,
74+
});
75+
}
6376
};
6477
const submitStatusChange = async () => {
6578
try {

tests/unit/roomRequests.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,37 @@ describe("Test Room Request Creation", async () => {
154154
);
155155
expect(ddbMock.calls.length).toEqual(0);
156156
});
157+
test("Validation failure: eventEnd equals eventStart", async () => {
158+
const testJwt = createJwt();
159+
ddbMock.rejects();
160+
const response = await supertest(app.server)
161+
.post("/api/v1/roomRequests")
162+
.set("authorization", `Bearer ${testJwt}`)
163+
.send({
164+
host: "Infrastructure Committee",
165+
title: "Valid Title",
166+
semester: "sp25",
167+
theme: "Athletics",
168+
description: "This is a valid description with at least ten words.",
169+
eventStart: "2025-04-25T10:00:00Z",
170+
eventEnd: "2025-04-25T10:00:00Z",
171+
isRecurring: false,
172+
setupNeeded: false,
173+
hostingMinors: false,
174+
locationType: "virtual",
175+
foodOrDrink: false,
176+
crafting: false,
177+
onCampusPartners: null,
178+
offCampusPartners: null,
179+
nonIllinoisSpeaker: null,
180+
nonIllinoisAttendees: null,
181+
});
182+
expect(response.statusCode).toBe(400);
183+
expect(response.body.message).toContain(
184+
"End date/time must be after start date/time",
185+
);
186+
expect(ddbMock.calls.length).toEqual(0);
187+
});
157188
test("Validation failure: isRecurring without recurrencePattern and endDate", async () => {
158189
const testJwt = createJwt();
159190
ddbMock.rejects();

0 commit comments

Comments
 (0)