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'] } }]) ) }) })