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

161 lines
6.4 KiB
TypeScript

/** @jest-environment node */
jest.mock('@/lib/material-organization-access', () => ({
...jest.requireActual('@/lib/material-organization-access'),
canAccessOperationsGMaterials: jest.fn().mockResolvedValue(false),
}))
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(6)
expect(prisma.material.findMany).not.toHaveBeenCalled()
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([{ createdAt: new Date('2026-07-01T00:00:00.000Z'), _count: { _all: 2 } }])
.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,
})
})
test('同角色同旧部门的不同账号不能共享专属素材统计缓存', async () => {
;(prisma.material.groupBy as jest.Mock).mockResolvedValue([])
;(prisma.material.count as jest.Mock).mockResolvedValueOnce(9).mockResolvedValueOnce(1)
const request = { nextUrl: { searchParams: new URLSearchParams('cache-case=organization-users') } } as any
;(getServerSession as jest.Mock).mockResolvedValue({ user: { id: 'g-member', role: 'ADMIN', department: '运营中心' } })
const first = await GET(request)
;(getServerSession as jest.Mock).mockResolvedValue({ user: { id: 'outside-member', role: 'ADMIN', department: '运营中心' } })
const second = await GET(request)
expect((await first.json()).total).toBe(9)
expect((await second.json()).total).toBe(1)
expect(second.headers.get('cache-control')).toContain('no-store')
})
test('同一账号调出运营中心 G 后不会命中原来的专属统计缓存', async () => {
;(prisma.material.groupBy as jest.Mock).mockResolvedValue([])
;(prisma.material.count as jest.Mock).mockResolvedValueOnce(9).mockResolvedValueOnce(1)
const context = { canAccessDeleted: false, expandedViewableTypes: ['任务-详情'], isSuperAdmin: false }
;(createMaterialQueryContext as jest.Mock)
.mockResolvedValueOnce({ ...context, canAccessOperationsGMaterials: true })
.mockResolvedValueOnce({ ...context, canAccessOperationsGMaterials: false })
const request = { nextUrl: { searchParams: new URLSearchParams('cache-case=organization-transfer') } } as any
expect((await (await GET(request)).json()).total).toBe(9)
expect((await (await GET(request)).json()).total).toBe(1)
expect(createMaterialQueryContext).toHaveBeenCalledTimes(2)
})
})