teqo-backup/tests/unit/largeS3Upload.unit.spec.ts
Francisco Solla 4c632d43bf feat(C199-large): gravações do acervo sem o teto de 4 GB
O diálogo, a rota e o stream deixam de recusar arquivos acima de 4 GiB; o
nome do arquivo e o `formatRecordingFileSize` continuam iguais. Acima de
2 GiB a mídia não passa mais pelo `fs.readFile` do Payload local API: a row
nasce de um placeholder de 0 byte e a mesma key é sobrescrita por upload
multipart direto no S3 (padrão C225-large, extraído para
`utilities/privateMedia/privateMediaUpload.ts` e reusado pelo espelho de
falas web, que mantém o teto de 5 GiB via `maxBytes` do chamador). O objeto
é removido se a transação falhar, sem row fantasma, e o upload multipart de
gravações não tem teto (`Infinity`).
2026-09-26 13:54:19 -03:00

115 lines
3.4 KiB
TypeScript

// @vitest-environment node
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import {
deleteLargeMedia,
isLargeMedia,
LARGE_MEDIA_THRESHOLD_BYTES,
mimeTypeForPath,
uploadLargeMedia,
} from '@/utilities/media/largeS3Upload'
const s3Env = {
S3_BUCKET: 'teqo-media',
S3_ENDPOINT: 'http://127.0.0.1:3900',
S3_ACCESS_KEY_ID: 'key',
S3_SECRET_ACCESS_KEY: 'secret',
}
const withTempFile = async (run: (path: string) => Promise<void>): Promise<void> => {
const dir = await mkdtemp(join(tmpdir(), 'large-s3-test-'))
try {
const path = join(dir, 'source.mp4')
await writeFile(path, Buffer.alloc(16))
await run(path)
} finally {
await rm(dir, { recursive: true, force: true })
}
}
describe('large S3 media upload', () => {
it('switches only above the 2 GiB Payload buffer boundary', () => {
expect(isLargeMedia(LARGE_MEDIA_THRESHOLD_BYTES)).toBe(false)
expect(isLargeMedia(LARGE_MEDIA_THRESHOLD_BYTES + 1)).toBe(true)
})
it('maps the media extension to a safe content type', () => {
expect(mimeTypeForPath('/tmp/source.mp4')).toBe('video/mp4')
expect(mimeTypeForPath('/tmp/source.mov')).toBe('video/quicktime')
expect(mimeTypeForPath('/tmp/source.mkv')).toBe('video/x-matroska')
expect(mimeTypeForPath('/tmp/source.m4v')).toBe('video/x-m4v')
expect(mimeTypeForPath('/tmp/source.unknown')).toBe('application/octet-stream')
})
it('honors the default ceiling and an explicit caller override', async () => {
await withTempFile(async (path) => {
await expect(
uploadLargeMedia({
inputPath: path,
env: s3Env,
maxBytes: 8,
upload: async () => undefined,
}),
).rejects.toThrow('excede o limite')
const keys: string[] = []
await uploadLargeMedia({
inputPath: path,
env: s3Env,
maxBytes: Number.POSITIVE_INFINITY,
upload: async ({ key }) => {
keys.push(key)
},
})
expect(keys).toHaveLength(1)
})
})
it('streams a large file to S3 and returns row metadata', async () => {
await withTempFile(async (path) => {
const calls: { key: string; contentType: string; filesize: number }[] = []
const uploaded = await uploadLargeMedia({
inputPath: path,
env: s3Env,
upload: async ({ key, contentType, filesize }) => {
calls.push({ key, contentType, filesize })
},
})
expect(uploaded.filename).toMatch(/^web-[0-9a-f-]+\.mp4$/)
expect(uploaded.mimeType).toBe('video/mp4')
expect(uploaded.filesize).toBe(16)
expect(uploaded.url).toContain('/teqo-media/')
expect(calls).toEqual([{ key: uploaded.filename, contentType: 'video/mp4', filesize: 16 }])
})
})
it('fails closed when S3 is not configured', async () => {
await withTempFile(async (path) => {
await expect(
uploadLargeMedia({
inputPath: path,
env: {},
upload: async () => undefined,
}),
).rejects.toThrow('S3')
})
})
it('deletes the exact object key on cleanup', async () => {
const keys: string[] = []
await deleteLargeMedia({
filename: 'web-test.mp4',
env: s3Env,
remove: async ({ key }) => {
keys.push(key)
},
})
expect(keys).toEqual(['web-test.mp4'])
})
})