From bd9866520ba20216d75ac5f025e63f68798017fe Mon Sep 17 00:00:00 2001 From: Savannah Savings Date: Mon, 8 Jun 2026 17:01:26 +0000 Subject: [PATCH] =?UTF-8?q?fix(portal):=20GRO-2203=20validate=20petId=20as?= =?UTF-8?q?=20UUID=20before=20PATCH=20lookup=20(500=E2=86=92404)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A non-UUID :petId passed straight into where(eq(pets.id, petId)) made Postgres throw "invalid input syntax for type uuid", surfacing as an unhandled 500. Guard the param with z.string().uuid() and return the existing 404 {"error":"Not found"} for malformed ids, mirroring the GRO-2014 fix in pets.ts. Valid-UUID-not-found already returned 404. - Add regression test (non-UUID petId → 404, no mutation) - Update UAT_PLAYBOOK.md §8 (TC-API-8.16) Co-Authored-By: Paperclip --- UAT_PLAYBOOK.md | 1 + src/__tests__/portalPets.test.ts | 17 +++++++++++++++++ src/routes/portal.ts | 8 ++++++++ 3 files changed, 26 insertions(+) diff --git a/UAT_PLAYBOOK.md b/UAT_PLAYBOOK.md index cacd4f4..0267135 100644 --- a/UAT_PLAYBOOK.md +++ b/UAT_PLAYBOOK.md @@ -281,6 +281,7 @@ This means: | TC-API-8.13 | Portal pet update — owner success + persistence (GRO-2187, fixes [GRO-1480](/GRO/issues/GRO-1480) §5.23) | With a portal session for the pet's owner, `PATCH /api/portal/pets/{petId}` with body `{ "name": "...", "breed": "...", "weightKg": 18.25, "healthAlerts": "...", "coatType": "double", "petSizeCategory": "xlarge", "preferredCuts": ["teddy bear"], "medicalAlerts": [{"type":"allergy","description":"oatmeal","severity":"medium"}] }` | 200 OK; response reflects the update with `petSizeCategory: "extra_large"` (web `xlarge` → DB `extra_large`). A follow-up `GET /api/portal/pets` shows the persisted values | | TC-API-8.14 | Portal pet update — non-owner blocked (GRO-2187) | `PATCH /api/portal/pets/{petId}` for a pet owned by a different client, using another client's portal session | 403 Forbidden (or 404 if pet id is unknown); no mutation persisted | | TC-API-8.15 | Portal pet update — invalid enum rejected (GRO-2187) | `PATCH /api/portal/pets/{petId}` with `coatType: "fluffy"` or `petSizeCategory: "gigantic"` | 422 Unprocessable Entity; pet unchanged | +| TC-API-8.16 | Portal pet update — malformed (non-UUID) petId returns 404 (GRO-2203) | With a valid portal session, `PATCH /api/portal/pets/not-a-uuid` with header `X-Impersonation-Session-Id` and body `{"coatType":"short"}` | 404 Not Found with body `{"error":"Not found"}` (was an unhandled 500 from the Postgres uuid cast in GRO-2203; mirrors the GRO-2014 guard). No mutation persisted | ### 4.9 Waitlist diff --git a/src/__tests__/portalPets.test.ts b/src/__tests__/portalPets.test.ts index c23bca0..a9e41c2 100644 --- a/src/__tests__/portalPets.test.ts +++ b/src/__tests__/portalPets.test.ts @@ -280,6 +280,23 @@ describe("PATCH /portal/pets/:petId", () => { expect(res.status).toBe(404); }); + it("returns 404 for a malformed (non-UUID) petId without hitting the db (GRO-2203)", async () => { + selectSessionRow = ACTIVE_SESSION; + // A non-UUID petId previously reached `where(eq(pets.id, ...))` and made + // Postgres throw "invalid input syntax for type uuid" → unhandled 500. + // It must now short-circuit to 404 before any select/update. + selectPetRow = PET; + + const res = await jsonPatch( + `/portal/pets/not-a-uuid`, + { coatType: "short" }, + { "X-Impersonation-Session-Id": SESSION_ID } + ); + + expect(res.status).toBe(404); + expect(updatedValues).toHaveLength(0); + }); + it("returns 422 for an invalid coatType", async () => { selectSessionRow = ACTIVE_SESSION; selectPetRow = PET; diff --git a/src/routes/portal.ts b/src/routes/portal.ts index 0b106fb..8b15f38 100644 --- a/src/routes/portal.ts +++ b/src/routes/portal.ts @@ -296,6 +296,14 @@ portalRouter.patch( const body = c.req.valid("json"); const clientId = c.get("portalClientId"); + // GRO-2203: validate UUID format before hitting Postgres. Passing a non-UUID + // string to a uuid column makes the driver throw ("invalid input syntax for + // type uuid"), which previously surfaced as an unhandled 500. Mirror the + // GRO-2014 fix in pets.ts and treat a malformed id as Not found. + if (!z.string().uuid().safeParse(petId).success) { + return c.json({ error: "Not found" }, 404); + } + const [pet] = await db .select() .from(pets)