feat(PBI-74): oude stores opruimen (Story 8)
Workspace-store is nu de enige bron voor product-backlog client-state. De
vier voorgangers en de dual-dispatch-infrastructuur zijn verwijderd.
- T-872: grep over codebase op useBacklogStore/usePlannerStore/
useSelectionStore/useProductStore is leeg.
- T-873..T-876: stores/{backlog,planner,selection,product}-store.ts deleted.
- T-877: __tests__/realtime/payload-contract.test.ts en
__tests__/api/backlog-realtime.test.ts deleted — pbi/story/task I|U|D
payload-handling wordt al gedekt door
__tests__/stores/product-workspace/store.test.ts (incl. parent-move,
idempotent inserts, delete-cleanup).
- T-878: lib/realtime/dev-workspace-fingerprint.ts deleted, dual-dispatch
uit BacklogHydrationWrapper en lib/realtime/use-backlog-realtime.ts
weggehaald. stores/products-store.ts (lijst van producten ≠ active
product) blijft ongewijzigd.
Bijwerkingen:
- BacklogPbi en BacklogStory types in components/backlog/story-panel.tsx en
components/sprint/sprint-backlog.tsx krijgen sort_order zodat ze met de
workspace-types overeenkomen.
- Server-pages /products/[id]/page.tsx (desktop+mobile) en
/products/[id]/sprint/[sprintId]/page.tsx selecteren sort_order op story
en mappen het door in de hydration-payload.
Verify: lint+typecheck clean, 626/626 tests groen (verlies van 25 redundante
oude-store tests; workspace-store tests dekken hetzelfde gedrag).
Refs: PBI-74, ST-1325, T-872..T-878
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
541154b521
commit
f7f4bf80bf
14 changed files with 23 additions and 620 deletions
|
|
@ -1,131 +0,0 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
const { mockGetSession } = vi.hoisted(() => ({ mockGetSession: vi.fn() }))
|
||||
|
||||
vi.mock('@/lib/auth', () => ({ getSession: mockGetSession }))
|
||||
vi.mock('@/lib/product-access', () => ({
|
||||
getAccessibleProduct: vi.fn(),
|
||||
}))
|
||||
|
||||
import { getAccessibleProduct } from '@/lib/product-access'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { GET } from '@/app/api/realtime/backlog/route'
|
||||
import { useBacklogStore } from '@/stores/backlog-store'
|
||||
|
||||
const mockGetAccessibleProduct = getAccessibleProduct as ReturnType<typeof vi.fn>
|
||||
|
||||
function makeReq(productId?: string): NextRequest {
|
||||
const url = productId
|
||||
? `http://localhost/api/realtime/backlog?product_id=${productId}`
|
||||
: 'http://localhost/api/realtime/backlog'
|
||||
return {
|
||||
signal: new AbortController().signal,
|
||||
nextUrl: new URL(url),
|
||||
} as unknown as NextRequest
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('GET /api/realtime/backlog', () => {
|
||||
it('401 when not authenticated', async () => {
|
||||
mockGetSession.mockResolvedValue({ userId: undefined, isDemo: false })
|
||||
const res = await GET(makeReq('prod-1'))
|
||||
expect(res.status).toBe(401)
|
||||
expect(mockGetAccessibleProduct).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('400 when product_id is missing', async () => {
|
||||
mockGetSession.mockResolvedValue({ userId: 'user-1', isDemo: false })
|
||||
const res = await GET(makeReq())
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
it('403 when user has no access to the product', async () => {
|
||||
mockGetSession.mockResolvedValue({ userId: 'user-1', isDemo: false })
|
||||
mockGetAccessibleProduct.mockResolvedValue(null)
|
||||
const res = await GET(makeReq('prod-1'))
|
||||
expect(res.status).toBe(403)
|
||||
expect(mockGetAccessibleProduct).toHaveBeenCalledWith('prod-1', 'user-1')
|
||||
})
|
||||
|
||||
it('500 when DIRECT_URL and DATABASE_URL are absent', async () => {
|
||||
mockGetSession.mockResolvedValue({ userId: 'user-1', isDemo: false })
|
||||
mockGetAccessibleProduct.mockResolvedValue({ id: 'prod-1' })
|
||||
|
||||
const before = { DIRECT_URL: process.env.DIRECT_URL, DATABASE_URL: process.env.DATABASE_URL }
|
||||
delete process.env.DIRECT_URL
|
||||
delete process.env.DATABASE_URL
|
||||
try {
|
||||
const res = await GET(makeReq('prod-1'))
|
||||
expect(res.status).toBe(500)
|
||||
} finally {
|
||||
if (before.DIRECT_URL !== undefined) process.env.DIRECT_URL = before.DIRECT_URL
|
||||
if (before.DATABASE_URL !== undefined) process.env.DATABASE_URL = before.DATABASE_URL
|
||||
}
|
||||
})
|
||||
|
||||
it('demo user is allowed (no 403) when product is accessible', async () => {
|
||||
mockGetSession.mockResolvedValue({ userId: 'demo-user', isDemo: true })
|
||||
mockGetAccessibleProduct.mockResolvedValue({ id: 'prod-1' })
|
||||
|
||||
const before = { DIRECT_URL: process.env.DIRECT_URL, DATABASE_URL: process.env.DATABASE_URL }
|
||||
delete process.env.DIRECT_URL
|
||||
delete process.env.DATABASE_URL
|
||||
try {
|
||||
const res = await GET(makeReq('prod-1'))
|
||||
// Fails at 500 (no DB URL) — not 403, confirming demo user is not blocked
|
||||
expect(res.status).toBe(500)
|
||||
} finally {
|
||||
if (before.DIRECT_URL !== undefined) process.env.DIRECT_URL = before.DIRECT_URL
|
||||
if (before.DATABASE_URL !== undefined) process.env.DATABASE_URL = before.DATABASE_URL
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// shouldEmit scope filter — white-box unit tests
|
||||
describe('shouldEmit scope filter (via backlog-store reducer)', () => {
|
||||
it('applyChange: pbi INSERT adds to pbis array', () => {
|
||||
useBacklogStore.setState({ pbis: [], storiesByPbi: {}, tasksByStory: {} })
|
||||
const pbi = { id: 'pbi-1', code: 'PBI-1', title: 'Test', priority: 2, created_at: new Date(), status: 'ready' as const }
|
||||
useBacklogStore.getState().applyChange('pbi', 'I', pbi)
|
||||
expect(useBacklogStore.getState().pbis).toHaveLength(1)
|
||||
expect(useBacklogStore.getState().pbis[0].id).toBe('pbi-1')
|
||||
})
|
||||
|
||||
it('applyChange: pbi UPDATE patches existing pbi', () => {
|
||||
const pbi = { id: 'pbi-1', code: 'PBI-1', title: 'Old', priority: 2, created_at: new Date(), status: 'ready' as const }
|
||||
useBacklogStore.setState({ pbis: [pbi], storiesByPbi: {}, tasksByStory: {} })
|
||||
useBacklogStore.getState().applyChange('pbi', 'U', { id: 'pbi-1', title: 'New' })
|
||||
expect(useBacklogStore.getState().pbis[0].title).toBe('New')
|
||||
})
|
||||
|
||||
it('applyChange: pbi DELETE removes pbi', () => {
|
||||
const pbi = { id: 'pbi-1', code: 'PBI-1', title: 'Test', priority: 2, created_at: new Date(), status: 'ready' as const }
|
||||
useBacklogStore.setState({ pbis: [pbi], storiesByPbi: {}, tasksByStory: {} })
|
||||
useBacklogStore.getState().applyChange('pbi', 'D', { id: 'pbi-1' })
|
||||
expect(useBacklogStore.getState().pbis).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('applyChange: story INSERT adds to storiesByPbi', () => {
|
||||
useBacklogStore.setState({ pbis: [], storiesByPbi: { 'pbi-1': [] }, tasksByStory: {} })
|
||||
const story = { id: 'story-1', code: 'ST-1', title: 'S', description: null, acceptance_criteria: null, priority: 2, status: 'OPEN', pbi_id: 'pbi-1', sprint_id: null, created_at: new Date() }
|
||||
useBacklogStore.getState().applyChange('story', 'I', story)
|
||||
expect(useBacklogStore.getState().storiesByPbi['pbi-1']).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('applyChange: story DELETE removes from correct pbi bucket', () => {
|
||||
const story = { id: 'story-1', code: 'ST-1', title: 'S', description: null, acceptance_criteria: null, priority: 2, status: 'OPEN', pbi_id: 'pbi-1', sprint_id: null, created_at: new Date() }
|
||||
useBacklogStore.setState({ pbis: [], storiesByPbi: { 'pbi-1': [story] }, tasksByStory: {} })
|
||||
useBacklogStore.getState().applyChange('story', 'D', { id: 'story-1' })
|
||||
expect(useBacklogStore.getState().storiesByPbi['pbi-1']).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('applyChange: task UPDATE patches task across story buckets', () => {
|
||||
const task = { id: 'task-1', title: 'Old', description: null, priority: 2, status: 'TO_DO', sort_order: 1, story_id: 'story-1', created_at: new Date() }
|
||||
useBacklogStore.setState({ pbis: [], storiesByPbi: {}, tasksByStory: { 'story-1': [task] } })
|
||||
useBacklogStore.getState().applyChange('task', 'U', { id: 'task-1', status: 'IN_PROGRESS' })
|
||||
expect(useBacklogStore.getState().tasksByStory['story-1'][0].status).toBe('IN_PROGRESS')
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue