Files
siftlode/frontend/src/lib/api/downloads.contract.test.ts
T
peter 3a688fa0f5 refactor(capture): the unit's tables move into downloads/models
The nine Capture tables (files, jobs, folders, shares, links, quota, cookies, profiles, library
resume points) leave app/models.py for the unit's own module; app.tables registers every unit's
tables for the migrations, the test database and the ownership check. No schema change.
2026-10-18 23:45:53 +02:00

71 lines
3.8 KiB
TypeScript

import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
// A bound that has to hold in two languages at once. The backend truncates a user-supplied display
// name to `DISPLAY_TEXT_MAX` on its way into a varchar column of the same width (the columns are
// declared with it); the upload dialog stops the user typing at the same number, so the truncation
// is never a silent surprise. Neither side can see the other's copy, so the gate reads both.
//
// Widening one side alone is the failure this catches: the column and the Python constant move
// together, the input keeps clipping at the old number, and the field simply stops accepting
// keystrokes with no message — visible only by reading two files in two languages.
//
// Read as TEXT, like `i18n/backendIds.contract.test.ts` does, rather than by importing the module:
// the api slice pulls in i18n, which touches localStorage at load and would drag a DOM into a check
// that is pure string comparison.
const src = (u: string) => readFileSync(new URL(u, import.meta.url), "utf-8");
describe("display-name bound", () => {
it("matches the backend's DISPLAY_TEXT_MAX", () => {
const backend = src("../../../../backend/app/models.py").match(
/^DISPLAY_TEXT_MAX\s*=\s*(\d+)$/m,
);
const frontend = src("./downloads.ts").match(/^export const DISPLAY_NAME_MAX = (\d+);$/m);
expect(backend, "backend/app/models.py no longer defines DISPLAY_TEXT_MAX").toBeTruthy();
expect(frontend, "lib/api/downloads.ts no longer exports DISPLAY_NAME_MAX").toBeTruthy();
expect(Number(frontend![1])).toBe(Number(backend![1]));
});
it("is the width the columns are actually declared with", () => {
// The mutation guard for the check above: both constants could agree on a number neither
// column uses. `String(DISPLAY_TEXT_MAX)` is how the three display columns are declared, so a
// literal creeping back into one of them fails here. `User.display_name` is a core table; the
// job's two are Capture's.
const models =
src("../../../../backend/app/models.py") + src("../../../../backend/app/downloads/models.py");
const declared = [...models.matchAll(/display_(?:name|uploader): Mapped.*?String\((.*?)\)/g)];
expect(declared).toHaveLength(3);
for (const m of declared) expect(m[1]).toBe("DISPLAY_TEXT_MAX");
});
});
// The viewer reads what the server decides (backend downloads/preview.py): the preview kinds it may
// be told and the header that says a text was cut. Each side names them in its own language, so the
// gate reads both: a kind the server adds that the viewer does not know would fall to "no preview".
describe("file preview", () => {
it("knows the kinds the server hands out", () => {
const backend = src("../../../../backend/app/downloads/preview.py").match(
/^PreviewKind = Literal\[(.*)\]$/m,
);
const frontend = src("./downloads.ts").match(/^export type FilePreviewKind = (.*);$/m);
expect(backend, "downloads/preview.py no longer defines PreviewKind").toBeTruthy();
expect(frontend, "lib/api/downloads.ts no longer exports FilePreviewKind").toBeTruthy();
const kinds = (s: string, sep: string) =>
s
.split(sep)
.map((k) => k.trim().replace(/"/g, ""))
.sort();
expect(kinds(frontend![1]!, "|")).toEqual(kinds(backend![1]!, ","));
});
it("reads the header the server cuts a text with", () => {
const backend = src("../../../../backend/app/routes/files.py").match(
/^TRUNCATED_HEADER = "(.*)"$/m,
);
const frontend = src("./files.ts").match(/^const PREVIEW_TRUNCATED_HEADER = "(.*)";$/m);
expect(backend, "routes/files.py no longer defines TRUNCATED_HEADER").toBeTruthy();
expect(frontend, "lib/api/files.ts no longer defines PREVIEW_TRUNCATED_HEADER").toBeTruthy();
expect(frontend![1]).toBe(backend![1]);
});
});