diff --git a/apps/api/src/routes/admin.ts b/apps/api/src/routes/admin.ts index 333b5c2..c68f354 100644 --- a/apps/api/src/routes/admin.ts +++ b/apps/api/src/routes/admin.ts @@ -153,11 +153,28 @@ adminRouter.put('/calendars/:id/shared', async (c) => { return c.json({ error: 'Invalid calendar id' }, 400); } - // Step 1: Clear is_shared on any currently-shared calendar - await db.update(calendars).set({ isShared: false }).where(eq(calendars.isShared, true)); + // CR-01: verify the target exists and flip the shared lane atomically. + // Without the existence check + transaction, a bad/stale id would clear the + // current shared calendar in step 1 and update 0 rows in step 2 — silently + // leaving the household with NO shared calendar while still returning ok. + const found = await db.transaction(async (tx) => { + const [target] = await tx + .select({ id: calendars.id }) + .from(calendars) + .where(eq(calendars.id, targetId)) + .limit(1); + if (!target) return false; - // Step 2: Set is_shared on the target calendar - await db.update(calendars).set({ isShared: true }).where(eq(calendars.id, targetId)); + // Step 1: Clear is_shared on any currently-shared calendar + await tx.update(calendars).set({ isShared: false }).where(eq(calendars.isShared, true)); + // Step 2: Set is_shared on the target calendar (D-06 single-select) + await tx.update(calendars).set({ isShared: true }).where(eq(calendars.id, targetId)); + return true; + }); + + if (!found) { + return c.json({ error: 'Calendar not found' }, 404); + } return c.json({ ok: true }, 200); }); diff --git a/apps/api/tests/routes/admin.test.ts b/apps/api/tests/routes/admin.test.ts index f081268..81d5248 100644 --- a/apps/api/tests/routes/admin.test.ts +++ b/apps/api/tests/routes/admin.test.ts @@ -456,6 +456,27 @@ describe('PUT /api/admin/calendars/:id/shared', () => { expect(sharedIds).toContain(calB); expect(sharedIds).not.toContain(calA); }); + + it('returns 404 for a non-existent target and does NOT clear the existing shared calendar (CR-01)', async () => { + const adminId = await seedUser('admin-shared-missing', true); + const calA = await seedCalendar(adminId, 'cal-a-keep', true); // currently the shared family lane + currentDevUserId = adminId; + const app = await getApp(); + + // PUT a target id that does not exist. The handler must verify the target + // exists BEFORE clearing the current shared lane, so a bad/stale id can + // never silently wipe the family's shared calendar (BLOCKER CR-01). + const res = await app.fetch(jsonRequest('PUT', '/api/admin/calendars/99999999/shared')); + expect(res.status).toBe(404); + + // calA must STILL be shared — the no-op target must not have cleared it. + const [rowA] = await db + .select({ isShared: calendars.isShared }) + .from(calendars) + .where(eq(calendars.id, calA)) + .limit(1); + expect(rowA.isShared).toBe(true); + }); }); // ===========================================================================