gongxiangp2/lib/__tests__/s3-head.test.ts
chenpei 7326e273e1 fix: harden media upload and release safety
Protect upload cleanup and atomic completion paths, add durable media worker leases and brand asset validation, and introduce verified migration/bootstrap/deployment gates.
2026-08-12 18:04:08 +08:00

91 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** @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()
})
})