gongxiangp2/lib/__tests__/materials-facets-route.test.ts

129 lines
4.4 KiB
TypeScript

/** @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 <T,>(result: T): Promise<T> => {
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,
})
})
})