mirror of
https://github.com/willchen96/mike.git
synced 2026-07-26 23:51:08 +02:00
Ported from the amal66 fork (see index Open-Legal-Products/mike#205) onto current main, adapted to this repo's backend/ layout (apps/api/src/lib -> backend/src/lib), plus a v8 coverage ratchet. Suites ported (51 new tests, verified locally): - access.test.ts (7): owner/shared/private project access, doc access, review sharing, document-ID filtering. Dropped the fork's "org RBAC" describe block (7 cases) — org_id/org_members multi-tenancy and the role/canManage fields do not exist in this repo's access.ts. - storage.test.ts (25): filename normalization/sanitization, RFC 5987 encoding, Content-Disposition, storage key helpers. Dropped the fork's vi.mock of lib/env — this repo has no env module; storage reads process.env directly and the tested helpers are pure. - userApiKeys.test.ts (10): normalizeApiKeyProvider + hasEnvApiKey. Added a beforeEach env clear so shell-exported API keys can't leak into assertions. - chatTypes.test.ts (9): resolveDoc/resolveDocLabel, which live in lib/chat/types.ts here (the fork's lib/chatTools.ts equivalent). Dropped generateSpotlightNonce cases (2) — no such export here. Suites dropped entirely (subject not present in this repo): - upload.test.ts — tested hasMagicBytes; this repo's lib/upload.ts is only the multer middleware and exports no magic-byte checker. - userSettings.test.ts — tested resolveTabularModel (fork-only keyed- provider fallback); this repo resolves tabular_model via resolveModel with a static default. Coverage ratchet: vitest.config.mts adds v8 coverage over src/lib/** with floors measured against this tree (2.58% stmts, 2.00% branches, 4.61% funcs, 2.58% lines -> floors 2/2/4/2). Full suite: 5 files, 63 tests passing (incl. the pre-existing 12 in downloadTokens.test.ts); npm run test:coverage and npm run build both pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
149 lines
5.1 KiB
TypeScript
149 lines
5.1 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
|
|
import {
|
|
normalizeDownloadFilename,
|
|
sanitizeDispositionFilename,
|
|
encodeRFC5987,
|
|
buildContentDisposition,
|
|
storageKey,
|
|
pdfStorageKey,
|
|
generatedDocKey,
|
|
versionStorageKey,
|
|
} from "../storage";
|
|
|
|
describe("normalizeDownloadFilename", () => {
|
|
it("trims surrounding whitespace", () => {
|
|
expect(normalizeDownloadFilename(" file.pdf ")).toBe("file.pdf");
|
|
});
|
|
|
|
it("falls back to 'download' for empty string", () => {
|
|
expect(normalizeDownloadFilename("")).toBe("download");
|
|
expect(normalizeDownloadFilename(" ")).toBe("download");
|
|
});
|
|
|
|
it("replaces control characters with underscore", () => {
|
|
expect(normalizeDownloadFilename("file\x00name.pdf")).toBe("file_name.pdf");
|
|
expect(normalizeDownloadFilename("file\x1fname.pdf")).toBe("file_name.pdf");
|
|
});
|
|
|
|
it("replaces forward and backward slashes with underscore", () => {
|
|
expect(normalizeDownloadFilename("dir/file.pdf")).toBe("dir_file.pdf");
|
|
expect(normalizeDownloadFilename("dir\\file.pdf")).toBe("dir_file.pdf");
|
|
});
|
|
|
|
it("preserves normal filenames unchanged", () => {
|
|
expect(normalizeDownloadFilename("Contract v2 (Final).pdf")).toBe(
|
|
"Contract v2 (Final).pdf",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("sanitizeDispositionFilename", () => {
|
|
it("strips double-quote characters", () => {
|
|
expect(sanitizeDispositionFilename('file"name.pdf')).toBe("file_name.pdf");
|
|
});
|
|
|
|
it("strips backslash characters", () => {
|
|
expect(sanitizeDispositionFilename("file\\name.pdf")).toBe("file_name.pdf");
|
|
});
|
|
|
|
it("strips non-ASCII characters", () => {
|
|
expect(sanitizeDispositionFilename("filéname.pdf")).toBe("fil_name.pdf");
|
|
});
|
|
|
|
it("still applies normalizeDownloadFilename rules first", () => {
|
|
expect(sanitizeDispositionFilename(" ")).toBe("download");
|
|
});
|
|
});
|
|
|
|
describe("encodeRFC5987", () => {
|
|
it("encodes spaces as %20", () => {
|
|
expect(encodeRFC5987("hello world")).toBe("hello%20world");
|
|
});
|
|
|
|
it("encodes single-quote as %27", () => {
|
|
expect(encodeRFC5987("it's")).toContain("%27");
|
|
});
|
|
|
|
it("encodes ( and ) as %28 and %29", () => {
|
|
const result = encodeRFC5987("a(b)c");
|
|
expect(result).toContain("%28");
|
|
expect(result).toContain("%29");
|
|
});
|
|
|
|
it("encodes * as %2A", () => {
|
|
expect(encodeRFC5987("a*b")).toContain("%2A");
|
|
});
|
|
|
|
it("leaves safe ASCII characters unencoded", () => {
|
|
expect(encodeRFC5987("file.pdf")).toBe("file.pdf");
|
|
});
|
|
});
|
|
|
|
describe("buildContentDisposition", () => {
|
|
it("produces an attachment header with ASCII filename", () => {
|
|
const header = buildContentDisposition("attachment", "contract.pdf");
|
|
expect(header).toMatch(/^attachment;/);
|
|
expect(header).toContain('filename="contract.pdf"');
|
|
expect(header).toContain("filename*=UTF-8''contract.pdf");
|
|
});
|
|
|
|
it("produces an inline header", () => {
|
|
const header = buildContentDisposition("inline", "preview.pdf");
|
|
expect(header).toMatch(/^inline;/);
|
|
});
|
|
|
|
it("encodes unicode filename in filename* param", () => {
|
|
const header = buildContentDisposition("attachment", "Ünïcödé.pdf");
|
|
expect(header).toContain("filename*=UTF-8''");
|
|
expect(header).not.toContain("Ü");
|
|
});
|
|
});
|
|
|
|
describe("storageKey", () => {
|
|
it("includes userId, docId, and correct extension", () => {
|
|
const key = storageKey("user1", "doc1", "contract.pdf");
|
|
expect(key).toBe("documents/user1/doc1/source.pdf");
|
|
});
|
|
|
|
it("falls back to .bin for extensions longer than 16 chars", () => {
|
|
const key = storageKey("user1", "doc1", "file.toolongextension1234");
|
|
expect(key).toBe("documents/user1/doc1/source.bin");
|
|
});
|
|
|
|
it("falls back to .bin when no extension", () => {
|
|
const key = storageKey("user1", "doc1", "noextension");
|
|
expect(key).toBe("documents/user1/doc1/source.bin");
|
|
});
|
|
});
|
|
|
|
describe("pdfStorageKey", () => {
|
|
it("places PDF in the correct path with stem", () => {
|
|
const key = pdfStorageKey("user1", "doc1", "contract");
|
|
expect(key).toBe("documents/user1/doc1/contract.pdf");
|
|
});
|
|
});
|
|
|
|
describe("generatedDocKey", () => {
|
|
it("uses generated/ prefix and .docx extension for docx files", () => {
|
|
const key = generatedDocKey("user1", "doc1", "output.docx");
|
|
expect(key).toBe("generated/user1/doc1/generated.docx");
|
|
});
|
|
|
|
it("falls back to .docx for extensions longer than 16 chars", () => {
|
|
const key = generatedDocKey("user1", "doc1", "output.toolongextension1234");
|
|
expect(key).toBe("generated/user1/doc1/generated.docx");
|
|
});
|
|
});
|
|
|
|
describe("versionStorageKey", () => {
|
|
it("includes userId, docId, versionSlug, and extension", () => {
|
|
const key = versionStorageKey("user1", "doc1", "v2", "contract.pdf");
|
|
expect(key).toBe("documents/user1/doc1/versions/v2.pdf");
|
|
});
|
|
|
|
it("falls back to .bin for unknown extensions", () => {
|
|
const key = versionStorageKey("user1", "doc1", "v2", "file");
|
|
expect(key).toBe("documents/user1/doc1/versions/v2.bin");
|
|
});
|
|
});
|