/** @jest-environment node */ import { getServerSession } from 'next-auth' import { prisma } from '@/lib/prisma' import { GET } from '@/app/api/materials/facets/route' import { buildMaterialWhereFromSearchParams, createMaterialQueryContext, } from '@/lib/material-query' jest.mock('next-auth', () => ({ getServerSession: jest.fn() })) jest.mock('@/lib/auth', () => ({ authOptions: {} })) jest.mock('@/lib/prisma', () => ({ prisma: { material: { count: jest.fn(), findMany: jest.fn(), groupBy: jest.fn(), }, }, })) jest.mock('@/lib/material-query', () => { class MaterialQueryError extends Error { status: number code: string constructor(status: number, code: string, message: string) { super(message) this.status = status this.code = code } } return { buildMaterialWhereFromSearchParams: jest.fn(), createMaterialQueryContext: jest.fn(), MaterialQueryError, normalizeMaterialTypeForUi: (value: string) => value, normalizeSelectableMaterialTypes: (values: string[]) => values, } }) describe('materials facets route', () => { beforeEach(() => { jest.clearAllMocks() ;(getServerSession as jest.Mock).mockResolvedValue({ user: { id: 'user-1', department: '品牌创意部', role: 'USER' }, }) ;(createMaterialQueryContext as jest.Mock).mockResolvedValue({ canAccessDeleted: false, expandedViewableTypes: ['任务-详情'], isSuperAdmin: false, }) ;(buildMaterialWhereFromSearchParams as jest.Mock).mockResolvedValue({ empty: false, where: { deletedAt: null, materialType: '任务-详情' }, }) }) test('一次请求只解析一次权限上下文,把统计查询降为 7 条且并发限制为 3', async () => { let activeQueries = 0 let maxActiveQueries = 0 const runQuery = async (result: T): Promise => { activeQueries += 1 maxActiveQueries = Math.max(maxActiveQueries, activeQueries) await new Promise((resolve) => setTimeout(resolve, 1)) activeQueries -= 1 return result } ;(prisma.material.count as jest.Mock).mockImplementation(() => runQuery(0)) ;(prisma.material.findMany as jest.Mock).mockImplementation(() => runQuery([])) ;(prisma.material.groupBy as jest.Mock).mockImplementation(() => runQuery([])) const response = await GET({ nextUrl: { searchParams: new URLSearchParams('materialType=任务-详情'), }, } as any) expect(response.status).toBe(200) expect(createMaterialQueryContext).toHaveBeenCalledTimes(1) expect(buildMaterialWhereFromSearchParams).toHaveBeenCalledTimes(7) expect(prisma.material.count).toHaveBeenCalledTimes(1) expect(prisma.material.groupBy).toHaveBeenCalledTimes(5) expect(prisma.material.findMany).toHaveBeenCalledTimes(1) expect(maxActiveQueries).toBeLessThanOrEqual(3) const context = (createMaterialQueryContext as jest.Mock).mock.results[0].value const resolvedContext = await context for (const call of (buildMaterialWhereFromSearchParams as jest.Mock).mock.calls) { expect(call[2]).toEqual(expect.objectContaining({ context: resolvedContext })) } }) test('各维度总数直接复用分组计数并保留空值素材', async () => { ;(prisma.material.count as jest.Mock).mockResolvedValue(9) ;(prisma.material.groupBy as jest.Mock) .mockResolvedValueOnce([ { categoryName: '客厅', _count: { _all: 2 } }, { categoryName: null, _count: { _all: 1 } }, ]) .mockResolvedValueOnce([{ sceneName: '室内', _count: { _all: 3 } }]) .mockResolvedValueOnce([{ platformName: '站内', _count: { _all: 4 } }]) .mockResolvedValueOnce([{ productCategoryName: '桌类', _count: { _all: 5 } }]) .mockResolvedValueOnce([{ materialType: '任务-详情', _count: { _all: 6 } }]) ;(prisma.material.findMany as jest.Mock).mockResolvedValue([ { createdAt: new Date('2026-07-01T00:00:00.000Z') }, { createdAt: new Date('2026-07-02T00:00:00.000Z') }, ]) const response = await GET({ nextUrl: { searchParams: new URLSearchParams() }, } as any) const body = await response.json() expect(body.total).toBe(9) expect(body.categories).toEqual([{ id: '客厅', name: '客厅', count: 2 }]) expect(body.totals).toEqual({ materialType: 6, category: 3, scene: 3, platform: 4, productCategory: 5, month: 2, }) }) })