gongxiangp2/lib/__tests__/material-update.test.ts

923 lines
27 KiB
TypeScript

import { prisma } from '@/lib/prisma'
import { createMaterialLog } from '@/lib/material-log'
import { compressImage, sanitizeFileName } from '@/lib/image-processor'
import { deleteFromS3, uploadToS3 } from '@/lib/s3'
import { uploadPublicDisplayToS3 } from '@/lib/public-display'
import { isSuperAdmin, isSystemAdmin } from '@/lib/department-permission'
import { queueTaskDetailFullSetNotification } from '@/lib/external-auto-listing'
import { updateMaterialWithSharedRules } from '@/lib/material-update'
jest.mock('@/lib/prisma', () => ({
prisma: {
material: {
findFirst: jest.fn(),
findMany: jest.fn(),
update: jest.fn(),
},
user: {
findUnique: jest.fn(),
findFirst: jest.fn(),
},
},
}))
jest.mock('@/lib/material-log', () => ({
createMaterialLog: jest.fn(),
}))
jest.mock('@/lib/s3', () => ({
deleteFromS3: jest.fn(),
uploadToS3: jest.fn(),
}))
jest.mock('@/lib/image-processor', () => ({
compressImage: jest.fn(),
sanitizeFileName: jest.fn(),
}))
jest.mock('@/lib/public-display', () => ({
buildMaterialPublicDisplayPath: jest.fn((fileName: string) => `materials/${fileName}`),
uploadPublicDisplayToS3: jest.fn(),
}))
jest.mock('@/lib/department-permission', () => ({
isSuperAdmin: jest.fn(),
isSystemAdmin: jest.fn(),
}))
jest.mock('@/lib/external-auto-listing', () => ({
queueTaskDetailFullSetNotification: jest.fn().mockResolvedValue(undefined),
}))
describe('material-update shared rules', () => {
const baseMaterial = {
id: 'material-1',
userId: 'user-1',
previewUrl: 'https://old-preview.example/cover.webp',
productModel: 'OLD-SPU',
categoryName: '旧店铺',
sceneName: '旧场景',
platformName: '旧平台',
productCategoryName: '旧品类',
materialType: '3D场景',
renderer: 'CR',
authorName: '旧作者',
taskNo: 'TASK202601141713525624',
dingtalkApprovalNo: null,
taskDetailRequesterUserId: null,
taskDetailRequesterName: null,
isFullSet: true,
assets: [],
deletedAt: null,
}
beforeEach(() => {
jest.clearAllMocks()
;(isSuperAdmin as jest.Mock).mockResolvedValue(false)
;(isSystemAdmin as jest.Mock).mockReturnValue(false)
;(sanitizeFileName as jest.Mock).mockImplementation((name: string) => name)
;(compressImage as jest.Mock).mockResolvedValue(Buffer.from('compressed'))
;(uploadPublicDisplayToS3 as jest.Mock).mockResolvedValue('https://new-preview.example/cover.webp')
;(uploadToS3 as jest.Mock).mockResolvedValue('https://new-preview.example/cover.webp')
;(deleteFromS3 as jest.Mock).mockResolvedValue(undefined)
})
test('切换到标准模型时应自动回写通用店铺和场景', async () => {
;(prisma.material.findFirst as jest.Mock).mockResolvedValue(baseMaterial)
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
...baseMaterial,
...data,
}))
await updateMaterialWithSharedRules({
materialId: 'material-1',
operatorUserId: 'user-1',
operatorRole: 'USER',
input: {
productModel: 'SPU-001',
categoryName: '',
sceneName: '',
platformName: 'Amazon',
productCategoryName: '椅子',
materialType: '标准模型',
renderer: '',
authorName: '新作者',
userId: 'user-1',
},
})
expect(prisma.material.update).toHaveBeenCalledWith({
where: { id: 'material-1' },
data: expect.objectContaining({
productModel: 'SPU-001',
categoryName: '通用',
sceneName: '通用',
platformName: 'Amazon',
productCategoryName: '椅子',
materialType: '标准模型',
}),
})
expect(createMaterialLog).toHaveBeenCalled()
})
test('音乐类型编辑时不应强制要求店铺和场景,并应自动回写通用', async () => {
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
...baseMaterial,
materialType: '音乐',
categoryName: '',
sceneName: '',
productCategoryName: '',
renderer: '',
})
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
...baseMaterial,
...data,
}))
const result = await updateMaterialWithSharedRules({
materialId: 'material-1',
operatorUserId: 'user-1',
operatorRole: 'USER',
input: {
productModel: 'MUSIC-001',
categoryName: '',
sceneName: '',
platformName: 'Spotify',
productCategoryName: '',
materialType: '音乐',
renderer: '',
authorName: '音乐作者',
userId: 'user-1',
},
})
expect('error' in result).toBe(false)
expect(prisma.material.update).toHaveBeenCalledWith({
where: { id: 'material-1' },
data: expect.objectContaining({
categoryName: '通用',
sceneName: '通用',
materialType: '音乐',
platformName: 'Spotify',
}),
})
})
test('标准模型缺少品类时应返回 400', async () => {
;(prisma.material.findFirst as jest.Mock).mockResolvedValue(baseMaterial)
const result = await updateMaterialWithSharedRules({
materialId: 'material-1',
operatorUserId: 'user-1',
operatorRole: 'USER',
input: {
productModel: 'SPU-001',
categoryName: '',
sceneName: '',
platformName: '',
productCategoryName: '',
materialType: '标准模型',
renderer: '',
authorName: '测试作者',
userId: 'user-1',
},
})
expect(result).toEqual({
error: '标准模型类型必须选择品类',
status: 400,
})
expect(prisma.material.update).not.toHaveBeenCalled()
})
test('证书-检测报告允许只填写报告名称 / 检测对象', async () => {
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
...baseMaterial,
materialType: '证书-检测报告',
categoryName: '通用',
sceneName: '通用',
productCategoryName: '板材',
productModel: '',
reportTitle: null,
renderer: '',
})
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
...baseMaterial,
...data,
}))
const result = await updateMaterialWithSharedRules({
materialId: 'material-1',
operatorUserId: 'user-1',
operatorRole: 'USER',
input: {
productModel: '',
reportTitle: 'E0级板材检测报告',
categoryName: '',
sceneName: '',
platformName: '',
productCategoryName: '板材',
materialType: '证书-检测报告',
renderer: '',
authorName: '测试作者',
userId: 'user-1',
},
})
expect('error' in result).toBe(false)
expect(prisma.material.update).toHaveBeenCalledWith({
where: { id: 'material-1' },
data: expect.objectContaining({
productModel: '',
reportTitle: 'E0级板材检测报告',
categoryName: '通用',
sceneName: '通用',
materialType: '证书-检测报告',
}),
})
})
test('证书-检测报告同时缺少 SPU 和报告名称时应返回 400', async () => {
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
...baseMaterial,
materialType: '证书-检测报告',
categoryName: '通用',
sceneName: '通用',
productCategoryName: '板材',
productModel: '',
reportTitle: null,
renderer: '',
})
const result = await updateMaterialWithSharedRules({
materialId: 'material-1',
operatorUserId: 'user-1',
operatorRole: 'USER',
input: {
productModel: '',
reportTitle: '',
categoryName: '',
sceneName: '',
platformName: '',
productCategoryName: '板材',
materialType: '证书-检测报告',
renderer: '',
authorName: '测试作者',
userId: 'user-1',
},
})
expect(result).toEqual({
error: 'SPU 和 报告名称/检测对象 至少填写一个',
status: 400,
})
expect(prisma.material.update).not.toHaveBeenCalled()
})
test('非任务详情类型应强制将 isFullSet 保存为 false', async () => {
;(prisma.material.findFirst as jest.Mock).mockResolvedValue(baseMaterial)
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
...baseMaterial,
...data,
}))
await updateMaterialWithSharedRules({
materialId: 'material-1',
operatorUserId: 'user-1',
operatorRole: 'USER',
input: {
productModel: 'MUSIC-001',
categoryName: '',
sceneName: '',
platformName: '',
productCategoryName: '',
materialType: '音乐',
renderer: '',
authorName: '音乐作者',
userId: 'user-1',
isFullSet: true,
},
})
expect(prisma.material.update).toHaveBeenCalledWith({
where: { id: 'material-1' },
data: expect.objectContaining({
categoryName: '通用',
sceneName: '通用',
isFullSet: false,
}),
})
})
test('任务-页面编辑时应保留任务号能力并要求店铺和场景', async () => {
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
...baseMaterial,
materialType: '任务-页面',
categoryName: '旧店铺',
sceneName: '旧场景',
taskNo: 'TASK202601141713525624-01',
})
;(prisma.material.findFirst as jest.Mock)
.mockResolvedValueOnce({
...baseMaterial,
materialType: '任务-页面',
categoryName: '旧店铺',
sceneName: '旧场景',
taskNo: 'TASK202601141713525624-01',
})
.mockResolvedValueOnce(null)
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
...baseMaterial,
...data,
}))
const result = await updateMaterialWithSharedRules({
materialId: 'material-1',
operatorUserId: 'user-1',
operatorRole: 'USER',
input: {
productModel: 'TASK-SPU-001',
categoryName: '新店铺',
sceneName: '新场景',
platformName: '',
productCategoryName: '',
materialType: '任务-页面',
renderer: '',
authorName: '任务作者',
userId: 'user-1',
taskNo: 'task202601141713525624-02',
},
})
expect('error' in result).toBe(false)
expect(prisma.material.update).toHaveBeenCalledWith({
where: { id: 'material-1' },
data: expect.objectContaining({
categoryName: '新店铺',
sceneName: '新场景',
taskNo: 'TASK202601141713525624-02',
materialType: '任务-页面',
}),
})
})
test('任务类素材任务号重复时应返回 409', async () => {
;(prisma.material.findFirst as jest.Mock)
.mockResolvedValueOnce({
...baseMaterial,
materialType: '任务-页面',
categoryName: '店铺A',
sceneName: '场景A',
taskNo: null,
})
.mockResolvedValueOnce({ id: 'material-2' })
const result = await updateMaterialWithSharedRules({
materialId: 'material-1',
operatorUserId: 'user-1',
operatorRole: 'USER',
input: {
productModel: 'TASK-SPU-001',
categoryName: '店铺A',
sceneName: '场景A',
platformName: '',
productCategoryName: '',
materialType: '任务-页面',
renderer: '',
authorName: '任务作者',
userId: 'user-1',
taskNo: 'TASK202601141713525624-01',
},
})
expect(result).toEqual({
error: '任务号已存在',
status: 409,
})
expect(prisma.material.update).not.toHaveBeenCalled()
})
test('任务-详情编辑时应支持保存和清空出图方式', async () => {
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
...baseMaterial,
materialType: '任务-详情',
taskDetailSourceType: '实拍',
categoryName: '店铺A',
sceneName: '场景A',
})
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
...baseMaterial,
...data,
}))
await updateMaterialWithSharedRules({
materialId: 'material-1',
operatorUserId: 'user-1',
operatorRole: 'USER',
input: {
productModel: 'TASK-SPU-001',
categoryName: '店铺A',
sceneName: '场景A',
platformName: '',
productCategoryName: '',
materialType: '任务-详情',
renderer: '',
authorName: '任务作者',
userId: 'user-1',
taskDetailSourceType: 'AI',
},
})
expect(prisma.material.update).toHaveBeenCalledWith({
where: { id: 'material-1' },
data: expect.objectContaining({
taskDetailSourceType: 'AI',
}),
})
await updateMaterialWithSharedRules({
materialId: 'material-1',
operatorUserId: 'user-1',
operatorRole: 'USER',
input: {
productModel: 'TASK-SPU-001',
categoryName: '店铺A',
sceneName: '场景A',
platformName: '',
productCategoryName: '',
materialType: '任务-详情',
renderer: '',
authorName: '任务作者',
userId: 'user-1',
taskDetailSourceType: '',
},
})
expect(prisma.material.update).toHaveBeenLastCalledWith({
where: { id: 'material-1' },
data: expect.objectContaining({
taskDetailSourceType: null,
}),
})
})
test('历史任务详情没有需求运营时,编辑其他字段仍可继续留空', async () => {
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
...baseMaterial,
materialType: '任务-详情',
categoryName: '店铺A',
sceneName: '场景A',
taskDetailRequesterUserId: null,
taskDetailRequesterName: null,
})
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
...baseMaterial,
...data,
}))
const result = await updateMaterialWithSharedRules({
materialId: 'material-1',
operatorUserId: 'user-1',
operatorRole: 'USER',
input: {
productModel: 'TASK-SPU-001',
categoryName: '店铺A',
sceneName: '场景A',
materialType: '任务-详情',
renderer: '',
authorName: '任务作者',
userId: 'user-1',
taskDetailRequesterUserId: '',
},
})
expect('error' in result).toBe(false)
expect(prisma.material.update).toHaveBeenCalledWith({
where: { id: 'material-1' },
data: expect.objectContaining({
taskDetailRequesterUserId: null,
taskDetailRequesterName: null,
}),
})
})
test('其他素材切换为任务详情时必须选择需求运营', async () => {
;(prisma.material.findFirst as jest.Mock).mockResolvedValue(baseMaterial)
const result = await updateMaterialWithSharedRules({
materialId: 'material-1',
operatorUserId: 'user-1',
operatorRole: 'USER',
input: {
productModel: 'TASK-SPU-001',
categoryName: '店铺A',
sceneName: '场景A',
materialType: '任务-详情',
renderer: '',
authorName: '任务作者',
userId: 'user-1',
taskDetailRequesterUserId: '',
},
})
expect(result).toEqual({
error: '请选择需求运营人员',
status: 400,
})
expect(prisma.material.update).not.toHaveBeenCalled()
})
test('已有需求运营的任务详情不能清空该人员', async () => {
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
...baseMaterial,
materialType: '任务-详情',
categoryName: '店铺A',
sceneName: '场景A',
taskDetailRequesterUserId: 'requester-old',
taskDetailRequesterName: '运营旧',
})
const result = await updateMaterialWithSharedRules({
materialId: 'material-1',
operatorUserId: 'user-1',
operatorRole: 'USER',
input: {
productModel: 'TASK-SPU-001',
categoryName: '店铺A',
sceneName: '场景A',
materialType: '任务-详情',
renderer: '',
authorName: '任务作者',
userId: 'user-1',
taskDetailRequesterUserId: '',
},
})
expect(result).toEqual({
error: '需求运营人员不能清空,只能更换',
status: 400,
})
expect(prisma.material.update).not.toHaveBeenCalled()
})
test('更换需求运营时应校验有效人员并由服务端保存姓名快照', async () => {
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
...baseMaterial,
materialType: '任务-详情',
categoryName: '店铺A',
sceneName: '场景A',
taskDetailRequesterUserId: 'requester-old',
taskDetailRequesterName: '运营旧',
})
;(prisma.user.findFirst as jest.Mock).mockResolvedValue({
id: 'requester-new',
username: 'new-operator',
realName: '运营新',
})
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
...baseMaterial,
...data,
}))
const result = await updateMaterialWithSharedRules({
materialId: 'material-1',
operatorUserId: 'user-1',
operatorRole: 'USER',
input: {
productModel: 'TASK-SPU-001',
categoryName: '店铺A',
sceneName: '场景A',
materialType: '任务-详情',
renderer: '',
authorName: '任务作者',
userId: 'user-1',
taskDetailRequesterUserId: 'requester-new',
},
})
expect('error' in result).toBe(false)
expect(prisma.user.findFirst).toHaveBeenCalledWith({
where: {
id: 'requester-new',
isActive: true,
status: 'active',
},
select: {
id: true,
username: true,
realName: true,
},
})
expect(prisma.material.update).toHaveBeenCalledWith({
where: { id: 'material-1' },
data: expect.objectContaining({
taskDetailRequesterUserId: 'requester-new',
taskDetailRequesterName: '运营新',
}),
})
})
test('任务详情切换为其他素材类型时应清空需求运营字段', async () => {
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
...baseMaterial,
materialType: '任务-详情',
taskDetailRequesterUserId: 'requester-old',
taskDetailRequesterName: '运营旧',
})
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
...baseMaterial,
...data,
}))
await updateMaterialWithSharedRules({
materialId: 'material-1',
operatorUserId: 'user-1',
operatorRole: 'USER',
input: {
productModel: 'MUSIC-001',
categoryName: '',
sceneName: '',
materialType: '音乐',
renderer: '',
authorName: '任务作者',
userId: 'user-1',
taskDetailRequesterUserId: 'requester-old',
},
})
expect(prisma.material.update).toHaveBeenCalledWith({
where: { id: 'material-1' },
data: expect.objectContaining({
taskDetailRequesterUserId: null,
taskDetailRequesterName: null,
}),
})
})
test('任务-其他编辑时不应强制要求店铺和场景,并应自动回写通用', async () => {
;(prisma.material.findFirst as jest.Mock)
.mockResolvedValueOnce({
...baseMaterial,
materialType: '任务-其他',
categoryName: '通用',
sceneName: '通用',
taskNo: 'TASK202603310001',
productModel: 'TASK202603310001',
})
.mockResolvedValueOnce(null)
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
...baseMaterial,
...data,
}))
const result = await updateMaterialWithSharedRules({
materialId: 'material-1',
operatorUserId: 'user-1',
operatorRole: 'USER',
input: {
productModel: 'TASK202603310001',
categoryName: '',
sceneName: '',
platformName: '',
productCategoryName: '',
materialType: '任务-其他',
renderer: '',
authorName: '任务作者',
userId: 'user-1',
taskNo: 'task202603310001',
dingtalkApprovalNo: '202508211445000157693',
},
})
expect('error' in result).toBe(false)
expect(prisma.material.update).toHaveBeenCalledWith({
where: { id: 'material-1' },
data: expect.objectContaining({
productModel: 'TASK202603310001',
categoryName: '通用',
sceneName: '通用',
taskNo: 'TASK202603310001',
dingtalkApprovalNo: '202508211445000157693',
materialType: '任务-其他',
}),
})
})
test('任务素材编辑时应拒绝无效的钉钉审批编号', async () => {
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
...baseMaterial,
materialType: '任务-页面',
})
const result = await updateMaterialWithSharedRules({
materialId: 'material-1',
operatorUserId: 'user-1',
operatorRole: 'USER',
input: {
productModel: 'SPU-001',
categoryName: '店铺A',
sceneName: '场景A',
materialType: '任务-页面',
renderer: '',
authorName: '任务作者',
dingtalkApprovalNo: '20250230123',
},
})
expect(result).toEqual({
error: '钉钉审批编号前8位不是有效日期',
status: 400,
})
expect(prisma.material.update).not.toHaveBeenCalled()
})
test('非任务素材不能写入钉钉审批编号', async () => {
;(prisma.material.findFirst as jest.Mock).mockResolvedValue(baseMaterial)
const result = await updateMaterialWithSharedRules({
materialId: 'material-1',
operatorUserId: 'user-1',
operatorRole: 'USER',
input: {
productModel: 'SPU-001',
categoryName: '店铺A',
sceneName: '场景A',
materialType: '3D场景',
renderer: 'CR',
authorName: '作者',
dingtalkApprovalNo: '202508211445000157693',
},
})
expect(result).toEqual({
error: '钉钉审批编号仅适用于“任务-”素材',
status: 400,
})
expect(prisma.material.update).not.toHaveBeenCalled()
})
test('任务-详情编辑时应按已有资产自动回写 isFullSet', async () => {
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
...baseMaterial,
materialType: '任务-详情',
isFullSet: false,
categoryName: '店铺A',
sceneName: '场景A',
assets: [
{ assetType: 'main-preview' },
{ assetType: 'sku-preview' },
{ assetType: 'detail-preview' },
],
})
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
...baseMaterial,
...data,
}))
await updateMaterialWithSharedRules({
materialId: 'material-1',
operatorUserId: 'user-1',
operatorRole: 'USER',
input: {
productModel: 'SPU-001',
categoryName: '店铺A',
sceneName: '场景A',
platformName: '',
productCategoryName: '',
materialType: '任务-详情',
renderer: '',
authorName: '任务作者',
userId: 'user-1',
} as any,
})
expect(prisma.material.update).toHaveBeenCalledWith({
where: { id: 'material-1' },
data: expect.objectContaining({
isFullSet: true,
taskNo: 'TASK202601141713525624',
}),
})
expect(queueTaskDetailFullSetNotification).toHaveBeenCalledWith('material-1')
})
test('已是全套的任务-详情再次编辑时不应重复触发自动上架通知', async () => {
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
...baseMaterial,
materialType: '任务-详情',
isFullSet: true,
categoryName: '店铺A',
sceneName: '场景A',
assets: [
{ assetType: 'main-preview' },
{ assetType: 'sku-preview' },
{ assetType: 'detail-preview' },
],
})
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
...baseMaterial,
...data,
isFullSet: true,
}))
await updateMaterialWithSharedRules({
materialId: 'material-1',
operatorUserId: 'user-1',
operatorRole: 'USER',
input: {
productModel: 'SPU-001',
categoryName: '店铺A',
sceneName: '场景A',
platformName: '',
productCategoryName: '',
materialType: '任务-详情',
renderer: '',
authorName: '任务作者',
userId: 'user-1',
} as any,
})
expect(queueTaskDetailFullSetNotification).not.toHaveBeenCalled()
})
test('任务类素材不允许手工替换预览图', async () => {
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
...baseMaterial,
materialType: '任务-详情',
categoryName: '店铺A',
sceneName: '场景A',
isFullSet: true,
assets: [
{ assetType: 'main-preview' },
{ assetType: 'sku-preview' },
{ assetType: 'detail-preview' },
],
})
const result = await updateMaterialWithSharedRules({
materialId: 'material-1',
operatorUserId: 'user-1',
operatorRole: 'USER',
input: {
productModel: 'SPU-001',
categoryName: '店铺A',
sceneName: '场景A',
platformName: '',
productCategoryName: '',
materialType: '任务-详情',
renderer: '',
authorName: '任务作者',
userId: 'user-1',
isFullSet: false,
},
previewImage: new File(['preview'], 'preview.png', { type: 'image/png' }),
})
expect(result).toEqual({
error: '当前素材类型不支持手工替换预览图',
status: 400,
})
expect(prisma.material.update).not.toHaveBeenCalled()
})
test('可手工替换的预览图应写入 public-display materials 前缀', async () => {
;(prisma.material.findFirst as jest.Mock).mockResolvedValue(baseMaterial)
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
...baseMaterial,
...data,
}))
const previewImage = {
name: 'cover.png',
size: 7,
arrayBuffer: jest.fn().mockResolvedValue(Buffer.from('preview')),
} as any
const result = await updateMaterialWithSharedRules({
materialId: 'material-1',
operatorUserId: 'user-1',
operatorRole: 'USER',
input: {
productModel: 'SPU-001',
categoryName: '新店铺',
sceneName: '新场景',
platformName: 'Amazon',
productCategoryName: '',
materialType: '3D场景',
renderer: 'CR',
authorName: '新作者',
userId: 'user-1',
},
previewImage,
})
expect('error' in result).toBe(false)
expect(uploadPublicDisplayToS3).toHaveBeenCalledWith(
Buffer.from('compressed'),
expect.stringMatching(/^materials\/preview-\d+-cover\.png$/),
'image/webp',
)
expect(uploadToS3).not.toHaveBeenCalled()
expect(deleteFromS3).toHaveBeenCalledWith('https://old-preview.example/cover.webp')
})
})