test(security): extend security.test.ts to cover all 7 API endpoints
Adds 401, 403 demo-block, and cross-user isolation tests for: GET /api/products/:id/next-story, GET /api/sprints/:id/tasks, PATCH /api/stories/:id/tasks/reorder, POST /api/stories/:id/log, POST /api/todos. Expands prisma mock to cover all required models. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5070dd4133
commit
ead91cef5f
1 changed files with 401 additions and 120 deletions
|
|
@ -1,22 +1,31 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
// Mock prisma
|
||||
vi.mock('@/lib/prisma', () => ({
|
||||
prisma: {
|
||||
product: {
|
||||
findMany: vi.fn(),
|
||||
findFirst: vi.fn(),
|
||||
},
|
||||
sprint: {
|
||||
findFirst: vi.fn(),
|
||||
},
|
||||
story: {
|
||||
findFirst: vi.fn(),
|
||||
},
|
||||
task: {
|
||||
findFirst: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
apiToken: {
|
||||
findUnique: vi.fn(),
|
||||
storyLog: {
|
||||
create: vi.fn(),
|
||||
},
|
||||
todo: {
|
||||
create: vi.fn(),
|
||||
},
|
||||
$transaction: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock api-auth to control which user is "authenticated"
|
||||
vi.mock('@/lib/api-auth', () => ({
|
||||
authenticateApiRequest: vi.fn(),
|
||||
}))
|
||||
|
|
@ -24,136 +33,408 @@ vi.mock('@/lib/api-auth', () => ({
|
|||
import { prisma } from '@/lib/prisma'
|
||||
import { authenticateApiRequest } from '@/lib/api-auth'
|
||||
import { GET as getProducts } from '@/app/api/products/route'
|
||||
import { GET as getNextStory } from '@/app/api/products/[id]/next-story/route'
|
||||
import { GET as getSprintTasks } from '@/app/api/sprints/[id]/tasks/route'
|
||||
import { PATCH as patchReorder } from '@/app/api/stories/[id]/tasks/reorder/route'
|
||||
import { POST as postStoryLog } from '@/app/api/stories/[id]/log/route'
|
||||
import { PATCH as patchTask } from '@/app/api/tasks/[id]/route'
|
||||
import { POST as postTodo } from '@/app/api/todos/route'
|
||||
|
||||
const mockPrisma = prisma as unknown as {
|
||||
product: { findMany: ReturnType<typeof vi.fn> }
|
||||
product: { findMany: ReturnType<typeof vi.fn>; findFirst: ReturnType<typeof vi.fn> }
|
||||
sprint: { findFirst: ReturnType<typeof vi.fn> }
|
||||
story: { findFirst: ReturnType<typeof vi.fn> }
|
||||
task: { findFirst: ReturnType<typeof vi.fn>; update: ReturnType<typeof vi.fn> }
|
||||
storyLog: { create: ReturnType<typeof vi.fn> }
|
||||
todo: { create: ReturnType<typeof vi.fn> }
|
||||
$transaction: ReturnType<typeof vi.fn>
|
||||
}
|
||||
const mockAuth = authenticateApiRequest as ReturnType<typeof vi.fn>
|
||||
|
||||
function makeRequest(method = 'GET', body?: unknown): Request {
|
||||
return new Request('http://localhost/api/test', {
|
||||
method,
|
||||
headers: { 'Authorization': 'Bearer test-token', 'Content-Type': 'application/json' },
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
const UNAUTHORIZED = { error: 'Unauthorized', status: 401 }
|
||||
const DEMO_AUTH = { userId: 'demo-user', isDemo: true }
|
||||
const USER_1_AUTH = { userId: 'user-1', isDemo: false }
|
||||
const USER_2_AUTH = { userId: 'user-2', isDemo: false }
|
||||
|
||||
function makeGet(url: string): Request {
|
||||
return new Request(url, {
|
||||
method: 'GET',
|
||||
headers: { Authorization: 'Bearer test-token' },
|
||||
})
|
||||
}
|
||||
|
||||
describe('Security: cross-user access', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
function makePost(url: string, body: unknown): Request {
|
||||
return new Request(url, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: 'Bearer test-token', 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
}
|
||||
|
||||
function makePatch(url: string, body: unknown): Request {
|
||||
return new Request(url, {
|
||||
method: 'PATCH',
|
||||
headers: { Authorization: 'Bearer test-token', 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
}
|
||||
|
||||
function routeCtx(id: string) {
|
||||
return { params: Promise.resolve({ id }) }
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// ─── GET /api/products ────────────────────────────────────────────────────────
|
||||
|
||||
describe('GET /api/products', () => {
|
||||
// TC-P-01
|
||||
it('returns 401 when no valid token provided', async () => {
|
||||
mockAuth.mockResolvedValue(UNAUTHORIZED)
|
||||
const res = await getProducts(makeGet('http://localhost/api/products'))
|
||||
expect(res.status).toBe(401)
|
||||
})
|
||||
|
||||
describe('GET /api/products', () => {
|
||||
it('returns only the authenticated user\'s products', async () => {
|
||||
mockAuth.mockResolvedValue({ userId: 'user-1', isDemo: false })
|
||||
mockPrisma.product.findMany.mockResolvedValue([
|
||||
{ id: 'prod-1', name: 'Product A', repo_url: null },
|
||||
])
|
||||
// TC-P-08
|
||||
it('returns only the authenticated user\'s products (cross-user isolation)', async () => {
|
||||
mockAuth.mockResolvedValue(USER_1_AUTH)
|
||||
mockPrisma.product.findMany.mockResolvedValue([{ id: 'prod-1', name: 'Product A', repo_url: null }])
|
||||
|
||||
const response = await getProducts(makeRequest())
|
||||
const data = await response.json()
|
||||
const res = await getProducts(makeGet('http://localhost/api/products'))
|
||||
const data = await res.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data).toHaveLength(1)
|
||||
// Verify the query includes owned products and products shared through membership.
|
||||
expect(mockPrisma.product.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
archived: false,
|
||||
OR: expect.arrayContaining([
|
||||
{ user_id: 'user-1' },
|
||||
{ members: { some: { user_id: 'user-1' } } },
|
||||
]),
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('returns 401 when no valid token provided', async () => {
|
||||
mockAuth.mockResolvedValue({ error: 'Unauthorized', status: 401 })
|
||||
|
||||
const response = await getProducts(makeRequest())
|
||||
expect(response.status).toBe(401)
|
||||
})
|
||||
})
|
||||
|
||||
describe('PATCH /api/tasks/:id', () => {
|
||||
it('returns 403 when task belongs to a different user', async () => {
|
||||
// User 2 is authenticated but the task belongs to user 1
|
||||
mockAuth.mockResolvedValue({ userId: 'user-2', isDemo: false })
|
||||
mockPrisma.task.findFirst.mockResolvedValue({
|
||||
id: 'task-1',
|
||||
story: {
|
||||
product: {
|
||||
user_id: 'user-1', // different user!
|
||||
},
|
||||
},
|
||||
expect(res.status).toBe(200)
|
||||
expect(mockPrisma.product.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
archived: false,
|
||||
OR: expect.arrayContaining([
|
||||
{ user_id: 'user-1' },
|
||||
{ members: { some: { user_id: 'user-1' } } },
|
||||
]),
|
||||
}),
|
||||
})
|
||||
|
||||
const response = await patchTask(
|
||||
makeRequest('PATCH', { status: 'DONE' }),
|
||||
{ params: Promise.resolve({ id: 'task-1' }) }
|
||||
)
|
||||
|
||||
expect(response.status).toBe(403)
|
||||
const data = await response.json()
|
||||
expect(data.error).toBeTruthy()
|
||||
})
|
||||
|
||||
it('returns 403 for demo users', async () => {
|
||||
mockAuth.mockResolvedValue({ userId: 'demo-user', isDemo: true })
|
||||
|
||||
const response = await patchTask(
|
||||
makeRequest('PATCH', { status: 'DONE' }),
|
||||
{ params: Promise.resolve({ id: 'task-1' }) }
|
||||
)
|
||||
|
||||
expect(response.status).toBe(403)
|
||||
})
|
||||
|
||||
it('allows update when task belongs to the authenticated user', async () => {
|
||||
mockAuth.mockResolvedValue({ userId: 'user-1', isDemo: false })
|
||||
mockPrisma.task.findFirst.mockResolvedValue({
|
||||
id: 'task-1',
|
||||
story: {
|
||||
product: {
|
||||
user_id: 'user-1', // same user
|
||||
},
|
||||
},
|
||||
})
|
||||
mockPrisma.task.update.mockResolvedValue({ id: 'task-1', status: 'DONE' })
|
||||
|
||||
const response = await patchTask(
|
||||
makeRequest('PATCH', { status: 'DONE' }),
|
||||
{ params: Promise.resolve({ id: 'task-1' }) }
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
})
|
||||
|
||||
it('returns 404 when task does not exist', async () => {
|
||||
mockAuth.mockResolvedValue({ userId: 'user-1', isDemo: false })
|
||||
mockPrisma.task.findFirst.mockResolvedValue(null)
|
||||
|
||||
const response = await patchTask(
|
||||
makeRequest('PATCH', { status: 'DONE' }),
|
||||
{ params: Promise.resolve({ id: 'nonexistent' }) }
|
||||
)
|
||||
|
||||
expect(response.status).toBe(404)
|
||||
})
|
||||
|
||||
it('returns 401 when no valid token', async () => {
|
||||
mockAuth.mockResolvedValue({ error: 'Unauthorized', status: 401 })
|
||||
|
||||
const response = await patchTask(
|
||||
makeRequest('PATCH', { status: 'DONE' }),
|
||||
{ params: Promise.resolve({ id: 'task-1' }) }
|
||||
)
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
})
|
||||
)
|
||||
expect(data).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
// ─── GET /api/products/:id/next-story ────────────────────────────────────────
|
||||
|
||||
describe('GET /api/products/:id/next-story', () => {
|
||||
// TC-NS-01
|
||||
it('returns 401 when no valid token provided', async () => {
|
||||
mockAuth.mockResolvedValue(UNAUTHORIZED)
|
||||
const res = await getNextStory(
|
||||
makeGet('http://localhost/api/products/prod-1/next-story'),
|
||||
routeCtx('prod-1')
|
||||
)
|
||||
expect(res.status).toBe(401)
|
||||
})
|
||||
|
||||
// TC-NS-03 / TC-NS-07: product not accessible covers both "not found" and cross-user
|
||||
it('returns 404 when product is not accessible to the authenticated user', async () => {
|
||||
mockAuth.mockResolvedValue(USER_1_AUTH)
|
||||
mockPrisma.sprint.findFirst.mockResolvedValue(null)
|
||||
|
||||
const res = await getNextStory(
|
||||
makeGet('http://localhost/api/products/prod-other/next-story'),
|
||||
routeCtx('prod-other')
|
||||
)
|
||||
expect(res.status).toBe(404)
|
||||
expect(mockPrisma.sprint.findFirst).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
product_id: 'prod-other',
|
||||
status: 'ACTIVE',
|
||||
product: expect.objectContaining({
|
||||
OR: expect.arrayContaining([{ user_id: 'user-1' }]),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
// TC-NS-07 explicit cross-user
|
||||
it('returns 404 for another user\'s product', async () => {
|
||||
mockAuth.mockResolvedValue(USER_2_AUTH)
|
||||
mockPrisma.sprint.findFirst.mockResolvedValue(null)
|
||||
|
||||
const res = await getNextStory(
|
||||
makeGet('http://localhost/api/products/prod-1/next-story'),
|
||||
routeCtx('prod-1')
|
||||
)
|
||||
expect(res.status).toBe(404)
|
||||
expect(mockPrisma.sprint.findFirst).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
product: expect.objectContaining({
|
||||
OR: expect.arrayContaining([{ user_id: 'user-2' }]),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// ─── GET /api/sprints/:id/tasks ───────────────────────────────────────────────
|
||||
|
||||
describe('GET /api/sprints/:id/tasks', () => {
|
||||
// TC-ST-01
|
||||
it('returns 401 when no valid token provided', async () => {
|
||||
mockAuth.mockResolvedValue(UNAUTHORIZED)
|
||||
const res = await getSprintTasks(
|
||||
makeGet('http://localhost/api/sprints/sprint-1/tasks'),
|
||||
routeCtx('sprint-1')
|
||||
)
|
||||
expect(res.status).toBe(401)
|
||||
})
|
||||
|
||||
// TC-ST-03
|
||||
it('returns 404 when sprint is not found', async () => {
|
||||
mockAuth.mockResolvedValue(USER_1_AUTH)
|
||||
mockPrisma.sprint.findFirst.mockResolvedValue(null)
|
||||
|
||||
const res = await getSprintTasks(
|
||||
makeGet('http://localhost/api/sprints/nonexistent/tasks'),
|
||||
routeCtx('nonexistent')
|
||||
)
|
||||
expect(res.status).toBe(404)
|
||||
})
|
||||
|
||||
// TC-ST-04
|
||||
it('returns 404 for another user\'s sprint', async () => {
|
||||
mockAuth.mockResolvedValue(USER_2_AUTH)
|
||||
mockPrisma.sprint.findFirst.mockResolvedValue(null)
|
||||
|
||||
const res = await getSprintTasks(
|
||||
makeGet('http://localhost/api/sprints/sprint-1/tasks'),
|
||||
routeCtx('sprint-1')
|
||||
)
|
||||
expect(res.status).toBe(404)
|
||||
expect(mockPrisma.sprint.findFirst).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
id: 'sprint-1',
|
||||
product: expect.objectContaining({
|
||||
OR: expect.arrayContaining([{ user_id: 'user-2' }]),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// ─── PATCH /api/stories/:id/tasks/reorder ────────────────────────────────────
|
||||
|
||||
describe('PATCH /api/stories/:id/tasks/reorder', () => {
|
||||
const VALID_BODY = { task_ids: ['task-x'] }
|
||||
|
||||
// TC-RO-01
|
||||
it('returns 401 when no valid token provided', async () => {
|
||||
mockAuth.mockResolvedValue(UNAUTHORIZED)
|
||||
const res = await patchReorder(
|
||||
makePatch('http://localhost/api/stories/story-1/tasks/reorder', VALID_BODY),
|
||||
routeCtx('story-1')
|
||||
)
|
||||
expect(res.status).toBe(401)
|
||||
})
|
||||
|
||||
// TC-RO-03
|
||||
it('returns 403 for demo users', async () => {
|
||||
mockAuth.mockResolvedValue(DEMO_AUTH)
|
||||
const res = await patchReorder(
|
||||
makePatch('http://localhost/api/stories/story-1/tasks/reorder', VALID_BODY),
|
||||
routeCtx('story-1')
|
||||
)
|
||||
expect(res.status).toBe(403)
|
||||
const data = await res.json()
|
||||
expect(data.error).toBe('Niet beschikbaar in demo-modus')
|
||||
})
|
||||
|
||||
// TC-RO-04 / TC-RO-05
|
||||
it('returns 404 when story is not accessible to the authenticated user', async () => {
|
||||
mockAuth.mockResolvedValue(USER_2_AUTH)
|
||||
mockPrisma.story.findFirst.mockResolvedValue(null)
|
||||
|
||||
const res = await patchReorder(
|
||||
makePatch('http://localhost/api/stories/story-1/tasks/reorder', VALID_BODY),
|
||||
routeCtx('story-1')
|
||||
)
|
||||
expect(res.status).toBe(404)
|
||||
expect(mockPrisma.story.findFirst).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
id: 'story-1',
|
||||
product: expect.objectContaining({
|
||||
OR: expect.arrayContaining([{ user_id: 'user-2' }]),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// ─── POST /api/stories/:id/log ────────────────────────────────────────────────
|
||||
|
||||
describe('POST /api/stories/:id/log', () => {
|
||||
const VALID_BODY = { type: 'IMPLEMENTATION_PLAN', content: 'Plan: step 1' }
|
||||
|
||||
// TC-L-01
|
||||
it('returns 401 when no valid token provided', async () => {
|
||||
mockAuth.mockResolvedValue(UNAUTHORIZED)
|
||||
const res = await postStoryLog(
|
||||
makePost('http://localhost/api/stories/story-1/log', VALID_BODY),
|
||||
routeCtx('story-1')
|
||||
)
|
||||
expect(res.status).toBe(401)
|
||||
})
|
||||
|
||||
// TC-L-03
|
||||
it('returns 403 for demo users', async () => {
|
||||
mockAuth.mockResolvedValue(DEMO_AUTH)
|
||||
const res = await postStoryLog(
|
||||
makePost('http://localhost/api/stories/story-1/log', VALID_BODY),
|
||||
routeCtx('story-1')
|
||||
)
|
||||
expect(res.status).toBe(403)
|
||||
const data = await res.json()
|
||||
expect(data.error).toBe('Niet beschikbaar in demo-modus')
|
||||
})
|
||||
|
||||
// TC-L-04 / TC-L-05
|
||||
it('returns 404 when story is not accessible to the authenticated user', async () => {
|
||||
mockAuth.mockResolvedValue(USER_2_AUTH)
|
||||
mockPrisma.story.findFirst.mockResolvedValue(null)
|
||||
|
||||
const res = await postStoryLog(
|
||||
makePost('http://localhost/api/stories/story-1/log', VALID_BODY),
|
||||
routeCtx('story-1')
|
||||
)
|
||||
expect(res.status).toBe(404)
|
||||
expect(mockPrisma.story.findFirst).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
id: 'story-1',
|
||||
product: expect.objectContaining({
|
||||
OR: expect.arrayContaining([{ user_id: 'user-2' }]),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// ─── PATCH /api/tasks/:id ─────────────────────────────────────────────────────
|
||||
|
||||
describe('PATCH /api/tasks/:id', () => {
|
||||
// TC-T-01
|
||||
it('returns 401 when no valid token provided', async () => {
|
||||
mockAuth.mockResolvedValue(UNAUTHORIZED)
|
||||
const res = await patchTask(
|
||||
makePatch('http://localhost/api/tasks/task-1', { status: 'DONE' }),
|
||||
routeCtx('task-1')
|
||||
)
|
||||
expect(res.status).toBe(401)
|
||||
})
|
||||
|
||||
// TC-T-03
|
||||
it('returns 403 for demo users', async () => {
|
||||
mockAuth.mockResolvedValue(DEMO_AUTH)
|
||||
const res = await patchTask(
|
||||
makePatch('http://localhost/api/tasks/task-1', { status: 'DONE' }),
|
||||
routeCtx('task-1')
|
||||
)
|
||||
expect(res.status).toBe(403)
|
||||
const data = await res.json()
|
||||
expect(data.error).toBeTruthy()
|
||||
})
|
||||
|
||||
// TC-T-04
|
||||
it('returns 404 when task does not exist', async () => {
|
||||
mockAuth.mockResolvedValue(USER_1_AUTH)
|
||||
mockPrisma.task.findFirst.mockResolvedValue(null)
|
||||
|
||||
const res = await patchTask(
|
||||
makePatch('http://localhost/api/tasks/nonexistent', { status: 'DONE' }),
|
||||
routeCtx('nonexistent')
|
||||
)
|
||||
expect(res.status).toBe(404)
|
||||
})
|
||||
|
||||
// TC-T-05
|
||||
it('returns 403 when task belongs to a different user', async () => {
|
||||
mockAuth.mockResolvedValue(USER_2_AUTH)
|
||||
mockPrisma.task.findFirst.mockResolvedValue({
|
||||
id: 'task-1',
|
||||
story: { product: { user_id: 'user-1' } },
|
||||
})
|
||||
|
||||
const res = await patchTask(
|
||||
makePatch('http://localhost/api/tasks/task-1', { status: 'DONE' }),
|
||||
routeCtx('task-1')
|
||||
)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
// TC-T-08 (happy path, sanity check)
|
||||
it('returns 200 when task belongs to the authenticated user', async () => {
|
||||
mockAuth.mockResolvedValue(USER_1_AUTH)
|
||||
mockPrisma.task.findFirst.mockResolvedValue({
|
||||
id: 'task-1',
|
||||
story: { product: { user_id: 'user-1' } },
|
||||
})
|
||||
mockPrisma.task.update.mockResolvedValue({ id: 'task-1', status: 'DONE' })
|
||||
|
||||
const res = await patchTask(
|
||||
makePatch('http://localhost/api/tasks/task-1', { status: 'DONE' }),
|
||||
routeCtx('task-1')
|
||||
)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
})
|
||||
|
||||
// ─── POST /api/todos ──────────────────────────────────────────────────────────
|
||||
|
||||
describe('POST /api/todos', () => {
|
||||
// product_id is required by the Zod schema (z.string().min(1))
|
||||
const VALID_BODY = { title: 'Test todo', product_id: 'prod-1' }
|
||||
|
||||
// TC-TD-01
|
||||
it('returns 401 when no valid token provided', async () => {
|
||||
mockAuth.mockResolvedValue(UNAUTHORIZED)
|
||||
const res = await postTodo(makePost('http://localhost/api/todos', VALID_BODY))
|
||||
expect(res.status).toBe(401)
|
||||
})
|
||||
|
||||
// TC-TD-03
|
||||
it('returns 403 for demo users', async () => {
|
||||
mockAuth.mockResolvedValue(DEMO_AUTH)
|
||||
const res = await postTodo(makePost('http://localhost/api/todos', VALID_BODY))
|
||||
expect(res.status).toBe(403)
|
||||
const data = await res.json()
|
||||
expect(data.error).toBe('Niet beschikbaar in demo-modus')
|
||||
})
|
||||
|
||||
// TC-TD-08
|
||||
it('returns 404 when product_id belongs to another user', async () => {
|
||||
mockAuth.mockResolvedValue(USER_2_AUTH)
|
||||
mockPrisma.product.findFirst.mockResolvedValue(null)
|
||||
|
||||
const res = await postTodo(
|
||||
makePost('http://localhost/api/todos', { title: 'Todo', product_id: 'prod-owned-by-user-1' })
|
||||
)
|
||||
expect(res.status).toBe(404)
|
||||
// Verify it queries by user_id, not productAccessFilter
|
||||
expect(mockPrisma.product.findFirst).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
id: 'prod-owned-by-user-1',
|
||||
user_id: 'user-2',
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue