Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/tenant-isolation-report.test.ts
T

178 lines
7.3 KiB
TypeScript

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();
});
});