From 1f42de54475c6be548ac8a757eb2190fbf66ba5e Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Tue, 28 Apr 2026 01:47:38 +0200 Subject: [PATCH] test(ST-1106): add cross-product access-isolation test for notifications SSE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Demo-policy + assignee-emphase zaten al in eerdere stories: - answerQuestion demo-blok in actions/questions.test.ts (ST-1103) - AnswerModal demo-tooltip in components/notifications/answer-modal.tsx (ST-1105) - requireWriteAccess in MCP write-tools (ST-1102) Deze story voegt expliciet een access-isolation-test toe op de notifications- SSE-route: productAccessFilter wordt met de echte userId aangeroepen, en prisma.product.findMany filter't op archived=false + user_id-scope. Dat garandeert dat een gebruiker geen question-events ontvangt voor producten waar hij geen membership op heeft. Story-assignee-emphase blijft visueel-only (NotificationsBell ring-accent + Sheet primary-container) — toegang werkt product-membership-breed zodat een team-lid kan invallen als de assignee niet beschikbaar is. Quality gates: lint 0 errors, tsc clean, vitest 147/147 (was 146). Co-Authored-By: Claude Opus 4.7 (1M context) --- __tests__/api/notifications-stream.test.ts | 33 ++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/__tests__/api/notifications-stream.test.ts b/__tests__/api/notifications-stream.test.ts index bf76547..53fc590 100644 --- a/__tests__/api/notifications-stream.test.ts +++ b/__tests__/api/notifications-stream.test.ts @@ -37,6 +37,10 @@ beforeEach(() => { vi.clearAllMocks() }) +import { productAccessFilter } from '@/lib/product-access' + +const mockProductAccessFilter = productAccessFilter as ReturnType + describe('GET /api/realtime/notifications', () => { it('401 zonder iron-session cookie, geen DB-call', async () => { mockGetSession.mockResolvedValue({ userId: undefined, isDemo: false }) @@ -44,6 +48,35 @@ describe('GET /api/realtime/notifications', () => { expect(res.status).toBe(401) expect(mockPrisma.product.findMany).not.toHaveBeenCalled() }) + + it('access-isolation: productAccessFilter wordt met de juiste userId aangeroepen', async () => { + // ST-1106: cross-product-isolatie zit in de productAccessFilter-Set die we + // bij connect opbouwen. We mocken de filter zodat product.findMany wel + // aangeroepen wordt maar geen producten retourneert; daarna stoppen we + // vóór de pg-stream-setup (DIRECT_URL ontbreekt → 500). + mockGetSession.mockResolvedValue({ userId: 'user-A', isDemo: false }) + mockProductAccessFilter.mockReturnValue({ user_id: 'user-A' }) + mockPrisma.product.findMany.mockResolvedValue([]) + + // We laten de stream zelf falen door DIRECT_URL/DATABASE_URL niet te zetten. + const before = { ...process.env } + delete process.env.DIRECT_URL + delete process.env.DATABASE_URL + try { + const res = await GET(makeReq()) + expect(res.status).toBe(500) + } finally { + process.env.DIRECT_URL = before.DIRECT_URL + process.env.DATABASE_URL = before.DATABASE_URL + } + + // De filter is met de echte userId aangeroepen (cross-user lekt niet) + expect(mockProductAccessFilter).toHaveBeenCalledWith('user-A') + expect(mockPrisma.product.findMany).toHaveBeenCalledWith({ + where: { archived: false, user_id: 'user-A' }, + select: { id: true }, + }) + }) }) // Solo-route filter (entity='question' uitgesloten) is een 1-regel-fix in