import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { checkTenantIsolationReport, collectTenantIsolationEvidence, discoverIsolationTestPaths, renderTenantIsolationReport, REPORT_PATH, runTenantIsolationReportCli, S04_SUMMARY_PATH, writeTenantIsolationReport, } from "./tenant-isolation-report"; let tempRoot: string; function writeFixture(relativePath: string, content: string): void { const absolutePath = path.join(tempRoot, relativePath); fs.mkdirSync(path.dirname(absolutePath), { recursive: true }); fs.writeFileSync(absolutePath, content, "utf8"); } function seedIsolationFixtures(): void { writeFixture( S04_SUMMARY_PATH, `--- verification_result: passed with apiKey: "s04-api-key-should-not-leak" patterns_established: - tenant_a_session keeps row_iso_secret_001 inside the resolver tenant boundary. - relationship_iso_parent_001 and relationship_iso_child_001 require parent and child authorization. --- **S04 summary for tenant_a_session@example.com and token = "s04-token-secret-value";** `, ); writeFixture( "apps/web/app/test/isolation/admin-routes.test.ts", `it("admin permission denies tenant_admin_secret@example.com bearer_token_secret_001", () => { const tenantId = "tenant_iso_alpha"; const token = "admin-token-secret-value"; expect(permission).toEqual("denied"); }); `, ); writeFixture( "apps/web/app/test/isolation/fixtures/user-session-isolation.test.ts", `test("returns 403 for foreign tenant row_iso_999 from tenant_b", () => { const userId = "user_iso_beta"; const apiKey = "api_key_entity_secret_001"; expect(status).toBe(403); }); it("skips relationship resolver for relationship_iso_parent_abc child relationship_iso_child_def", () => { const session = "session_iso_gamma"; const logId = "log_iso_delta"; expect(repository.update).not.toHaveBeenCalled(); }); `, ); } beforeEach(() => { tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "tenant-isolation-report-")); seedIsolationFixtures(); }); afterEach(() => { vi.restoreAllMocks(); fs.rmSync(tempRoot, { force: true, recursive: true }); }); describe("tenant isolation report renderer", () => { it("renders deterministic report sections and evidence labels", () => { const evidence = collectTenantIsolationEvidence(tempRoot); const firstReport = renderTenantIsolationReport(evidence); const secondReport = renderTenantIsolationReport(collectTenantIsolationEvidence(tempRoot)); expect(secondReport).toBe(firstReport); expect(firstReport).toContain("## Scope"); expect(firstReport).toContain("## Source Evidence"); expect(firstReport).toContain("## Boundary Matrix"); expect(firstReport).toContain("| Source | Test count | Checked access boundaries | Deny-path evidence |"); expect(firstReport).toContain("## Prior Slice Evidence"); expect(firstReport).toContain("## Deny-Path Coverage"); expect(firstReport).toContain("## Redaction Rules"); expect(firstReport).toContain("## Verification Commands"); expect(firstReport).toContain("## Redaction Contract"); expect(firstReport).toContain("admin permission gate"); expect(firstReport).toContain("tenant and user scoped resolver"); expect(firstReport).toContain("relationship parent/child authorization"); expect(firstReport).toContain("403 response for unauthorized tenant access"); expect(firstReport).toContain("denied path skips data mutation or lookup"); expect(firstReport).toContain("pnpm exec vitest run scripts/tenant-isolation-report.test.ts"); expect(firstReport).toContain("pnpm exec tsx scripts/tenant-isolation-report.ts --check"); expect(firstReport).toContain("pnpm exec tsx scripts/docs-link-check.ts"); }); it("redacts concrete fixture identifiers, credentials, and source snippets while keeping categories", () => { const report = renderTenantIsolationReport(collectTenantIsolationEvidence(tempRoot)); expect(report).toContain("tenant A"); expect(report).toContain("tenant B"); expect(report).toContain("API credential"); expect(report).toContain("row record"); expect(report).toContain("relationship record"); expect(report).toContain("user session"); expect(report).not.toContain("tenant_admin_secret@example.com"); expect(report).not.toContain("tenant_a_session@example.com"); expect(report).not.toContain("tenant_iso_alpha"); expect(report).not.toContain("tenant_b"); expect(report).not.toContain("row_iso_999"); expect(report).not.toContain("relationship_iso_parent_abc"); expect(report).not.toContain("relationship_iso_child_def"); expect(report).not.toContain("user_iso_beta"); expect(report).not.toContain("session_iso_gamma"); expect(report).not.toContain("log_iso_delta"); expect(report).not.toContain("admin-token-secret-value"); expect(report).not.toContain("api_key_entity_secret_001"); expect(report).not.toContain("s04-api-key-should-not-leak"); expect(report).not.toContain("s04-token-secret-value"); expect(report).not.toContain("expect(repository.update).not.toHaveBeenCalled();"); expect(report).not.toContain("const tenantId"); }); it("reports missing evidence files with concise diagnostics", () => { fs.rmSync(path.join(tempRoot, "apps/web/app/test/isolation/fixtures/user-session-isolation.test.ts")); expect(() => collectTenantIsolationEvidence(tempRoot)).toThrow( "missing expected isolation evidence file: apps/web/app/test/isolation/fixtures/user-session-isolation.test.ts", ); }); it("discovers new isolation tests under curated isolation roots", () => { writeFixture( "apps/api/src/isolation/api-key-entity-rows.test.ts", `it("returns 403 for cross-tenant API-key row access", () => { expect(status).toBe(403); }); `, ); expect(discoverIsolationTestPaths(tempRoot)).toEqual([ "apps/api/src/isolation/api-key-entity-rows.test.ts", "apps/web/app/test/isolation/admin-routes.test.ts", "apps/web/app/test/isolation/fixtures/user-session-isolation.test.ts", ]); const evidence = collectTenantIsolationEvidence(tempRoot); expect(evidence.generatedFrom).toContain("apps/api/src/isolation/api-key-entity-rows.test.ts"); }); it("detects stale generated output in check mode without leaking fixture content", () => { const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true); const stdoutWrite = vi.spyOn(process.stdout, "write").mockImplementation(() => true); writeTenantIsolationReport(tempRoot); fs.writeFileSync(path.join(tempRoot, REPORT_PATH), "# stale report with tenant_iso_alpha and api_key_entity_secret_001\n", "utf8"); expect(checkTenantIsolationReport(tempRoot)).toEqual({ ok: false, reportPath: path.join(tempRoot, REPORT_PATH), failures: [`stale generated report: ${REPORT_PATH}`], }); expect(runTenantIsolationReportCli(["--root=" + tempRoot, "--check"])).toBe(1); const stderrOutput = stderrWrite.mock.calls.map((call) => String(call[0])).join(""); expect(stderrOutput).toContain(`FAIL stale generated report: ${REPORT_PATH}`); expect(stderrOutput).not.toContain("tenant_iso_alpha"); expect(stderrOutput).not.toContain("api_key_entity_secret_001"); expect(stdoutWrite).not.toHaveBeenCalled(); }); });