62 lines
3.4 KiB
TypeScript
62 lines
3.4 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { describe, expect, it } from "vitest";
|
|
import { resolveRepoPath } from "~/utils/testing/repoPaths";
|
|
|
|
const fileService = readFileSync(resolveRepoPath("apps/web/app/utils/integrations/fileService.server.ts"), "utf8");
|
|
const searchService = readFileSync(resolveRepoPath("apps/web/app/services/search/globalSearch.server.ts"), "utf8");
|
|
const mediaRoute = readFileSync(resolveRepoPath("apps/web/app/routes/app.$tenant/media.$fileId.$variant.tsx"), "utf8");
|
|
const browserRoute = readFileSync(resolveRepoPath("apps/web/app/routes/app.$tenant/files._index.tsx"), "utf8");
|
|
|
|
function section(source: string, start: string, end: string): string {
|
|
return source.split(start)[1]?.split(end)[0] ?? "";
|
|
}
|
|
|
|
describe("central file visibility boundary", () => {
|
|
it("routes detail visibility through the authoritative scope without owner/read_all fast paths", () => {
|
|
const detailGuard = section(fileService, "async function getVisibleFileOrThrowWithTx", "/** Load a single file");
|
|
|
|
expect(detailGuard).toContain("fileReadScopeWhereWithTx(tx, tenantId, ctx, [fileId], queryState)");
|
|
expect(detailGuard.match(/tx\.file\.findFirst/g)).toHaveLength(1);
|
|
});
|
|
|
|
it("shares one project-principal query state across detail and linked-target visibility", () => {
|
|
const linkedDetail = section(fileService, "export async function getVisibleFileWithLinks", "export type ListFilesFilter");
|
|
|
|
expect(linkedDetail).toContain("const queryState = createFileVisibilityQueryState()");
|
|
expect(linkedDetail).toContain("getVisibleFileOrThrowWithTx(tx, tenantId, fileId, ctx, queryState)");
|
|
expect(linkedDetail).toContain("filterVisibleFileLinksWithTx(tx, tenantId, links, ctx, queryState)");
|
|
});
|
|
|
|
it("routes the browser rows and filter metadata through the same scope", () => {
|
|
const browser = section(fileService, "export async function loadFileBrowserData", "// --- Bulk ZIP");
|
|
|
|
expect(browser.match(/fileReadScopeWhereWithTx\(tx, input\.tenantId, input\.ctx\)/g)).toHaveLength(1);
|
|
expect(browser).toContain("listFilesForBrowserWithTx(tx, input, visibility)");
|
|
expect(browser).toContain("loadFileFilterRowsWithTx(tx, input.tenantId, visibility)");
|
|
expect(browserRoute).toContain("await loadFileBrowserData({");
|
|
expect(browserRoute).not.toContain("listFileFilterOptions");
|
|
});
|
|
|
|
it("routes bulk ZIP candidate selection through the same scope", () => {
|
|
const bulkZip = section(fileService, "export async function collectFilesForZip", "/** Ensure a zip entry name is unique");
|
|
|
|
expect(bulkZip).toContain("fileReadScopeWhereWithTx(tx, input.tenantId, input.ctx, ids)");
|
|
});
|
|
|
|
it("routes SQL search and Meilisearch revalidation through the same scope", () => {
|
|
const sqlFiles = section(searchService, "async function searchFiles", "type TaskVisibilityCondition");
|
|
const revalidation = section(searchService, 'case "file": {', 'case "task": {');
|
|
|
|
expect(sqlFiles).toContain("fileReadScopeWhereWithTx(database, ctx.tenantId, ctx)");
|
|
expect(revalidation).toContain("fileReadScopeWhereWithTx(input.database, input.ctx.tenantId, input.ctx, ids)");
|
|
});
|
|
|
|
it("requires the visibility-checked file loader before media signing", () => {
|
|
const visibility = mediaRoute.indexOf("await getVisibleFileOrThrow(ctx.tenantId, fileId, ctx)");
|
|
const signing = mediaRoute.indexOf("await createMediaDownloadUrl");
|
|
|
|
expect(visibility).toBeGreaterThan(-1);
|
|
expect(signing).toBeGreaterThan(visibility);
|
|
});
|
|
});
|