2026-05-28 15:11:36 +08:00

327 lines
9.9 KiB
TypeScript

import { NextRequest } from 'next/server'
import { GET, POST } from '../route'
jest.mock('@/lib/auth', () => ({
authOptions: {},
}))
jest.mock('next-auth', () => ({
getServerSession: jest.fn(),
}))
jest.mock('@/lib/prisma', () => ({
prisma: {
material: {
findFirst: jest.fn(),
findUnique: jest.fn(),
},
user: {
findUnique: jest.fn(),
},
department: {
findMany: jest.fn(),
},
},
}))
jest.mock('@/lib/material-share-token', () => ({
createMaterialShareToken: jest.fn(),
findValidMaterialShareToken: jest.fn(),
}))
jest.mock('@/lib/url-rewrite', () => ({
toSharedMaterialAssetProxyUrl: jest.fn((url, token) =>
url ? `/api/materials/asset?url=${encodeURIComponent(url)}&shareToken=${encodeURIComponent(token)}` : null
),
}))
jest.mock('@/lib/public-share-settings', () => ({
isPublicShareEnabled: jest.fn(),
}))
const { getServerSession } = require('next-auth')
const { prisma } = require('@/lib/prisma')
const { createMaterialShareToken, findValidMaterialShareToken } = require('@/lib/material-share-token')
const { isPublicShareEnabled } = require('@/lib/public-share-settings')
describe('/api/materials/share', () => {
beforeEach(() => {
jest.clearAllMocks()
isPublicShareEnabled.mockResolvedValue(true)
})
it('未登录时拒绝创建分享票据', async () => {
getServerSession.mockResolvedValue(null)
const response = await POST(
new NextRequest('http://localhost/api/materials/share', {
method: 'POST',
body: JSON.stringify({ materialId: 'material-1' }),
})
)
const data = await response.json()
expect(response.status).toBe(401)
expect(data.error).toBe('请先登录')
expect(createMaterialShareToken).not.toHaveBeenCalled()
})
it('公开分享开关关闭时拒绝创建新分享票据', async () => {
getServerSession.mockResolvedValue({ user: { id: 'user-1', role: 'USER' } })
prisma.user.findUnique.mockResolvedValue({
id: 'user-1',
role: 'USER',
isSuperAdmin: false,
department: '设计部',
managedDepartment: null,
})
isPublicShareEnabled.mockResolvedValue(false)
const response = await POST(
new NextRequest('http://localhost/api/materials/share', {
method: 'POST',
body: JSON.stringify({ materialId: 'material-1' }),
})
)
const data = await response.json()
expect(response.status).toBe(403)
expect(data.error).toBe('公开分享功能已关闭')
expect(prisma.material.findFirst).not.toHaveBeenCalled()
expect(createMaterialShareToken).not.toHaveBeenCalled()
})
it('已登录用户可以为未删除素材创建分享票据', async () => {
getServerSession.mockResolvedValue({ user: { id: 'user-1', role: 'USER' } })
prisma.user.findUnique.mockResolvedValue({
id: 'user-1',
role: 'USER',
isSuperAdmin: false,
department: '设计部',
managedDepartment: null,
})
prisma.material.findFirst.mockResolvedValue({
id: 'material-1',
userId: 'user-1',
ownerType: 'USER',
ownerId: 'user-1',
createdByUserId: 'user-1',
})
createMaterialShareToken.mockResolvedValue({
token: 'share-token',
expiresAt: new Date('2026-05-31T00:00:00.000Z'),
})
const response = await POST(
new NextRequest('http://localhost/api/materials/share', {
method: 'POST',
body: JSON.stringify({ materialId: 'material-1' }),
})
)
const data = await response.json()
expect(response.status).toBe(200)
expect(prisma.material.findFirst).toHaveBeenCalledWith({
where: { id: 'material-1', deletedAt: null },
select: {
id: true,
userId: true,
ownerType: true,
ownerId: true,
createdByUserId: true,
user: {
select: {
id: true,
department: true,
},
},
},
})
expect(createMaterialShareToken).toHaveBeenCalledWith('material-1', 'user-1')
expect(data.shareToken).toBe('share-token')
})
it('无关普通用户不能为别人的素材创建分享票据', async () => {
getServerSession.mockResolvedValue({ user: { id: 'user-3', role: 'USER' } })
prisma.user.findUnique.mockResolvedValue({
id: 'user-3',
role: 'USER',
isSuperAdmin: false,
department: '运营部',
managedDepartment: null,
})
prisma.material.findFirst.mockResolvedValue({
id: 'material-1',
userId: 'user-1',
ownerType: 'USER',
ownerId: 'user-1',
createdByUserId: 'user-1',
})
const response = await POST(
new NextRequest('http://localhost/api/materials/share', {
method: 'POST',
body: JSON.stringify({ materialId: 'material-1' }),
})
)
const data = await response.json()
expect(response.status).toBe(403)
expect(data.error).toBe('无权分享该素材')
expect(createMaterialShareToken).not.toHaveBeenCalled()
})
it('部门管理员只能为管理范围内素材创建分享票据', async () => {
getServerSession.mockResolvedValue({ user: { id: 'manager-1', role: 'ADMIN' } })
prisma.user.findUnique.mockResolvedValue({
id: 'manager-1',
role: 'ADMIN',
isSuperAdmin: false,
department: '设计部',
managedDepartment: '设计部',
})
prisma.material.findFirst.mockResolvedValue({
id: 'material-1',
userId: 'user-1',
ownerType: 'TEAM',
ownerId: 'team-1',
createdByUserId: 'user-1',
user: {
id: 'user-1',
department: '设计部-三维组',
},
})
createMaterialShareToken.mockResolvedValue({
token: 'share-token',
expiresAt: new Date('2026-05-31T00:00:00.000Z'),
})
const response = await POST(
new NextRequest('http://localhost/api/materials/share', {
method: 'POST',
body: JSON.stringify({ materialId: 'material-1' }),
})
)
expect(response.status).toBe(200)
expect(createMaterialShareToken).toHaveBeenCalledWith('material-1', 'manager-1')
expect(prisma.department.findMany).not.toHaveBeenCalled()
})
it('普通管理员不能跨部门创建分享票据', async () => {
getServerSession.mockResolvedValue({ user: { id: 'manager-1', role: 'ADMIN' } })
prisma.user.findUnique.mockResolvedValue({
id: 'manager-1',
role: 'ADMIN',
isSuperAdmin: false,
department: '设计部',
managedDepartment: '设计部',
})
prisma.material.findFirst.mockResolvedValue({
id: 'material-1',
userId: 'user-2',
ownerType: 'TEAM',
ownerId: 'team-1',
createdByUserId: 'user-2',
user: {
id: 'user-2',
department: '运营部',
},
})
prisma.department.findMany.mockResolvedValue([
{ name: '设计部' },
{ name: '运营部' },
])
const response = await POST(
new NextRequest('http://localhost/api/materials/share', {
method: 'POST',
body: JSON.stringify({ materialId: 'material-1' }),
})
)
const data = await response.json()
expect(response.status).toBe(403)
expect(data.error).toBe('无权分享该素材')
expect(createMaterialShareToken).not.toHaveBeenCalled()
})
it('没有有效分享票据时拒绝公开读取素材', async () => {
findValidMaterialShareToken.mockResolvedValue(null)
const response = await GET(
new NextRequest('http://localhost/api/materials/share?id=material-1&token=bad-token')
)
const data = await response.json()
expect(response.status).toBe(403)
expect(data.error).toBe('分享链接无效或已过期')
expect(prisma.material.findFirst).not.toHaveBeenCalled()
})
it('公开分享开关关闭时旧分享链接也不能继续公开读取', async () => {
isPublicShareEnabled.mockResolvedValue(false)
const response = await GET(
new NextRequest('http://localhost/api/materials/share?id=material-1&token=share-token')
)
const data = await response.json()
expect(response.status).toBe(403)
expect(data.error).toBe('公开分享功能已关闭')
expect(findValidMaterialShareToken).not.toHaveBeenCalled()
expect(prisma.material.findFirst).not.toHaveBeenCalled()
})
it('有效分享票据可读取分享数据并把图片改成带票据的代理地址', async () => {
findValidMaterialShareToken.mockResolvedValue({ id: 'token-row', materialId: 'material-1' })
prisma.material.findFirst.mockResolvedValue({
id: 'material-1',
previewUrl: 'http://minio:9000/bucket/preview.webp',
productModel: 'SPU-001',
brandName: null,
seriesName: null,
skuCode: null,
materialTexture: null,
categoryName: '店铺',
sceneName: '场景',
materialType: '3DMAX',
renderer: 'CR',
renderTime: null,
imageResolution: null,
colorMode: null,
authorName: '作者',
zipFileName: 'material.zip',
zipFileSize: BigInt(1024),
downloadCount: 0,
likeCount: 0,
displayImages: JSON.stringify(['http://minio:9000/bucket/detail.webp']),
createdAt: new Date('2026-05-24T00:00:00.000Z'),
isPremium: false,
videoCoverUrl: null,
videoPreview540Url: null,
videoPreviewClipUrl: null,
audioUrl: null,
})
const response = await GET(
new NextRequest('http://localhost/api/materials/share?id=material-1&token=share-token')
)
const data = await response.json()
expect(response.status).toBe(200)
expect(findValidMaterialShareToken).toHaveBeenCalledWith('share-token', 'material-1')
expect(prisma.material.findFirst).toHaveBeenCalledWith({
where: { id: 'material-1', deletedAt: null },
select: expect.objectContaining({
id: true,
previewUrl: true,
audioUrl: true,
}),
})
expect(data.zipFileSize).toBe('1024')
expect(data.previewUrl).toContain('shareToken=share-token')
expect(data.displayImages[0]).toContain('shareToken=share-token')
})
})