Files
accounted/lib/documents/__tests__/upload-size.test.ts
T
Jakob Wennberg 749f90fe62 feat(inbox): direct-to-storage upload for files over the hosted body limit (#1551) (#2030)
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
2026-08-30 11:55:42 +02:00

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)
})
})