749f90fe62
Hosted uploads larger than the 4 MB multipart ceiling (Vercel's 4.5 MB request-body cap) now go POST /upload/create (signed PUT URL, rate-limited) -> PUT to the raw Storage URL -> POST /upload/complete (server-side magic-byte and size validation, sha256, WORM move, idempotent), reusing the #1378 pending-upload primitives. uploadAndExtract is split into uploadDocument + processArchivedDocument so both paths share the inbox pipeline. Dokumentinkorgen and the supplier-invoice form use the new path only above the threshold; files that fit keep the multipart route. Cap stays at 10 MB (the issue asks for 20 MB: founder call). Refs #1551
97 lines
3.7 KiB
TypeScript
97 lines
3.7 KiB
TypeScript
import { describe, it, expect, afterEach, vi } from 'vitest'
|
|
import {
|
|
HOSTED_MAX_UPLOAD_BYTES,
|
|
HOSTED_REQUEST_BODY_LIMIT_BYTES,
|
|
INBOX_MAX_UPLOAD_BYTES,
|
|
exceedsHostedUploadLimit,
|
|
exceedsInboxUploadLimit,
|
|
formatMegabytes,
|
|
inboxTooLargeMessage,
|
|
isShrinkableImage,
|
|
tooLargeMessage,
|
|
} from '../upload-size'
|
|
import { shrinkImageForUpload } from '../shrink-image'
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs()
|
|
})
|
|
|
|
describe('upload size limits', () => {
|
|
it('stays under the platform ceiling so the multipart envelope fits', () => {
|
|
expect(HOSTED_MAX_UPLOAD_BYTES).toBeLessThan(HOSTED_REQUEST_BODY_LIMIT_BYTES)
|
|
})
|
|
|
|
it('flags a file over the limit on hosted', () => {
|
|
vi.stubEnv('NEXT_PUBLIC_SELF_HOSTED', '')
|
|
expect(exceedsHostedUploadLimit(HOSTED_MAX_UPLOAD_BYTES + 1)).toBe(true)
|
|
expect(exceedsHostedUploadLimit(HOSTED_MAX_UPLOAD_BYTES)).toBe(false)
|
|
})
|
|
|
|
// Docker self-hosting has no proxy in front of the app, so the route's own
|
|
// MAX_FILE_SIZE governs and nothing should be refused or re-encoded here.
|
|
it('never flags a file on self-hosted', () => {
|
|
vi.stubEnv('NEXT_PUBLIC_SELF_HOSTED', 'true')
|
|
expect(exceedsHostedUploadLimit(50 * 1024 * 1024)).toBe(false)
|
|
})
|
|
|
|
it('recognises the image types a canvas can re-encode', () => {
|
|
expect(isShrinkableImage('image/jpeg')).toBe(true)
|
|
expect(isShrinkableImage('image/HEIC')).toBe(true)
|
|
expect(isShrinkableImage('application/pdf')).toBe(false)
|
|
expect(isShrinkableImage('')).toBe(false)
|
|
expect(isShrinkableImage(null)).toBe(false)
|
|
})
|
|
|
|
it('names both the actual size and the ceiling', () => {
|
|
const message = tooLargeMessage(6 * 1024 * 1024)
|
|
expect(message).toContain('6,0 MB')
|
|
expect(message).toContain(formatMegabytes(HOSTED_MAX_UPLOAD_BYTES))
|
|
})
|
|
})
|
|
|
|
// The inbox ceiling is the route's own MAX_FILE_SIZE (10 MB), mirrored here
|
|
// because core components cannot import from the extension. Files between
|
|
// the hosted body limit and this one take the direct-to-storage path.
|
|
describe('inbox upload ceiling', () => {
|
|
it('sits above the hosted body limit and matches the route promise of 10 MB', () => {
|
|
expect(INBOX_MAX_UPLOAD_BYTES).toBe(10 * 1024 * 1024)
|
|
expect(INBOX_MAX_UPLOAD_BYTES).toBeGreaterThan(HOSTED_REQUEST_BODY_LIMIT_BYTES)
|
|
})
|
|
|
|
it('applies on every deployment: self-hosted has the same route cap', () => {
|
|
vi.stubEnv('NEXT_PUBLIC_SELF_HOSTED', 'true')
|
|
expect(exceedsInboxUploadLimit(INBOX_MAX_UPLOAD_BYTES + 1)).toBe(true)
|
|
expect(exceedsInboxUploadLimit(INBOX_MAX_UPLOAD_BYTES)).toBe(false)
|
|
})
|
|
|
|
it('names the actual size and the inbox ceiling, not the hosted one', () => {
|
|
const message = inboxTooLargeMessage(12 * 1024 * 1024)
|
|
expect(message).toContain('12,0 MB')
|
|
expect(message).toContain(formatMegabytes(INBOX_MAX_UPLOAD_BYTES))
|
|
expect(message).not.toContain(formatMegabytes(HOSTED_MAX_UPLOAD_BYTES))
|
|
})
|
|
})
|
|
|
|
describe('shrinkImageForUpload', () => {
|
|
function fakeFile(size: number, type: string): File {
|
|
return { size, type, name: 'kvitto.heic', lastModified: 0 } as File
|
|
}
|
|
|
|
it('returns a file that already fits untouched', async () => {
|
|
const file = fakeFile(1024, 'image/jpeg')
|
|
expect(await shrinkImageForUpload(file)).toBe(file)
|
|
})
|
|
|
|
it('returns a PDF untouched: there is nothing a canvas can do with it', async () => {
|
|
const file = fakeFile(9 * 1024 * 1024, 'application/pdf')
|
|
expect(await shrinkImageForUpload(file)).toBe(file)
|
|
})
|
|
|
|
// No createImageBitmap outside a browser (and none for HEIC outside Safari):
|
|
// the caller falls back to the size message rather than a silent failure.
|
|
it('returns the original where the browser cannot decode it', async () => {
|
|
const file = fakeFile(9 * 1024 * 1024, 'image/heic')
|
|
expect(await shrinkImageForUpload(file)).toBe(file)
|
|
})
|
|
})
|