Skip to content

Commit d151351

Browse files
authored
Fix room-reservation frontend parsing (#138)
* dont deploy to DEV again when pushing to PROD * update parsing schemas * set seconds to zero when validatng * add transform to recurrenceEndDate * fix parsing * kick off ci
1 parent d451457 commit d151351

File tree

7 files changed

+389
-198
lines changed

7 files changed

+389
-198
lines changed

.github/workflows/deploy-prod.yml

Lines changed: 1 addition & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -25,55 +25,6 @@ jobs:
2525
- name: Run unit testing
2626
run: make test_unit
2727

28-
deploy-test-dev:
29-
runs-on: ubuntu-latest
30-
concurrency:
31-
group: ${{ github.event.repository.name }}-dev
32-
cancel-in-progress: false
33-
permissions:
34-
id-token: write
35-
contents: read
36-
environment: "AWS DEV"
37-
name: Deploy to DEV and Run Tests
38-
needs:
39-
- test-unit
40-
steps:
41-
- name: Set up Node for testing
42-
uses: actions/setup-node@v4
43-
with:
44-
node-version: 22.x
45-
46-
- uses: actions/checkout@v4
47-
env:
48-
HUSKY: "0"
49-
- uses: aws-actions/setup-sam@v2
50-
with:
51-
use-installer: true
52-
- name: Set up Python 3.11
53-
uses: actions/setup-python@v5
54-
with:
55-
python-version: 3.11
56-
- uses: aws-actions/configure-aws-credentials@v4
57-
with:
58-
role-to-assume: arn:aws:iam::427040638965:role/GitHubActionsRole
59-
role-session-name: Core_Dev_Prod_Deployment_${{ github.run_id }}
60-
aws-region: us-east-1
61-
- name: Publish to AWS
62-
run: make deploy_dev
63-
env:
64-
HUSKY: "0"
65-
VITE_RUN_ENVIRONMENT: dev
66-
67-
- name: Run live testing
68-
run: make test_live_integration
69-
env:
70-
JWT_KEY: ${{ secrets.JWT_KEY }}
71-
- name: Run E2E testing
72-
run: make test_e2e
73-
env:
74-
PLAYWRIGHT_USERNAME: ${{ secrets.PLAYWRIGHT_USERNAME }}
75-
PLAYWRIGHT_PASSWORD: ${{ secrets.PLAYWRIGHT_PASSWORD }}
76-
7728
deploy-prod:
7829
runs-on: ubuntu-latest
7930
name: Deploy to Prod and Run Health Check
@@ -105,7 +56,7 @@ jobs:
10556
- uses: aws-actions/configure-aws-credentials@v4
10657
with:
10758
role-to-assume: arn:aws:iam::298118738376:role/GitHubActionsRole
108-
role-session-name: Core_Dev_Prod_Deployment_${{ github.run_id }}
59+
role-session-name: Core_Prod_Deployment_${{ github.run_id }}
10960
aws-region: us-east-1
11061
- name: Publish to AWS
11162
run: make deploy_prod

src/common/types/roomRequest.ts

Lines changed: 59 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -152,58 +152,67 @@ 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+
}).transform((date) => {
160+
const d = new Date(date);
161+
d.setSeconds(0, 0);
162+
return d;
163+
}),
164+
eventEnd: z.coerce.date({
165+
required_error: "Event end date and time is required",
166+
invalid_type_error: "Event end must be a valid date and time",
167+
}).transform((date) => {
168+
const d = new Date(date);
169+
d.setSeconds(0, 0);
170+
return d;
171+
}),
172+
theme: z.enum(eventThemeOptions, {
173+
required_error: "Event theme must be provided",
174+
invalid_type_error: "Event theme must be provided",
175+
}),
176+
description: z
177+
.string()
178+
.min(10, "Description must have at least 10 words")
179+
.max(1000, "Description cannot exceed 1000 characters")
180+
.refine((val) => val.split(/\s+/).filter(Boolean).length >= 10, {
181+
message: "Description must have at least 10 words",
169182
}),
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-
})
183+
// Recurring event fields
184+
isRecurring: z.boolean().default(false),
185+
recurrencePattern: z.enum(["weekly", "biweekly", "monthly"]).optional(),
186+
recurrenceEndDate: z.coerce.date().optional().transform((date) => {
187+
if (!date) { return date; }
188+
const d = new Date(date);
189+
d.setSeconds(0, 0);
190+
return d;
191+
}),
192+
// Setup time fields
193+
setupNeeded: z.boolean().default(false),
194+
setupMinutesBefore: z.number().min(5).max(60).optional(),
195+
// Existing fields
196+
hostingMinors: z.boolean(),
197+
locationType: z.enum(["in-person", "virtual", "both"]),
198+
spaceType: z.optional(z.string().min(1)),
199+
specificRoom: z.optional(z.string().min(1)),
200+
estimatedAttendees: z.optional(z.number().positive()),
201+
seatsNeeded: z.optional(z.number().positive()),
202+
setupDetails: z.string().min(1).nullable().optional(),
203+
onCampusPartners: z.string().min(1).nullable(),
204+
offCampusPartners: z.string().min(1).nullable(),
205+
nonIllinoisSpeaker: z.string().min(1).nullable(),
206+
nonIllinoisAttendees: z.number().min(1).nullable(),
207+
foodOrDrink: z.boolean(),
208+
crafting: z.boolean(),
209+
comments: z.string().optional(),
210+
})
211+
212+
export const roomRequestSchema = roomRequestDataSchema
200213
.refine(
201214
(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;
215+
return data.eventEnd > data.eventStart;
207216
},
208217
{
209218
message: "End date/time must be after start date/time",
@@ -241,7 +250,7 @@ export const roomRequestSchema = roomRequestBaseSchema
241250
if (data.isRecurring && data.recurrenceEndDate && data.eventStart) {
242251
const endDateWithTime = new Date(data.recurrenceEndDate);
243252
endDateWithTime.setHours(23, 59, 59, 999);
244-
return endDateWithTime >= data.eventStart;
253+
return endDateWithTime.getTime() >= data.eventStart.getTime();
245254
}
246255
return true;
247256
},

src/ui/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@
4343
"react-qr-reader": "^3.0.0-beta-1",
4444
"react-router-dom": "^7.5.2",
4545
"zod": "^3.24.3",
46-
"zod-openapi": "^4.2.4"
46+
"zod-openapi": "^4.2.4",
47+
"zod-validation-error": "^3.4.0"
4748
},
4849
"devDependencies": {
4950
"@eslint/compat": "^1.2.8",

src/ui/pages/roomRequest/NewRoomRequest.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import {
3030
} from "@common/types/roomRequest";
3131
import { useNavigate } from "react-router-dom";
3232
import { notifications } from "@mantine/notifications";
33+
import { fromError } from "zod-validation-error";
34+
import { ZodError } from "zod";
3335

3436
// Component for yes/no questions with conditional content
3537
interface ConditionalFieldProps {
@@ -208,7 +210,6 @@ const NewRoomRequest: React.FC<NewRoomRequestProps> = ({
208210
// Get all validation errors from zod, which returns ReactNode
209211
const allErrors: Record<string, React.ReactNode> =
210212
zodResolver(roomRequestSchema)(values);
211-
212213
// If in view mode, return no errors
213214
if (viewOnly) {
214215
return {};
@@ -310,7 +311,7 @@ const NewRoomRequest: React.FC<NewRoomRequestProps> = ({
310311
}, [form.values.isRecurring]);
311312

312313
const handleSubmit = async () => {
313-
if (viewOnly) {
314+
if (viewOnly || isSubmitting) {
314315
return;
315316
}
316317
const apiFormValues = { ...form.values };
@@ -331,19 +332,24 @@ const NewRoomRequest: React.FC<NewRoomRequestProps> = ({
331332
try {
332333
values = await roomRequestSchema.parseAsync(apiFormValues);
333334
} catch (e) {
335+
let message = "Check the browser console for more details.";
336+
if (e instanceof ZodError) {
337+
message = fromError(e).toString();
338+
}
334339
notifications.show({
335340
title: "Submission failed to validate",
336-
message: "Check the browser console for more details.",
341+
message,
342+
color: "red",
337343
});
338-
throw e;
344+
setIsSubmitting(false);
345+
return;
339346
}
340347
const response = await createRoomRequest(values);
348+
await navigate("/roomRequests");
341349
notifications.show({
342350
title: "Room Request Submitted",
343351
message: `The request ID is ${response.id}.`,
344352
});
345-
setIsSubmitting(false);
346-
navigate("/roomRequests");
347353
} catch (e) {
348354
notifications.show({
349355
color: "red",

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: 33 additions & 2 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();
@@ -368,8 +399,8 @@ describe("Test Room Request Creation", async () => {
368399
theme: "Athletics",
369400
description:
370401
"A well-formed description that has at least ten total words.",
371-
eventStart: new Date("2025-04-24T12:00:00Z"),
372-
eventEnd: new Date("2025-04-24T13:00:00Z"),
402+
eventStart: "2025-04-24T12:00:00Z",
403+
eventEnd: "2025-04-24T13:00:00Z",
373404
isRecurring: false,
374405
setupNeeded: false,
375406
hostingMinors: false,

0 commit comments

Comments
 (0)