wacrm/src/lib/storage/upload-media.test.ts
Arnas Donauskas 96e7a56e3e
fix(inbox): harden media send — GC orphans, per-type caps, voice cap
Addresses senior-review findings on the media-send feature:

- Storage lifecycle: uploads are GC'd on the abandonment paths
  (discarded draft, failed/errored send, unmount with a staged draft,
  and replacing one draft with another) via a new deleteAccountMedia
  helper. Previously every cancel/failure orphaned a public object.
- Per-kind size caps (image 5MB, video/audio/document 16MB) mirror
  Meta's limits and are enforced BEFORE upload, so a too-large image no
  longer uploads then fails at the Meta call.
- Voice notes auto-stop at a 5-min cap and always render as a voice note
  (transcode anything that isn't already OGG/Opus — Safari MP4 included),
  not just Chromium WebM.
- Send route rejects unknown message_type and captions over Meta's 1024
  cap up front; transcode route guards its output size too.
- DraftPreview hoisted to a stable module-scope component so the caption
  input no longer remounts (and drops focus) on each keystroke.

Tests: per-kind cap assertions added (418 total). typecheck/lint/build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:28:19 +03:00

47 lines
1.8 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { buildMediaPath, MEDIA_MAX_BYTES_BY_KIND } from "./upload-media";
const ACCOUNT = "11111111-2222-3333-4444-555555555555";
describe("buildMediaPath", () => {
it("namespaces under account-<id> so RLS write policies match", () => {
const path = buildMediaPath(ACCOUNT, "photo.png", 1700000000000);
expect(path).toBe(`account-${ACCOUNT}/1700000000000-photo.png`);
expect(path.split("/")[0]).toBe(`account-${ACCOUNT}`);
});
it("lower-cases the extension and sanitizes the basename", () => {
const path = buildMediaPath(ACCOUNT, "My Invoice (final).PDF", 1700000000000);
expect(path).toBe(`account-${ACCOUNT}/1700000000000-My_Invoice_final_.pdf`);
});
it("caps the basename at 40 chars", () => {
const long = "a".repeat(100) + ".png";
const path = buildMediaPath(ACCOUNT, long, 1700000000000);
const base = path.split("/")[1].replace("1700000000000-", "").replace(".png", "");
expect(base.length).toBe(40);
});
it("falls back to 'file' / 'bin' for a nameless input", () => {
const path = buildMediaPath(ACCOUNT, "", 1700000000000);
expect(path).toBe(`account-${ACCOUNT}/1700000000000-file.bin`);
});
it("defaults the extension to bin when there is none", () => {
const path = buildMediaPath(ACCOUNT, "README", 1700000000000);
expect(path).toBe(`account-${ACCOUNT}/1700000000000-README.bin`);
});
});
describe("MEDIA_MAX_BYTES_BY_KIND", () => {
it("caps images at Meta's tighter 5 MB limit", () => {
expect(MEDIA_MAX_BYTES_BY_KIND.image).toBe(5 * 1024 * 1024);
});
it("caps video/audio/document at the 16 MB bucket limit", () => {
expect(MEDIA_MAX_BYTES_BY_KIND.video).toBe(16 * 1024 * 1024);
expect(MEDIA_MAX_BYTES_BY_KIND.audio).toBe(16 * 1024 * 1024);
expect(MEDIA_MAX_BYTES_BY_KIND.document).toBe(16 * 1024 * 1024);
});
});