mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-12 22:13:05 +00:00
Derivada AVIF 720px q60 armazenada como objeto-irmão determinístico de cada foto aprovada (flickr-<id>-grade.avif), servida direto quando o cliente negocia Accept: image/avif; fallback JPEG on-the-fly em ausência/falha, heal preguiçoso via after(), warm no afterChange da aprovação e backfill pnpm archive:thumbnails idempotente com recibo e guardas *_CONFIRM. Sem migration, sem UI, sem mudança de URL pública ou pull-down. Closes #1424
168 lines
6.3 KiB
TypeScript
168 lines
6.3 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
acceptsAvif,
|
|
isPrivateMediaMissingError,
|
|
PRIVATE_MEDIA_FALLBACK_MIME_TYPE,
|
|
privateMediaContentDisposition,
|
|
privateMediaContentType,
|
|
privateMediaHeaders,
|
|
type PrivateMediaRange,
|
|
} from '@/lib/privateMedia'
|
|
|
|
const fullRange: PrivateMediaRange = {
|
|
status: 200,
|
|
headers: { 'Accept-Ranges': 'bytes', 'Content-Length': '100' },
|
|
}
|
|
|
|
const partialRange: PrivateMediaRange = {
|
|
status: 206,
|
|
headers: { 'Accept-Ranges': 'bytes', 'Content-Length': '10', 'Content-Range': 'bytes 0-9/100' },
|
|
}
|
|
|
|
const invalidRange: PrivateMediaRange = {
|
|
status: 416,
|
|
headers: { 'Content-Range': 'bytes */100' },
|
|
}
|
|
|
|
describe('private media response rules (C193/C199)', () => {
|
|
it('serves only allowlisted types inline; everything else degrades', () => {
|
|
expect(privateMediaContentType('video/mp4')).toBe('video/mp4')
|
|
expect(privateMediaContentType('video/quicktime')).toBe('video/quicktime')
|
|
expect(privateMediaContentType('video/x-matroska')).toBe('video/x-matroska')
|
|
expect(privateMediaContentType('audio/mpeg')).toBe('audio/mpeg')
|
|
expect(privateMediaContentType('image/jpeg')).toBe('image/jpeg')
|
|
expect(privateMediaContentType('text/html')).toBe(PRIVATE_MEDIA_FALLBACK_MIME_TYPE)
|
|
expect(privateMediaContentType(null)).toBe(PRIVATE_MEDIA_FALLBACK_MIME_TYPE)
|
|
})
|
|
|
|
// S27 — the content pieces' photo/audio families: a published photo must
|
|
// render and an audio piece must play, so these render inline; markup
|
|
// (SVG) and formats browsers do not render (HEIC) stay download-only.
|
|
it('serves the content piece image and audio families inline', () => {
|
|
for (const mimeType of [
|
|
'image/png',
|
|
'image/webp',
|
|
'image/gif',
|
|
'image/avif',
|
|
'audio/mp4',
|
|
'audio/aac',
|
|
'audio/ogg',
|
|
'audio/opus',
|
|
'audio/wav',
|
|
'audio/webm',
|
|
'audio/flac',
|
|
]) {
|
|
expect(privateMediaContentType(mimeType), mimeType).toBe(mimeType)
|
|
}
|
|
|
|
expect(privateMediaContentType('image/heic')).toBe(PRIVATE_MEDIA_FALLBACK_MIME_TYPE)
|
|
expect(privateMediaContentType('image/svg+xml')).toBe(PRIVATE_MEDIA_FALLBACK_MIME_TYPE)
|
|
})
|
|
|
|
it('builds both filename forms and encodes non-ASCII', () => {
|
|
expect(privateMediaContentDisposition('reel.mp4', false)).toBe(
|
|
'inline; filename="reel.mp4"; filename*=UTF-8\'\'reel.mp4',
|
|
)
|
|
expect(privateMediaContentDisposition('capa ção.png', true)).toBe(
|
|
'attachment; filename="capa_o.png"; filename*=UTF-8\'\'capa%20%C3%A7%C3%A3o.png',
|
|
)
|
|
expect(privateMediaContentDisposition(null, false)).toContain('filename="arquivo"')
|
|
})
|
|
|
|
it('keeps the range headers and adds the private streaming contract', () => {
|
|
const headers = privateMediaHeaders({
|
|
range: partialRange,
|
|
mimeType: 'video/mp4',
|
|
filename: 'reel.mp4',
|
|
download: false,
|
|
})
|
|
expect(headers.get('Content-Range')).toBe('bytes 0-9/100')
|
|
expect(headers.get('Content-Length')).toBe('10')
|
|
expect(headers.get('Accept-Ranges')).toBe('bytes')
|
|
expect(headers.get('Content-Type')).toBe('video/mp4')
|
|
expect(headers.get('Content-Disposition')).toContain('inline')
|
|
expect(headers.get('Cache-Control')).toBe('private, no-store')
|
|
expect(headers.get('X-Content-Type-Options')).toBe('nosniff')
|
|
expect(headers.get('Content-Security-Policy')).toBeNull()
|
|
})
|
|
|
|
it('forces a download for unsafe types and for ?download=1', () => {
|
|
const unsafe = privateMediaHeaders({
|
|
range: fullRange,
|
|
mimeType: 'text/html',
|
|
filename: 'roteiro.html',
|
|
download: false,
|
|
})
|
|
expect(unsafe.get('Content-Type')).toBe(PRIVATE_MEDIA_FALLBACK_MIME_TYPE)
|
|
expect(unsafe.get('Content-Disposition')).toContain('attachment')
|
|
expect(unsafe.get('Content-Security-Policy')).toBe("default-src 'none'")
|
|
|
|
const explicit = privateMediaHeaders({
|
|
range: fullRange,
|
|
mimeType: 'video/mp4',
|
|
filename: 'reel.mp4',
|
|
download: true,
|
|
})
|
|
expect(explicit.get('Content-Disposition')).toContain('attachment')
|
|
})
|
|
|
|
it('passes the 416 headers through untouched', () => {
|
|
const headers = privateMediaHeaders({
|
|
range: invalidRange,
|
|
mimeType: 'video/mp4',
|
|
filename: 'reel.mp4',
|
|
download: false,
|
|
})
|
|
expect(headers.get('Content-Range')).toBe('bytes */100')
|
|
expect(headers.get('Content-Type')).toBe('video/mp4')
|
|
})
|
|
})
|
|
|
|
describe('private media AVIF negotiation (C248)', () => {
|
|
it('accepts only an explicit image/avif token with a positive quality', () => {
|
|
expect(acceptsAvif('image/avif')).toBe(true)
|
|
expect(acceptsAvif('IMAGE/AVIF')).toBe(true)
|
|
expect(acceptsAvif('text/html, image/avif')).toBe(true)
|
|
expect(acceptsAvif('image/avif;q=0.5')).toBe(true)
|
|
expect(acceptsAvif('image/avif; q=0.5, image/webp')).toBe(true)
|
|
expect(acceptsAvif('image/avif;q=0')).toBe(false)
|
|
expect(acceptsAvif('image/avif;q=0.000')).toBe(false)
|
|
})
|
|
|
|
it('keeps the JPEG fallback for headers that never opted in', () => {
|
|
expect(acceptsAvif(null)).toBe(false)
|
|
expect(acceptsAvif(undefined)).toBe(false)
|
|
expect(acceptsAvif('')).toBe(false)
|
|
expect(acceptsAvif('*/*')).toBe(false)
|
|
expect(acceptsAvif('image/*')).toBe(false)
|
|
expect(acceptsAvif('image/jpeg, image/webp')).toBe(false)
|
|
expect(acceptsAvif('image/avifjunk')).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('private media missing-object classification (C246)', () => {
|
|
it('recognizes the S3 and local-disk absence shapes', () => {
|
|
expect(isPrivateMediaMissingError({ name: 'NoSuchKey' })).toBe(true)
|
|
expect(isPrivateMediaMissingError({ name: 'NotFound' })).toBe(true)
|
|
expect(isPrivateMediaMissingError({ Code: 'NoSuchKey' })).toBe(true)
|
|
expect(isPrivateMediaMissingError({ $metadata: { httpStatusCode: 404 } })).toBe(true)
|
|
expect(isPrivateMediaMissingError(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }))).toBe(
|
|
true,
|
|
)
|
|
})
|
|
|
|
it('never classifies a read failure of an existing object as missing', () => {
|
|
expect(isPrivateMediaMissingError(new Error('checksum mismatch'))).toBe(false)
|
|
expect(isPrivateMediaMissingError({ name: 'BadDigest' })).toBe(false)
|
|
expect(
|
|
isPrivateMediaMissingError(
|
|
Object.assign(new Error('socket hang up'), { $metadata: { httpStatusCode: 500 } }),
|
|
),
|
|
).toBe(false)
|
|
expect(isPrivateMediaMissingError(null)).toBe(false)
|
|
expect(isPrivateMediaMissingError('erro')).toBe(false)
|
|
})
|
|
})
|