77 lines
3.1 KiB
JavaScript
77 lines
3.1 KiB
JavaScript
import path from "node:path";
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import { TENANT_SCOPE_MANIFEST } from "../../prisma/tenant-scope-manifest.mjs";
|
|
import { discoverIndirectTenantModels, parsePrismaSchemaDirectory, validateTenantScopeManifest } from "./prisma-tenant-scope.mjs";
|
|
|
|
function model(model, options = {}) {
|
|
return {
|
|
model,
|
|
table: model,
|
|
schema: options.schema ?? "fixture.prisma",
|
|
tenantField: options.tenantField ?? null,
|
|
owningRelationTargets: options.owningRelationTargets ?? [],
|
|
};
|
|
}
|
|
|
|
describe("tenant scope manifest", () => {
|
|
it("classifies every current Prisma model and every discovered indirect boundary", () => {
|
|
const models = parsePrismaSchemaDirectory(path.join(process.cwd(), "prisma", "schema"));
|
|
const result = validateTenantScopeManifest(models, TENANT_SCOPE_MANIFEST);
|
|
|
|
expect(result.errors).toEqual([]);
|
|
expect(result.modelCount).toBe(377);
|
|
expect(result.classifiedCount).toBe(377);
|
|
expect(result.scopeCounts).toEqual({
|
|
"tenant-required": 195,
|
|
"tenant-or-global": 29,
|
|
"derived-child": 36,
|
|
"tenant-root": 1,
|
|
"global-catalog": 11,
|
|
"global-shared": 16,
|
|
"platform-admin": 89,
|
|
});
|
|
expect(result.indirectModels.size).toBe(37);
|
|
expect(result.indirectModels).toContain("Registration");
|
|
expect(result.indirectModels).toContain("KnowledgeBaseArticleDownvotes");
|
|
});
|
|
|
|
it("reports an unclassified schema model", () => {
|
|
const models = [model("Known", { tenantField: { nullable: false } }), model("NewModel")];
|
|
const manifest = { Known: { scope: "tenant-required", enforcement: "tenant-policy" } };
|
|
|
|
const result = validateTenantScopeManifest(models, manifest);
|
|
|
|
expect(result.errors).toContain("fixture.prisma: NewModel is missing from the tenant scope manifest");
|
|
});
|
|
|
|
it("detects recursively derived FK children classified as global", () => {
|
|
const models = [
|
|
model("TenantRow", { tenantField: { nullable: false } }),
|
|
model("Child", { owningRelationTargets: ["TenantRow"] }),
|
|
model("GrandChild", { owningRelationTargets: ["Child"] }),
|
|
];
|
|
const manifest = {
|
|
TenantRow: { scope: "tenant-required", enforcement: "tenant-policy" },
|
|
Child: { scope: "derived-child", enforcement: "join-policy" },
|
|
GrandChild: { scope: "global-shared", enforcement: "none" },
|
|
};
|
|
|
|
const indirect = discoverIndirectTenantModels(models, manifest);
|
|
const result = validateTenantScopeManifest(models, manifest);
|
|
|
|
expect(indirect).toEqual(new Set(["Child", "GrandChild"]));
|
|
expect(result.errors).toContain("fixture.prisma: GrandChild has an owning FK path to tenant data but is classified global-shared");
|
|
});
|
|
|
|
it("requires a reason for enforcement overrides", () => {
|
|
const models = [model("Hybrid", { tenantField: { nullable: true } })];
|
|
const manifest = { Hybrid: { scope: "tenant-or-global", enforcement: "none" } };
|
|
|
|
const result = validateTenantScopeManifest(models, manifest);
|
|
|
|
expect(result.errors).toContain("Hybrid overrides tenant-or-global enforcement without an explicit reason");
|
|
});
|
|
});
|