318 lines
9.9 KiB
TypeScript
318 lines
9.9 KiB
TypeScript
import { NextRequest } from 'next/server'
|
|
import { GET } from '../route'
|
|
|
|
jest.mock('@/lib/auth', () => ({
|
|
authOptions: {},
|
|
}))
|
|
|
|
jest.mock('next-auth', () => ({
|
|
getServerSession: jest.fn(),
|
|
}))
|
|
|
|
jest.mock('@/lib/prisma', () => ({
|
|
prisma: {
|
|
material: {
|
|
count: jest.fn(),
|
|
findMany: jest.fn(),
|
|
},
|
|
user: {
|
|
findUnique: jest.fn(),
|
|
},
|
|
department: {
|
|
findMany: jest.fn(),
|
|
},
|
|
uploadField: {
|
|
findMany: jest.fn(),
|
|
},
|
|
fieldDefinition: {
|
|
findMany: jest.fn(),
|
|
},
|
|
$queryRaw: jest.fn(),
|
|
materialLike: {
|
|
findMany: jest.fn(),
|
|
},
|
|
materialFavorite: {
|
|
findMany: jest.fn(),
|
|
},
|
|
},
|
|
}))
|
|
|
|
jest.mock('@/lib/url-rewrite', () => ({
|
|
rewriteMaterialStorageUrlsForClient: jest.fn((material) => material),
|
|
}))
|
|
|
|
jest.mock('@/lib/material-log', () => ({
|
|
createMaterialLog: jest.fn(),
|
|
}))
|
|
|
|
jest.mock('@/lib/license', () => ({
|
|
planDepartmentScopeWithPaidCore: jest.fn(),
|
|
planMaterialListQueryWithPaidCore: jest.fn(),
|
|
}))
|
|
|
|
jest.mock('@/lib/s3', () => ({
|
|
deleteFromS3: jest.fn(),
|
|
isS3Url: jest.fn(() => false),
|
|
}))
|
|
|
|
const { getServerSession } = require('next-auth')
|
|
const { prisma } = require('@/lib/prisma')
|
|
const { planDepartmentScopeWithPaidCore, planMaterialListQueryWithPaidCore } = require('@/lib/license')
|
|
|
|
describe('GET /api/materials 权限边界', () => {
|
|
beforeEach(() => {
|
|
jest.resetAllMocks()
|
|
prisma.user.findUnique.mockResolvedValue({
|
|
id: 'user-1',
|
|
role: 'USER',
|
|
isSuperAdmin: false,
|
|
isActive: true,
|
|
canDownload: true,
|
|
department: '设计部',
|
|
managedDepartment: null,
|
|
})
|
|
prisma.department.findMany.mockResolvedValue([
|
|
{ name: '设计部' },
|
|
{ name: '设计部-建模组' },
|
|
{ name: '运营部' },
|
|
])
|
|
planMaterialListQueryWithPaidCore.mockResolvedValue({
|
|
valid: true,
|
|
search: '',
|
|
standardSearchFields: [],
|
|
customSearchFieldKeys: [],
|
|
standardHomeFilters: [],
|
|
customHomeFilters: [],
|
|
moduleAvailable: true,
|
|
})
|
|
planDepartmentScopeWithPaidCore.mockResolvedValue({
|
|
valid: true,
|
|
scopedDepartments: ['设计部', '设计部-建模组'],
|
|
withinScope: true,
|
|
})
|
|
})
|
|
|
|
it('未登录时拒绝读取素材列表', async () => {
|
|
getServerSession.mockResolvedValue(null)
|
|
|
|
const response = await GET(new NextRequest('http://localhost/api/materials?pageSize=1'))
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(401)
|
|
expect(data.error).toBe('请先登录')
|
|
expect(prisma.material.count).not.toHaveBeenCalled()
|
|
expect(prisma.material.findMany).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('已登录时正常读取素材列表', async () => {
|
|
getServerSession.mockResolvedValue({
|
|
user: { id: 'user-1', role: 'USER' },
|
|
})
|
|
prisma.material.count.mockResolvedValue(0)
|
|
prisma.material.findMany.mockResolvedValue([])
|
|
prisma.uploadField.findMany.mockResolvedValue([])
|
|
prisma.fieldDefinition.findMany.mockResolvedValue([])
|
|
|
|
const response = await GET(new NextRequest('http://localhost/api/materials?pageSize=1'))
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(data.materials).toEqual([])
|
|
expect(data.totalCount).toBe(0)
|
|
expect(prisma.material.count).toHaveBeenCalled()
|
|
expect(prisma.material.findMany).toHaveBeenCalled()
|
|
})
|
|
|
|
it('禁用用户即使带着旧会话也不能读取素材列表', async () => {
|
|
getServerSession.mockResolvedValue({
|
|
user: { id: 'user-1', role: 'USER' },
|
|
})
|
|
prisma.user.findUnique.mockResolvedValueOnce({
|
|
id: 'user-1',
|
|
role: 'USER',
|
|
isSuperAdmin: false,
|
|
isActive: false,
|
|
canDownload: true,
|
|
department: '设计部',
|
|
managedDepartment: null,
|
|
})
|
|
|
|
const response = await GET(new NextRequest('http://localhost/api/materials?pageSize=1'))
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(403)
|
|
expect(data.error).toContain('账号不可用')
|
|
expect(prisma.material.count).not.toHaveBeenCalled()
|
|
expect(prisma.material.findMany).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('普通用户不能直接读取回收站素材列表', async () => {
|
|
getServerSession.mockResolvedValue({
|
|
user: { id: 'user-1', role: 'USER' },
|
|
})
|
|
|
|
const response = await GET(new NextRequest('http://localhost/api/materials?showDeleted=true&pageSize=1'))
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(403)
|
|
expect(data.error).toBe('无权限查看回收站')
|
|
expect(prisma.material.count).not.toHaveBeenCalled()
|
|
expect(prisma.material.findMany).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('普通管理员读取素材列表时只返回自己和管理部门范围内的素材', async () => {
|
|
getServerSession.mockResolvedValue({
|
|
user: { id: 'manager-1', role: 'ADMIN' },
|
|
})
|
|
prisma.user.findUnique.mockResolvedValueOnce({
|
|
id: 'manager-1',
|
|
role: 'ADMIN',
|
|
isSuperAdmin: false,
|
|
isActive: true,
|
|
canDownload: true,
|
|
department: '设计部',
|
|
managedDepartment: '设计部',
|
|
})
|
|
prisma.material.count.mockResolvedValue(0)
|
|
prisma.material.findMany.mockResolvedValue([])
|
|
prisma.uploadField.findMany.mockResolvedValue([])
|
|
prisma.fieldDefinition.findMany.mockResolvedValue([])
|
|
|
|
const response = await GET(new NextRequest('http://localhost/api/materials?pageSize=1'))
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(prisma.material.count.mock.calls[0][0].where.AND).toEqual(
|
|
expect.arrayContaining([
|
|
{ deletedAt: null },
|
|
expect.objectContaining({
|
|
OR: expect.arrayContaining([
|
|
{ userId: 'manager-1' },
|
|
{ ownerType: 'USER', ownerId: 'manager-1' },
|
|
{ user: { department: { in: ['设计部', '设计部-建模组'] } } },
|
|
{ user: { department: { startsWith: '设计部-' } } },
|
|
]),
|
|
}),
|
|
])
|
|
)
|
|
})
|
|
|
|
it('普通管理员读取回收站时同样只返回管理范围内的素材', async () => {
|
|
getServerSession.mockResolvedValue({
|
|
user: { id: 'manager-1', role: 'ADMIN' },
|
|
})
|
|
prisma.user.findUnique.mockResolvedValueOnce({
|
|
id: 'manager-1',
|
|
role: 'ADMIN',
|
|
isSuperAdmin: false,
|
|
isActive: true,
|
|
canDownload: true,
|
|
department: '设计部',
|
|
managedDepartment: '设计部',
|
|
})
|
|
prisma.material.count.mockResolvedValue(0)
|
|
prisma.material.findMany.mockResolvedValue([])
|
|
prisma.uploadField.findMany.mockResolvedValue([])
|
|
prisma.fieldDefinition.findMany.mockResolvedValue([])
|
|
|
|
const response = await GET(new NextRequest('http://localhost/api/materials?showDeleted=true&pageSize=1'))
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(prisma.material.count.mock.calls[0][0].where.AND).toEqual(
|
|
expect.arrayContaining([
|
|
{ deletedAt: { not: null } },
|
|
expect.objectContaining({
|
|
OR: expect.arrayContaining([
|
|
{ userId: 'manager-1' },
|
|
{ ownerType: 'USER', ownerId: 'manager-1' },
|
|
{ user: { department: { in: ['设计部', '设计部-建模组'] } } },
|
|
{ user: { department: { startsWith: '设计部-' } } },
|
|
]),
|
|
}),
|
|
])
|
|
)
|
|
})
|
|
|
|
it('搜索时会把可搜索自定义字段纳入查询', async () => {
|
|
getServerSession.mockResolvedValue({
|
|
user: { id: 'user-1', role: 'USER' },
|
|
})
|
|
prisma.uploadField.findMany.mockResolvedValue([
|
|
{ key: 'customCode' },
|
|
{ key: 'productModel' },
|
|
])
|
|
planMaterialListQueryWithPaidCore.mockResolvedValue({
|
|
valid: true,
|
|
search: 'CODE-001',
|
|
standardSearchFields: ['productModel', 'brandName'],
|
|
customSearchFieldKeys: ['customCode'],
|
|
standardHomeFilters: [],
|
|
customHomeFilters: [],
|
|
moduleAvailable: true,
|
|
})
|
|
prisma.$queryRaw.mockResolvedValue([{ id: 'material-custom-1' }])
|
|
prisma.fieldDefinition.findMany.mockResolvedValue([])
|
|
prisma.material.count.mockResolvedValue(1)
|
|
prisma.material.findMany.mockResolvedValue([])
|
|
|
|
const response = await GET(new NextRequest('http://localhost/api/materials?search=CODE-001&pageSize=1'))
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(prisma.uploadField.findMany).toHaveBeenCalledWith({
|
|
where: { isSearchable: true },
|
|
select: { key: true },
|
|
})
|
|
expect(planMaterialListQueryWithPaidCore).toHaveBeenCalledWith({
|
|
search: 'CODE-001',
|
|
searchableFields: [
|
|
{ key: 'customCode' },
|
|
{ key: 'productModel' },
|
|
],
|
|
homeFieldFilters: [],
|
|
enabledHomeFilterFields: [],
|
|
})
|
|
expect(prisma.$queryRaw).toHaveBeenCalled()
|
|
expect(prisma.material.count.mock.calls[0][0].where.AND).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
OR: expect.arrayContaining([{ id: { in: ['material-custom-1'] } }]),
|
|
}),
|
|
])
|
|
)
|
|
})
|
|
|
|
it('首页自定义字段筛选会按 metadata 字段值过滤素材', async () => {
|
|
getServerSession.mockResolvedValue({
|
|
user: { id: 'user-1', role: 'USER' },
|
|
})
|
|
prisma.fieldDefinition.findMany.mockResolvedValue([{ key: 'customCode' }])
|
|
planMaterialListQueryWithPaidCore.mockResolvedValueOnce({
|
|
valid: true,
|
|
search: '',
|
|
standardSearchFields: [],
|
|
customSearchFieldKeys: [],
|
|
standardHomeFilters: [],
|
|
customHomeFilters: [{ key: 'customCode', value: 'CODE-001' }],
|
|
moduleAvailable: true,
|
|
})
|
|
prisma.$queryRaw.mockResolvedValue([{ id: 'material-custom-1' }])
|
|
prisma.material.count.mockResolvedValue(1)
|
|
prisma.material.findMany.mockResolvedValue([])
|
|
|
|
const response = await GET(
|
|
new NextRequest('http://localhost/api/materials?homeField_customCode=CODE-001&pageSize=1')
|
|
)
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(prisma.fieldDefinition.findMany).toHaveBeenCalledWith({
|
|
where: {
|
|
key: { in: ['customCode'] },
|
|
isFilterEnabled: true,
|
|
},
|
|
select: { key: true },
|
|
})
|
|
expect(prisma.material.count.mock.calls[0][0].where.AND).toEqual(
|
|
expect.arrayContaining([{ id: { in: ['material-custom-1'] } }])
|
|
)
|
|
})
|
|
})
|