161 lines
6.4 KiB
TypeScript
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)
|
|
})
|
|
})
|