test(ST-1106): add cross-product access-isolation test for notifications SSE
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) <noreply@anthropic.com>
This commit is contained in:
parent
3243282bfd
commit
1f42de5447
1 changed files with 33 additions and 0 deletions
|
|
@ -37,6 +37,10 @@ beforeEach(() => {
|
|||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
import { productAccessFilter } from '@/lib/product-access'
|
||||
|
||||
const mockProductAccessFilter = productAccessFilter as ReturnType<typeof vi.fn>
|
||||
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue