Protect upload cleanup and atomic completion paths, add durable media worker leases and brand asset validation, and introduce verified migration/bootstrap/deployment gates.
91 lines
3.0 KiB
TypeScript
91 lines
3.0 KiB
TypeScript
/** @jest-environment node */
|
||
|
||
export {}
|
||
|
||
const sendMock = jest.fn()
|
||
const headObjectCommandMock = jest.fn((input) => ({ input }))
|
||
|
||
jest.mock('@aws-sdk/client-s3', () => ({
|
||
S3Client: jest.fn(() => ({ send: sendMock })),
|
||
PutObjectCommand: jest.fn((input) => ({ input })),
|
||
DeleteObjectCommand: jest.fn((input) => ({ input })),
|
||
GetObjectCommand: jest.fn((input) => ({ input })),
|
||
HeadObjectCommand: headObjectCommandMock,
|
||
}))
|
||
|
||
describe('verifyUploadedS3Object', () => {
|
||
beforeEach(() => {
|
||
jest.clearAllMocks()
|
||
process.env.S3_BUCKET = 'gongxiangpan'
|
||
process.env.S3_PUBLIC_DOMAIN = 'https://s3.jiansun.vip/gongxiangpan'
|
||
process.env.S3_ENDPOINT = 'https://s3.jiansun.vip'
|
||
process.env.S3_REGION = 'us-east-1'
|
||
process.env.S3_ACCESS_KEY_ID = 'test-key'
|
||
process.env.S3_SECRET_ACCESS_KEY = 'test-secret'
|
||
process.env.S3_FILE_PATH = '由壹点AI/'
|
||
})
|
||
|
||
test('使用受控 Bucket/Key 做 HEAD,并接受匹配的大小和类型', async () => {
|
||
sendMock.mockResolvedValue({
|
||
ContentLength: 7975,
|
||
ContentType: 'image/png; charset=binary',
|
||
})
|
||
jest.resetModules()
|
||
const { verifyUploadedS3Object } = await import('@/lib/s3')
|
||
|
||
await verifyUploadedS3Object(
|
||
'https://s3.jiansun.vip/gongxiangpan/%E7%94%B1%E5%A3%B9%E7%82%B9AI/brand/header.png',
|
||
7975,
|
||
'image/png',
|
||
)
|
||
|
||
expect(headObjectCommandMock).toHaveBeenCalledWith({
|
||
Bucket: 'gongxiangpan',
|
||
Key: '由壹点AI/brand/header.png',
|
||
})
|
||
expect(sendMock).toHaveBeenCalledTimes(1)
|
||
})
|
||
|
||
test('对象大小不一致时拒绝通过', async () => {
|
||
sendMock.mockResolvedValue({ ContentLength: 1, ContentType: 'image/png' })
|
||
jest.resetModules()
|
||
const { verifyUploadedS3Object } = await import('@/lib/s3')
|
||
|
||
await expect(
|
||
verifyUploadedS3Object(
|
||
'https://s3.jiansun.vip/gongxiangpan/%E7%94%B1%E5%A3%B9%E7%82%B9AI/brand/header.png',
|
||
7975,
|
||
'image/png',
|
||
),
|
||
).rejects.toThrow('上传对象大小校验失败')
|
||
})
|
||
|
||
test('对象类型不一致时拒绝通过', async () => {
|
||
sendMock.mockResolvedValue({ ContentLength: 7975, ContentType: 'text/html' })
|
||
jest.resetModules()
|
||
const { verifyUploadedS3Object } = await import('@/lib/s3')
|
||
|
||
await expect(
|
||
verifyUploadedS3Object(
|
||
'https://s3.jiansun.vip/gongxiangpan/%E7%94%B1%E5%A3%B9%E7%82%B9AI/brand/header.png',
|
||
7975,
|
||
'image/png',
|
||
),
|
||
).rejects.toThrow('上传对象类型校验失败')
|
||
})
|
||
|
||
test.each([
|
||
'https://attacker.example.com/gongxiangpan/由壹点AI/brand/header.png',
|
||
'https://s3.jiansun.vip/gongxiangpan/other-project/header.png',
|
||
])('非受控地址或业务前缀不会发出 HEAD: %s', async (url) => {
|
||
jest.resetModules()
|
||
const { verifyUploadedS3Object } = await import('@/lib/s3')
|
||
|
||
await expect(
|
||
verifyUploadedS3Object(url, 7975, 'image/png'),
|
||
).rejects.toThrow()
|
||
expect(headObjectCommandMock).not.toHaveBeenCalled()
|
||
expect(sendMock).not.toHaveBeenCalled()
|
||
})
|
||
})
|