T-HUB/apps/archive-bot/src/names.test.ts
glass beadian d58db63fe6 Add group cloud: files index api, web UI, archive sync bot
The Telegram-like group file contract (packages/core shared-files.ts) becomes
a working subsystem, built in three parallel tracks:

API (NestJS): migration 029 group_files (chat_media_archive metadata-only
index with partial replay-unique + RLS, group_file_shares with sha256 token
persistence, archive_sync_state/outbox durable-claim seam incl. a unique
claim_key index), group-files module (list/search with cursor pagination,
direct upload ≤20 MiB streamed+hashed to oCIS WebDAV, download with ACL +
retention rechecks, share links with expiry, retention modes incl.
legalHold) — RBAC group-membership authz + audit chain events.

Web: Files section per group — group picker, drag-drop upload (20 MiB cap),
search + MIME/origin filters, cursor pagination, download via authenticated
blob fetch, share dialog with expiry + shown-once token, admin retention
dialog; bilingual EN/ES (wiki i18n pattern); nav sidebar/palette/bottom-nav
wiring; MIME classes aligned to the DB CHECK (document/other).

Archive bot (apps/archive-bot): plain sync-loop Matrix bot mirroring
attachments from explicitly-mapped unencrypted archive rooms — raw /sync
with durable since-cursor, outbox-claimed ingestion (hash-verified stream,
no temp files), magic-byte MIME sniffing, safe-name normalization, WebDAV
upload, indexed + replied with link, redaction→unavailable; health endpoint;
Dockerfile. Tests 74/74, typecheck clean across api/web/bot.
2026-10-11 08:21:11 +00:00

40 lines
1.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, expect, it } from "vitest";
import { MAX_SAFE_NAME_BYTES, safeFileName } from "./names.js";
describe("safeFileName", () => {
it("keeps ordinary names", () => {
expect(safeFileName("Meeting notes.pdf")).toBe("Meeting notes.pdf");
});
it("strips directories and traversal", () => {
expect(safeFileName("../../etc/passwd")).toBe("passwd");
expect(safeFileName("C:\\Users\\x\\evil.exe")).toBe("evil.exe");
expect(safeFileName("..")).toBe("attachment");
});
it("removes control/bidi characters and reserved punctuation", () => {
expect(safeFileName("a\u0000b\u202ecod.exe")).toBe("abcod.exe");
expect(safeFileName('we<ir>d:"na|me?*.txt')).toBe("we_ir_d_na_me_.txt");
expect(safeFileName(" .hidden ")).toBe("hidden");
});
it("NFKC-normalises and collapses whitespace", () => {
expect(safeFileName("full \t width.PNG")).toBe("full width.png");
});
it("guards Windows device names", () => {
expect(safeFileName("CON.txt")).toBe("_CON.txt");
});
it("appends the sniffed extension only when none exists", () => {
expect(safeFileName("photo", "jpg")).toBe("photo.jpg");
expect(safeFileName("photo.jpeg", "jpg")).toBe("photo.jpeg");
expect(safeFileName("", "pdf")).toBe("attachment.pdf");
});
it("bounds length in UTF-8 bytes while preserving the extension", () => {
const name = safeFileName(`${"ü".repeat(200)}.pdf`);
expect(Buffer.byteLength(name, "utf8")).toBeLessThanOrEqual(MAX_SAFE_NAME_BYTES);
expect(name.endsWith(".pdf")).toBe(true);
});
});