Idea-jobs and idea-questions are user-private (M12 grill-keuze 8) — they flow through /api/realtime/notifications, not /api/realtime/solo. app/api/realtime/notifications/route.ts: - Pre-fetch user's idea-ids → accessibleIdeaIds Set (avoids per-event DB lookup) - New IdeaJobPayload type (claude_job_enqueued/_status with kind=IDEA_*) - New QuestionPayload narrows: story_id and idea_id mutually exclusive (DB check-constraint enforces it) - Routing: idea-jobs filtered on user_id; idea-questions on accessibleIdeaIds; story-questions on accessibleProductIds (existing path) app/api/realtime/solo/route.ts: - JobPayload extended with optional kind + idea_id - shouldEmit filters out kind=IDEA_GRILL/IDEA_MAKE_PLAN — they don't belong on the product/sprint Solo Paneel Tests: 539/539 green; notifications-stream test mock updated for idea.findMany. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
85 lines
3 KiB
TypeScript
85 lines
3 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
const { mockGetSession } = vi.hoisted(() => ({ mockGetSession: vi.fn() }))
|
|
|
|
vi.mock('@/lib/auth', () => ({
|
|
getSession: mockGetSession,
|
|
}))
|
|
|
|
vi.mock('@/lib/prisma', () => ({
|
|
prisma: {
|
|
product: { findMany: vi.fn() },
|
|
claudeQuestion: { findMany: vi.fn() },
|
|
idea: { findMany: vi.fn().mockResolvedValue([]) },
|
|
},
|
|
}))
|
|
|
|
vi.mock('@/lib/product-access', () => ({
|
|
productAccessFilter: vi.fn().mockReturnValue({}),
|
|
getAccessibleProduct: vi.fn(),
|
|
}))
|
|
|
|
import { prisma } from '@/lib/prisma'
|
|
import type { NextRequest } from 'next/server'
|
|
import { GET } from '@/app/api/realtime/notifications/route'
|
|
|
|
const mockPrisma = prisma as unknown as {
|
|
product: { findMany: ReturnType<typeof vi.fn> }
|
|
claudeQuestion: { findMany: ReturnType<typeof vi.fn> }
|
|
}
|
|
|
|
function makeReq(): NextRequest {
|
|
// Minimaal NextRequest-shape voor de auth-pad — we komen niet bij de
|
|
// pg-stream-setup omdat de auth-fail vóór dat punt gebeurt.
|
|
return { signal: new AbortController().signal } as unknown as NextRequest
|
|
}
|
|
|
|
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 })
|
|
const res = await GET(makeReq())
|
|
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
|
|
// app/api/realtime/solo/route.ts. Visueel reviewbaar in de diff; full-stream-
|
|
// regressie wordt handmatig gedekt in ST-1108-acceptatie.
|