From 931f7679222e469b1f1f8cb2ac92524a2ae1a83f Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Tue, 9 Jun 2026 14:22:21 -0400 Subject: [PATCH] test(04-07): add failing sharee-403 tests for T-04-08 owner-only isShared guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - T-04-08 test 1: sharee PATCH { isShared: false } must get 403 and list_shares row unchanged (currently 200 + shares wiped — bug) - T-04-08 test 2: sharee PATCH { isShared: true } must get 403 and no new shares inserted (currently 200 + shares fan-out — bug) - Both tests fail now; GREEN once owner-only guard added to lists.ts --- apps/api/tests/routes/lists.test.ts | 50 +++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/apps/api/tests/routes/lists.test.ts b/apps/api/tests/routes/lists.test.ts index 17c1097..68ee40a 100644 --- a/apps/api/tests/routes/lists.test.ts +++ b/apps/api/tests/routes/lists.test.ts @@ -448,6 +448,56 @@ describe('PATCH /api/lists/:id (authorized update)', () => { const body = await res.json() as { name: string } expect(body.name).toBe('Sharee Renamed') }) + + it('T-04-08: sharee sending { isShared: false } gets 403 and list_shares is unchanged', async () => { + const ownerId = await seedUser('t04-08-owner') + const shareeId = await seedUser('t04-08-sharee') + const listId = await seedList(ownerId, 'T04-08 Shared List', true) + await shareList(listId, shareeId) + + // Act as sharee + currentDevUserId = shareeId + const app = await getApp() + + // Sharee tries to set isShared: false — must be blocked + const res = await app.request(jsonRequest('PATCH', `/api/lists/${listId}`, { isShared: false })) + expect(res.status).toBe(403) + const body = await res.json() as { error: string } + // Response should mention owner/sharing + expect(body.error).toMatch(/owner|shar/i) + + // Prove list_shares is untouched — sharee row still exists + const { and, eq } = await import('drizzle-orm') + const shares = await db.select().from(listShares).where( + and(eq(listShares.listId, listId), eq(listShares.userId, shareeId)), + ) + expect(shares.length).toBe(1) + }) + + it('T-04-08: sharee sending { isShared: true } gets 403 and no new shares are inserted', async () => { + const ownerId = await seedUser('t04-08-true-owner') + const shareeId = await seedUser('t04-08-true-sharee') + // Private list (isShared: false) but sharee already has explicit access + const listId = await seedList(ownerId, 'T04-08 Private List', false) + await shareList(listId, shareeId) + + // Act as sharee + currentDevUserId = shareeId + const app = await getApp() + + // Record share count before the attempted mutation + const { eq } = await import('drizzle-orm') + const beforeShares = await db.select().from(listShares).where(eq(listShares.listId, listId)) + const beforeCount = beforeShares.length + + // Sharee tries to set isShared: true — must be blocked + const res = await app.request(jsonRequest('PATCH', `/api/lists/${listId}`, { isShared: true })) + expect(res.status).toBe(403) + + // Prove no new shares were inserted + const afterShares = await db.select().from(listShares).where(eq(listShares.listId, listId)) + expect(afterShares.length).toBe(beforeCount) + }) }) // ---------------------------------------------------------------------------