cedar-mcp-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +12 -0
- package/.github/workflows/ci.yml +31 -0
- package/.github/workflows/release.yml +42 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +241 -0
- package/CONTRIBUTING.md +83 -0
- package/LICENSE +182 -0
- package/README.md +1635 -0
- package/SECURITY.md +37 -0
- package/dist/http-server.d.ts +61 -0
- package/dist/http-server.d.ts.map +1 -0
- package/dist/http-server.js +194 -0
- package/dist/http-server.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +270 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/policy-ast.d.ts +49 -0
- package/dist/parser/policy-ast.d.ts.map +1 -0
- package/dist/parser/policy-ast.js +311 -0
- package/dist/parser/policy-ast.js.map +1 -0
- package/dist/prompts/index.d.ts +38 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +172 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/resources/ref-resolver.d.ts +23 -0
- package/dist/resources/ref-resolver.d.ts.map +1 -0
- package/dist/resources/ref-resolver.js +128 -0
- package/dist/resources/ref-resolver.js.map +1 -0
- package/dist/resources/store-manager.d.ts +64 -0
- package/dist/resources/store-manager.d.ts.map +1 -0
- package/dist/resources/store-manager.js +221 -0
- package/dist/resources/store-manager.js.map +1 -0
- package/dist/server.d.ts +18 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +539 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/advise/avp-rules.d.ts +49 -0
- package/dist/tools/advise/avp-rules.d.ts.map +1 -0
- package/dist/tools/advise/avp-rules.js +59 -0
- package/dist/tools/advise/avp-rules.js.map +1 -0
- package/dist/tools/advise/cedar-patterns.d.ts +24 -0
- package/dist/tools/advise/cedar-patterns.d.ts.map +1 -0
- package/dist/tools/advise/cedar-patterns.js +57 -0
- package/dist/tools/advise/cedar-patterns.js.map +1 -0
- package/dist/tools/advise/context-builder.d.ts +28 -0
- package/dist/tools/advise/context-builder.d.ts.map +1 -0
- package/dist/tools/advise/context-builder.js +89 -0
- package/dist/tools/advise/context-builder.js.map +1 -0
- package/dist/tools/advise/gotchas.d.ts +15 -0
- package/dist/tools/advise/gotchas.d.ts.map +1 -0
- package/dist/tools/advise/gotchas.js +83 -0
- package/dist/tools/advise/gotchas.js.map +1 -0
- package/dist/tools/advise.d.ts +96 -0
- package/dist/tools/advise.d.ts.map +1 -0
- package/dist/tools/advise.js +258 -0
- package/dist/tools/advise.js.map +1 -0
- package/dist/tools/authorize-batch.d.ts +35 -0
- package/dist/tools/authorize-batch.d.ts.map +1 -0
- package/dist/tools/authorize-batch.js +262 -0
- package/dist/tools/authorize-batch.js.map +1 -0
- package/dist/tools/authorize.d.ts +115 -0
- package/dist/tools/authorize.d.ts.map +1 -0
- package/dist/tools/authorize.js +373 -0
- package/dist/tools/authorize.js.map +1 -0
- package/dist/tools/check-change.d.ts +19 -0
- package/dist/tools/check-change.d.ts.map +1 -0
- package/dist/tools/check-change.js +91 -0
- package/dist/tools/check-change.js.map +1 -0
- package/dist/tools/diff-schema.d.ts +103 -0
- package/dist/tools/diff-schema.d.ts.map +1 -0
- package/dist/tools/diff-schema.js +379 -0
- package/dist/tools/diff-schema.js.map +1 -0
- package/dist/tools/diff-stores.d.ts +45 -0
- package/dist/tools/diff-stores.d.ts.map +1 -0
- package/dist/tools/diff-stores.js +222 -0
- package/dist/tools/diff-stores.js.map +1 -0
- package/dist/tools/explain.d.ts +80 -0
- package/dist/tools/explain.d.ts.map +1 -0
- package/dist/tools/explain.js +187 -0
- package/dist/tools/explain.js.map +1 -0
- package/dist/tools/format.d.ts +11 -0
- package/dist/tools/format.d.ts.map +1 -0
- package/dist/tools/format.js +20 -0
- package/dist/tools/format.js.map +1 -0
- package/dist/tools/generate-sample.d.ts +28 -0
- package/dist/tools/generate-sample.d.ts.map +1 -0
- package/dist/tools/generate-sample.js +568 -0
- package/dist/tools/generate-sample.js.map +1 -0
- package/dist/tools/link-template.d.ts +17 -0
- package/dist/tools/link-template.d.ts.map +1 -0
- package/dist/tools/link-template.js +78 -0
- package/dist/tools/link-template.js.map +1 -0
- package/dist/tools/list-template-links.d.ts +16 -0
- package/dist/tools/list-template-links.d.ts.map +1 -0
- package/dist/tools/list-template-links.js +22 -0
- package/dist/tools/list-template-links.js.map +1 -0
- package/dist/tools/list-templates.d.ts +16 -0
- package/dist/tools/list-templates.d.ts.map +1 -0
- package/dist/tools/list-templates.js +36 -0
- package/dist/tools/list-templates.js.map +1 -0
- package/dist/tools/translate.d.ts +11 -0
- package/dist/tools/translate.d.ts.map +1 -0
- package/dist/tools/translate.js +53 -0
- package/dist/tools/translate.js.map +1 -0
- package/dist/tools/validate-entities.d.ts +19 -0
- package/dist/tools/validate-entities.d.ts.map +1 -0
- package/dist/tools/validate-entities.js +88 -0
- package/dist/tools/validate-entities.js.map +1 -0
- package/dist/tools/validate-schema.d.ts +22 -0
- package/dist/tools/validate-schema.d.ts.map +1 -0
- package/dist/tools/validate-schema.js +89 -0
- package/dist/tools/validate-schema.js.map +1 -0
- package/dist/tools/validate-template.d.ts +18 -0
- package/dist/tools/validate-template.d.ts.map +1 -0
- package/dist/tools/validate-template.js +59 -0
- package/dist/tools/validate-template.js.map +1 -0
- package/dist/tools/validate.d.ts +90 -0
- package/dist/tools/validate.d.ts.map +1 -0
- package/dist/tools/validate.js +351 -0
- package/dist/tools/validate.js.map +1 -0
- package/dist/utils/format-detector.d.ts +49 -0
- package/dist/utils/format-detector.d.ts.map +1 -0
- package/dist/utils/format-detector.js +298 -0
- package/dist/utils/format-detector.js.map +1 -0
- package/examples/README.md +36 -0
- package/examples/abac-multi-tenant/README.md +150 -0
- package/examples/abac-multi-tenant/entities/users-and-docs.json +33 -0
- package/examples/abac-multi-tenant/policies/member-read-internal.cedar +9 -0
- package/examples/abac-multi-tenant/policies/owner-full-access.cedar +9 -0
- package/examples/abac-multi-tenant/policies/premium-share-guard.cedar +9 -0
- package/examples/abac-multi-tenant/policies/private-doc-guard.cedar +13 -0
- package/examples/abac-multi-tenant/run.ts +92 -0
- package/examples/abac-multi-tenant/schema.json +60 -0
- package/examples/api-gateway-path-routing/README.md +154 -0
- package/examples/api-gateway-path-routing/entities/users-and-roles.json +20 -0
- package/examples/api-gateway-path-routing/policies/admin-full-access.cedar +6 -0
- package/examples/api-gateway-path-routing/policies/developer-projects.cedar +14 -0
- package/examples/api-gateway-path-routing/policies/viewer-readonly.cedar +10 -0
- package/examples/api-gateway-path-routing/run.ts +108 -0
- package/examples/api-gateway-path-routing/schema.json +54 -0
- package/examples/rbac-document-management/README.md +167 -0
- package/examples/rbac-document-management/entities/users-and-docs.json +43 -0
- package/examples/rbac-document-management/policies/admin.cedar +6 -0
- package/examples/rbac-document-management/policies/editor.cedar +6 -0
- package/examples/rbac-document-management/policies/top-secret-forbid.cedar +13 -0
- package/examples/rbac-document-management/policies/viewer.cedar +6 -0
- package/examples/rbac-document-management/run.ts +87 -0
- package/examples/rbac-document-management/schema.json +57 -0
- package/package.json +50 -0
- package/src/http-server.ts +239 -0
- package/src/index.ts +294 -0
- package/src/parser/policy-ast.ts +345 -0
- package/src/prompts/README.md +3 -0
- package/src/prompts/index.ts +217 -0
- package/src/resources/ref-resolver.ts +134 -0
- package/src/resources/store-manager.ts +248 -0
- package/src/server.ts +711 -0
- package/src/tools/advise/avp-rules.ts +70 -0
- package/src/tools/advise/cedar-patterns.ts +73 -0
- package/src/tools/advise/context-builder.ts +109 -0
- package/src/tools/advise/gotchas.ts +92 -0
- package/src/tools/advise.ts +366 -0
- package/src/tools/authorize-batch.ts +345 -0
- package/src/tools/authorize.ts +464 -0
- package/src/tools/check-change.ts +119 -0
- package/src/tools/diff-schema.ts +510 -0
- package/src/tools/diff-stores.ts +298 -0
- package/src/tools/explain.ts +278 -0
- package/src/tools/format.ts +33 -0
- package/src/tools/generate-sample.ts +665 -0
- package/src/tools/link-template.ts +109 -0
- package/src/tools/list-template-links.ts +41 -0
- package/src/tools/list-templates.ts +55 -0
- package/src/tools/translate.ts +66 -0
- package/src/tools/validate-entities.ts +125 -0
- package/src/tools/validate-schema.ts +128 -0
- package/src/tools/validate-template.ts +72 -0
- package/src/tools/validate.ts +459 -0
- package/src/utils/format-detector.ts +356 -0
- package/test/fixtures/docmgmt.ts +121 -0
- package/test/fixtures/multitenant.ts +163 -0
- package/test/index.test.ts +96 -0
- package/test/integration/e2e/behavior.test.ts +359 -0
- package/test/integration/e2e/edge-cases.test.ts +365 -0
- package/test/integration/e2e/failure-modes.test.ts +266 -0
- package/test/integration/e2e/protocol.test.ts +252 -0
- package/test/integration/http-smoke.test.ts +588 -0
- package/test/integration/smoke.test.ts +475 -0
- package/test/prompts/prompts.test.ts +173 -0
- package/test/property/properties.test.ts +234 -0
- package/test/resources/ref-resolver.test.ts +186 -0
- package/test/resources/store-manager.test.ts +344 -0
- package/test/setup.test.ts +7 -0
- package/test/tools/advise/avp-rules.test.ts +76 -0
- package/test/tools/advise.test.ts +339 -0
- package/test/tools/authorize-batch.test.ts +459 -0
- package/test/tools/authorize.test.ts +682 -0
- package/test/tools/check-change.test.ts +104 -0
- package/test/tools/cross-fixture.test.ts +170 -0
- package/test/tools/diff-schema.test.ts +355 -0
- package/test/tools/diff-stores.test.ts +291 -0
- package/test/tools/explain.test.ts +221 -0
- package/test/tools/format.test.ts +33 -0
- package/test/tools/generate-sample.test.ts +480 -0
- package/test/tools/link-template.test.ts +90 -0
- package/test/tools/list-templates.test.ts +151 -0
- package/test/tools/translate.test.ts +89 -0
- package/test/tools/validate-entities.test.ts +178 -0
- package/test/tools/validate-schema.test.ts +86 -0
- package/test/tools/validate-template.test.ts +89 -0
- package/test/tools/validate.test.ts +331 -0
- package/test/utils/format-detector.test.ts +518 -0
- package/tsconfig.json +17 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { handleCheckChange } from "../../src/tools/check-change.js";
|
|
3
|
+
|
|
4
|
+
describe("cedar_check_policy_change", () => {
|
|
5
|
+
it("4.1 — principal change requires recreate", async () => {
|
|
6
|
+
const result = await handleCheckChange({
|
|
7
|
+
old_policy: `permit(principal in MyApp::Role::"customer_account_access", action == MyApp::Action::"POST", resource);`,
|
|
8
|
+
new_policy: `permit(principal in MyApp::Role::"customer_management_access", action == MyApp::Action::"POST", resource);`,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
expect(result.can_update_in_place).toBe(false);
|
|
12
|
+
const change = result.changes.find((c) => c.field === "principal");
|
|
13
|
+
expect(change).toBeDefined();
|
|
14
|
+
expect(change!.in_place_allowed).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("4.2 — effect change (permit → forbid) requires recreate", async () => {
|
|
18
|
+
const result = await handleCheckChange({
|
|
19
|
+
old_policy: `permit(principal in MyApp::Role::"temp_access", action, resource);`,
|
|
20
|
+
new_policy: `forbid(principal in MyApp::Role::"temp_access", action, resource);`,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(result.can_update_in_place).toBe(false);
|
|
24
|
+
const change = result.changes.find((c) => c.field === "effect");
|
|
25
|
+
expect(change).toBeDefined();
|
|
26
|
+
expect(change!.in_place_allowed).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("4.3 — resource change requires recreate", async () => {
|
|
30
|
+
const result = await handleCheckChange({
|
|
31
|
+
old_policy: `permit(principal, action == MyApp::Action::"READ", resource == MyApp::Document::"doc-A");`,
|
|
32
|
+
new_policy: `permit(principal, action == MyApp::Action::"READ", resource == MyApp::Document::"doc-B");`,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(result.can_update_in_place).toBe(false);
|
|
36
|
+
const change = result.changes.find((c) => c.field === "resource");
|
|
37
|
+
expect(change).toBeDefined();
|
|
38
|
+
expect(change!.in_place_allowed).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("4.4 — action change is in-place OK", async () => {
|
|
42
|
+
const result = await handleCheckChange({
|
|
43
|
+
old_policy: `permit(principal in MyApp::Role::"editor", action == MyApp::Action::"READ", resource);`,
|
|
44
|
+
new_policy: `permit(principal in MyApp::Role::"editor", action in [MyApp::Action::"READ", MyApp::Action::"WRITE"], resource);`,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(result.can_update_in_place).toBe(true);
|
|
48
|
+
const change = result.changes.find((c) => c.field === "action");
|
|
49
|
+
expect(change).toBeDefined();
|
|
50
|
+
expect(change!.in_place_allowed).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("4.5 — condition change is in-place OK", async () => {
|
|
54
|
+
const result = await handleCheckChange({
|
|
55
|
+
old_policy: `permit(principal, action, resource) when { principal.name == "user_a" && resource.status == "active" };`,
|
|
56
|
+
new_policy: `permit(principal, action, resource) when { principal.name == "user_a" && ["active", "pending"].contains(resource.status) };`,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(result.can_update_in_place).toBe(true);
|
|
60
|
+
const change = result.changes.find((c) => c.field === "conditions");
|
|
61
|
+
expect(change).toBeDefined();
|
|
62
|
+
expect(change!.in_place_allowed).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("4.6 — mixed changes: principal blocks in-place even though action + condition are OK", async () => {
|
|
66
|
+
const result = await handleCheckChange({
|
|
67
|
+
old_policy: `permit(
|
|
68
|
+
principal in MyApp::Role::"old_role",
|
|
69
|
+
action == MyApp::Action::"READ",
|
|
70
|
+
resource
|
|
71
|
+
) when { resource.status == "active" };`,
|
|
72
|
+
new_policy: `permit(
|
|
73
|
+
principal in MyApp::Role::"new_role",
|
|
74
|
+
action in [MyApp::Action::"READ", MyApp::Action::"WRITE"],
|
|
75
|
+
resource
|
|
76
|
+
) when { resource.status == "active" || resource.status == "pending" };`,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(result.can_update_in_place).toBe(false);
|
|
80
|
+
expect(result.changes.find((c) => c.field === "principal")?.in_place_allowed).toBe(false);
|
|
81
|
+
expect(result.changes.find((c) => c.field === "action")?.in_place_allowed).toBe(true);
|
|
82
|
+
expect(result.changes.find((c) => c.field === "conditions")?.in_place_allowed).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("returns structured error for invalid Cedar input instead of throwing", async () => {
|
|
86
|
+
const result = await handleCheckChange({
|
|
87
|
+
old_policy: "this is not cedar",
|
|
88
|
+
new_policy: `permit(principal, action, resource);`,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(result.error).toBeDefined();
|
|
92
|
+
expect(result.error).toContain("Failed to parse old_policy");
|
|
93
|
+
expect(result.can_update_in_place).toBe(false);
|
|
94
|
+
expect(result.changes).toHaveLength(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("4.7 — identical policies: no changes, can update in place", async () => {
|
|
98
|
+
const policy = `permit(principal in MyApp::Role::"viewer", action == MyApp::Action::"READ", resource);`;
|
|
99
|
+
const result = await handleCheckChange({ old_policy: policy, new_policy: policy });
|
|
100
|
+
|
|
101
|
+
expect(result.can_update_in_place).toBe(true);
|
|
102
|
+
expect(result.changes).toHaveLength(0);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Cross-fixture smoke tests: exercise the tool surface against Dataset 2
|
|
2
|
+
// (Insurance ABAC) to confirm tools aren't locked to Dataset 1 assumptions.
|
|
3
|
+
import { describe, it, expect } from "vitest";
|
|
4
|
+
import { handleAuthorize } from "../../src/tools/authorize.js";
|
|
5
|
+
import { handleValidate } from "../../src/tools/validate.js";
|
|
6
|
+
import { handleValidateSchema } from "../../src/tools/validate-schema.js";
|
|
7
|
+
import { handleValidateEntities } from "../../src/tools/validate-entities.js";
|
|
8
|
+
import { handleDiffSchema } from "../../src/tools/diff-schema.js";
|
|
9
|
+
import {
|
|
10
|
+
POLICIES,
|
|
11
|
+
SCHEMA_JSON,
|
|
12
|
+
ENTITIES,
|
|
13
|
+
} from "../fixtures/multitenant.js";
|
|
14
|
+
import { SCHEMA_JSON as DOCMGMT_SCHEMA } from "../fixtures/docmgmt.js";
|
|
15
|
+
|
|
16
|
+
const SCHEMA_STR = JSON.stringify(SCHEMA_JSON);
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// cedar_validate_schema — Dataset 2 schema is well-formed
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
describe("cedar_validate_schema — Insurance (Dataset 2)", () => {
|
|
22
|
+
it("CF-VS1: Insurance JSON schema is valid", async () => {
|
|
23
|
+
const result = await handleValidateSchema({ schema: SCHEMA_STR });
|
|
24
|
+
expect(result.valid).toBe(true);
|
|
25
|
+
expect(result.errors).toHaveLength(0);
|
|
26
|
+
expect(result.namespaces).toContain("Insurance");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("CF-VS2: reports correct entity type and action counts", async () => {
|
|
30
|
+
const result = await handleValidateSchema({ schema: SCHEMA_STR });
|
|
31
|
+
// Identity, Role, Policy = 3 entity types; CREATE, READ, UPDATE = 3 actions
|
|
32
|
+
expect(result.entity_type_count).toBe(3);
|
|
33
|
+
expect(result.action_count).toBe(3);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// cedar_validate — policies validate cleanly against Dataset 2 schema
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
describe("cedar_validate — Insurance policies (Dataset 2)", () => {
|
|
41
|
+
it("CF-V1: all Insurance policies are valid against the schema", async () => {
|
|
42
|
+
const result = await handleValidate({ policies: POLICIES, schema: SCHEMA_STR });
|
|
43
|
+
expect(result.valid).toBe(true);
|
|
44
|
+
expect(result.errors).toHaveLength(0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("CF-V2: policy referencing a nonexistent attribute is invalid", async () => {
|
|
48
|
+
const bad = `permit(principal, action, resource) when { resource.nonexistent == "x" };`;
|
|
49
|
+
const result = await handleValidate({ policies: bad, schema: SCHEMA_STR });
|
|
50
|
+
expect(result.valid).toBe(false);
|
|
51
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// cedar_validate_entities — Dataset 2 entity set is valid
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
describe("cedar_validate_entities — Insurance entities (Dataset 2)", () => {
|
|
59
|
+
it("CF-VE1: full Insurance entity set is valid against its schema", async () => {
|
|
60
|
+
const result = await handleValidateEntities({
|
|
61
|
+
entities: JSON.stringify(ENTITIES),
|
|
62
|
+
schema: SCHEMA_STR,
|
|
63
|
+
});
|
|
64
|
+
expect(result.valid).toBe(true);
|
|
65
|
+
expect(result.errors).toHaveLength(0);
|
|
66
|
+
expect(result.entity_count).toBe(ENTITIES.length);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("CF-VE2: optional insurer attribute absent on POL-004 does not cause a validation error", async () => {
|
|
70
|
+
// POL-004 intentionally has no `insurer` field — must still be valid (required: false)
|
|
71
|
+
const pol004 = ENTITIES.filter((e) => e.uid.id === "POL-004");
|
|
72
|
+
const result = await handleValidateEntities({
|
|
73
|
+
entities: JSON.stringify(pol004),
|
|
74
|
+
schema: SCHEMA_STR,
|
|
75
|
+
});
|
|
76
|
+
expect(result.valid).toBe(true);
|
|
77
|
+
expect(result.errors).toHaveLength(0);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// cedar_authorize — ABAC decisions against Dataset 2
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
describe("cedar_authorize — Insurance ABAC (Dataset 2)", () => {
|
|
85
|
+
it("CF-A1: tenant-a is allowed READ on POL-001 (matching vertical + business_unit)", async () => {
|
|
86
|
+
const result = await handleAuthorize({
|
|
87
|
+
policies: POLICIES,
|
|
88
|
+
principal: 'Insurance::Identity::"tenant-a"',
|
|
89
|
+
action: 'Insurance::Action::"READ"',
|
|
90
|
+
resource: 'Insurance::Policy::"POL-001"',
|
|
91
|
+
entities: JSON.stringify(ENTITIES),
|
|
92
|
+
});
|
|
93
|
+
expect(result.decision).toBe("Allow");
|
|
94
|
+
expect(result.errors).toHaveLength(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("CF-A2: tenant-a is denied READ on POL-002 (wrong business_unit)", async () => {
|
|
98
|
+
// POL-002 vertical=commercial_landlord — not in tenant-a permit list
|
|
99
|
+
const result = await handleAuthorize({
|
|
100
|
+
policies: POLICIES,
|
|
101
|
+
principal: 'Insurance::Identity::"tenant-a"',
|
|
102
|
+
action: 'Insurance::Action::"READ"',
|
|
103
|
+
resource: 'Insurance::Policy::"POL-002"',
|
|
104
|
+
entities: JSON.stringify(ENTITIES),
|
|
105
|
+
});
|
|
106
|
+
expect(result.decision).toBe("Deny");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("CF-A3: tenant-b READ denied on POL-004 — resource has no insurer attr", async () => {
|
|
110
|
+
// POL-004 has no insurer; `resource has insurer` is false → policy doesn't apply
|
|
111
|
+
const result = await handleAuthorize({
|
|
112
|
+
policies: POLICIES,
|
|
113
|
+
principal: 'Insurance::Identity::"tenant-b"',
|
|
114
|
+
action: 'Insurance::Action::"READ"',
|
|
115
|
+
resource: 'Insurance::Policy::"POL-004"',
|
|
116
|
+
entities: JSON.stringify(ENTITIES),
|
|
117
|
+
});
|
|
118
|
+
expect(result.decision).toBe("Deny");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("CF-A4: unknown-client READ denied — default deny, no policy matches", async () => {
|
|
122
|
+
const result = await handleAuthorize({
|
|
123
|
+
policies: POLICIES,
|
|
124
|
+
principal: 'Insurance::Identity::"unknown-client"',
|
|
125
|
+
action: 'Insurance::Action::"READ"',
|
|
126
|
+
resource: 'Insurance::Policy::"POL-001"',
|
|
127
|
+
entities: JSON.stringify(ENTITIES),
|
|
128
|
+
});
|
|
129
|
+
expect(result.decision).toBe("Deny");
|
|
130
|
+
expect(result.determining_policies).toHaveLength(0);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("CF-A5: tenant-d CREATE allowed", async () => {
|
|
134
|
+
const result = await handleAuthorize({
|
|
135
|
+
policies: POLICIES,
|
|
136
|
+
principal: 'Insurance::Identity::"tenant-d"',
|
|
137
|
+
action: 'Insurance::Action::"CREATE"',
|
|
138
|
+
resource: 'Insurance::Policy::"POL-001"',
|
|
139
|
+
entities: JSON.stringify(ENTITIES),
|
|
140
|
+
});
|
|
141
|
+
expect(result.decision).toBe("Allow");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("CF-A6: tenant-d UPDATE denied — not in permitted action list", async () => {
|
|
145
|
+
const result = await handleAuthorize({
|
|
146
|
+
policies: POLICIES,
|
|
147
|
+
principal: 'Insurance::Identity::"tenant-d"',
|
|
148
|
+
action: 'Insurance::Action::"UPDATE"',
|
|
149
|
+
resource: 'Insurance::Policy::"POL-001"',
|
|
150
|
+
entities: JSON.stringify(ENTITIES),
|
|
151
|
+
});
|
|
152
|
+
expect(result.decision).toBe("Deny");
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// cedar_diff_schema — cross-dataset diff (Dataset 1 vs Dataset 2)
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
describe("cedar_diff_schema — DocMgmt vs Insurance (cross-fixture)", () => {
|
|
160
|
+
it("CF-DS1: diffing two distinct namespaces reports both namespaces as added/removed", async () => {
|
|
161
|
+
const result = await handleDiffSchema({
|
|
162
|
+
blue: JSON.stringify(DOCMGMT_SCHEMA),
|
|
163
|
+
green: SCHEMA_STR,
|
|
164
|
+
});
|
|
165
|
+
// DocMgmt was in blue → removed; Insurance is in green → added
|
|
166
|
+
expect(result.namespaces_removed).toContain("DocMgmt");
|
|
167
|
+
expect(result.namespaces_added).toContain("Insurance");
|
|
168
|
+
expect(result.risk_level).toBe("breaking");
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { handleDiffSchema } from "../../src/tools/diff-schema.js";
|
|
3
|
+
|
|
4
|
+
// Canonical Dataset 1 schema for diff tests. Keep small & focused so each
|
|
5
|
+
// test case is self-contained and the diff under test is obvious from inspection.
|
|
6
|
+
const BASE_SCHEMA = `
|
|
7
|
+
namespace App {
|
|
8
|
+
entity User in [Role] = { name: String, email: String };
|
|
9
|
+
entity Role;
|
|
10
|
+
entity Document in [Folder] = { owner: String, classification: String };
|
|
11
|
+
entity Folder;
|
|
12
|
+
action READ, WRITE appliesTo {
|
|
13
|
+
principal: [User],
|
|
14
|
+
resource: [Document]
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
`.trim();
|
|
18
|
+
|
|
19
|
+
describe("cedar_diff_schema — structural", () => {
|
|
20
|
+
it("DS1: identical schemas produce an empty diff with risk_level safe", async () => {
|
|
21
|
+
const result = await handleDiffSchema({ blue: BASE_SCHEMA, green: BASE_SCHEMA });
|
|
22
|
+
|
|
23
|
+
expect(result.risk_level).toBe("safe");
|
|
24
|
+
expect(result.entity_types.added).toHaveLength(0);
|
|
25
|
+
expect(result.entity_types.removed).toHaveLength(0);
|
|
26
|
+
expect(result.entity_types.modified).toHaveLength(0);
|
|
27
|
+
expect(result.actions.added).toHaveLength(0);
|
|
28
|
+
expect(result.actions.removed).toHaveLength(0);
|
|
29
|
+
expect(result.actions.modified).toHaveLength(0);
|
|
30
|
+
expect(result.summary).toMatch(/no schema changes/i);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("DS2: entity type added in green → entity_types.added, risk safe", async () => {
|
|
34
|
+
const green = BASE_SCHEMA.replace(
|
|
35
|
+
"entity Folder;",
|
|
36
|
+
"entity Folder;\n entity Tag = { label: String };"
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const result = await handleDiffSchema({ blue: BASE_SCHEMA, green });
|
|
40
|
+
|
|
41
|
+
expect(result.entity_types.added).toContainEqual({ namespace: "App", name: "Tag" });
|
|
42
|
+
expect(result.risk_level).toBe("safe");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("DS3: entity type removed in green → entity_types.removed, risk breaking", async () => {
|
|
46
|
+
const green = BASE_SCHEMA.replace("entity Folder;\n ", "");
|
|
47
|
+
// Folder is referenced in 'entity Document in [Folder]' — also strip that
|
|
48
|
+
const greenClean = green.replace(" in [Folder]", "");
|
|
49
|
+
|
|
50
|
+
const result = await handleDiffSchema({ blue: BASE_SCHEMA, green: greenClean });
|
|
51
|
+
|
|
52
|
+
expect(result.entity_types.removed).toContainEqual(
|
|
53
|
+
expect.objectContaining({ namespace: "App", name: "Folder", risk: "breaking" })
|
|
54
|
+
);
|
|
55
|
+
expect(result.risk_level).toBe("breaking");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("cedar_diff_schema — attribute changes", () => {
|
|
60
|
+
it("DS4a: optional attribute added → safe", async () => {
|
|
61
|
+
const green = BASE_SCHEMA.replace(
|
|
62
|
+
"{ name: String, email: String }",
|
|
63
|
+
"{ name: String, email: String, phone?: String }"
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const result = await handleDiffSchema({ blue: BASE_SCHEMA, green });
|
|
67
|
+
|
|
68
|
+
const userMod = result.entity_types.modified.find((m) => m.name === "User");
|
|
69
|
+
expect(userMod).toBeDefined();
|
|
70
|
+
const phoneChange = userMod!.attribute_changes?.find((c) => c.attr === "phone");
|
|
71
|
+
expect(phoneChange).toBeDefined();
|
|
72
|
+
expect(phoneChange!.change).toBe("added");
|
|
73
|
+
expect(phoneChange!.risk).toBe("safe");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("DS4b: required attribute added → breaking", async () => {
|
|
77
|
+
const green = BASE_SCHEMA.replace(
|
|
78
|
+
"{ name: String, email: String }",
|
|
79
|
+
"{ name: String, email: String, phone: String }"
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const result = await handleDiffSchema({ blue: BASE_SCHEMA, green });
|
|
83
|
+
|
|
84
|
+
const userMod = result.entity_types.modified.find((m) => m.name === "User");
|
|
85
|
+
const phoneChange = userMod!.attribute_changes?.find((c) => c.attr === "phone");
|
|
86
|
+
expect(phoneChange!.change).toBe("added");
|
|
87
|
+
expect(phoneChange!.risk).toBe("breaking");
|
|
88
|
+
expect(result.risk_level).toBe("breaking");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("DS5: attribute removed → breaking", async () => {
|
|
92
|
+
const green = BASE_SCHEMA.replace(
|
|
93
|
+
"{ name: String, email: String }",
|
|
94
|
+
"{ name: String }"
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const result = await handleDiffSchema({ blue: BASE_SCHEMA, green });
|
|
98
|
+
|
|
99
|
+
const userMod = result.entity_types.modified.find((m) => m.name === "User");
|
|
100
|
+
const emailChange = userMod!.attribute_changes?.find((c) => c.attr === "email");
|
|
101
|
+
expect(emailChange!.change).toBe("removed");
|
|
102
|
+
expect(emailChange!.risk).toBe("breaking");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("DS6: attribute type changed (String → Long) → breaking", async () => {
|
|
106
|
+
const green = BASE_SCHEMA.replace(
|
|
107
|
+
"{ name: String, email: String }",
|
|
108
|
+
"{ name: Long, email: String }"
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const result = await handleDiffSchema({ blue: BASE_SCHEMA, green });
|
|
112
|
+
|
|
113
|
+
const userMod = result.entity_types.modified.find((m) => m.name === "User");
|
|
114
|
+
const nameChange = userMod!.attribute_changes?.find((c) => c.attr === "name");
|
|
115
|
+
expect(nameChange!.change).toBe("type_changed");
|
|
116
|
+
expect(nameChange!.old_type).toBe("String");
|
|
117
|
+
expect(nameChange!.new_type).toBe("Long");
|
|
118
|
+
expect(nameChange!.risk).toBe("breaking");
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("cedar_diff_schema — action changes", () => {
|
|
123
|
+
it("DS7: action added → safe", async () => {
|
|
124
|
+
const green = BASE_SCHEMA.replace(
|
|
125
|
+
"action READ, WRITE appliesTo",
|
|
126
|
+
"action READ, WRITE, DELETE appliesTo"
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const result = await handleDiffSchema({ blue: BASE_SCHEMA, green });
|
|
130
|
+
|
|
131
|
+
expect(result.actions.added).toContainEqual({ namespace: "App", name: "DELETE" });
|
|
132
|
+
expect(result.risk_level).toBe("safe");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("DS8: action removed → breaking", async () => {
|
|
136
|
+
const green = BASE_SCHEMA.replace("action READ, WRITE appliesTo", "action READ appliesTo");
|
|
137
|
+
|
|
138
|
+
const result = await handleDiffSchema({ blue: BASE_SCHEMA, green });
|
|
139
|
+
|
|
140
|
+
expect(result.actions.removed).toContainEqual(
|
|
141
|
+
expect.objectContaining({ namespace: "App", name: "WRITE", risk: "breaking" })
|
|
142
|
+
);
|
|
143
|
+
expect(result.risk_level).toBe("breaking");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("DS9: action principal_types widened → review", async () => {
|
|
147
|
+
// Add a new principal type; action gains another principal type
|
|
148
|
+
const blue = `
|
|
149
|
+
namespace App {
|
|
150
|
+
entity User;
|
|
151
|
+
entity Admin;
|
|
152
|
+
entity Document;
|
|
153
|
+
action READ appliesTo { principal: [User], resource: [Document] };
|
|
154
|
+
}
|
|
155
|
+
`.trim();
|
|
156
|
+
const green = `
|
|
157
|
+
namespace App {
|
|
158
|
+
entity User;
|
|
159
|
+
entity Admin;
|
|
160
|
+
entity Document;
|
|
161
|
+
action READ appliesTo { principal: [User, Admin], resource: [Document] };
|
|
162
|
+
}
|
|
163
|
+
`.trim();
|
|
164
|
+
|
|
165
|
+
const result = await handleDiffSchema({ blue, green });
|
|
166
|
+
|
|
167
|
+
const readMod = result.actions.modified.find((m) => m.name === "READ");
|
|
168
|
+
expect(readMod).toBeDefined();
|
|
169
|
+
expect(readMod!.principal_types?.added).toEqual(["App::Admin"]);
|
|
170
|
+
expect(readMod!.principal_types?.risk).toBe("review");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("DS10: action principal_types narrowed → breaking", async () => {
|
|
174
|
+
const blue = `
|
|
175
|
+
namespace App {
|
|
176
|
+
entity User;
|
|
177
|
+
entity Admin;
|
|
178
|
+
entity Document;
|
|
179
|
+
action READ appliesTo { principal: [User, Admin], resource: [Document] };
|
|
180
|
+
}
|
|
181
|
+
`.trim();
|
|
182
|
+
const green = `
|
|
183
|
+
namespace App {
|
|
184
|
+
entity User;
|
|
185
|
+
entity Admin;
|
|
186
|
+
entity Document;
|
|
187
|
+
action READ appliesTo { principal: [User], resource: [Document] };
|
|
188
|
+
}
|
|
189
|
+
`.trim();
|
|
190
|
+
|
|
191
|
+
const result = await handleDiffSchema({ blue, green });
|
|
192
|
+
|
|
193
|
+
const readMod = result.actions.modified.find((m) => m.name === "READ");
|
|
194
|
+
expect(readMod!.principal_types?.removed).toEqual(["App::Admin"]);
|
|
195
|
+
expect(readMod!.principal_types?.risk).toBe("breaking");
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("cedar_diff_schema — input formats and errors", () => {
|
|
200
|
+
it("DS13: malformed blue schema returns error result, not crash", async () => {
|
|
201
|
+
const result = await handleDiffSchema({ blue: "not a schema", green: BASE_SCHEMA });
|
|
202
|
+
|
|
203
|
+
expect(result.error).toBeTruthy();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("DS14: blue and green in different formats (JSON vs cedarschema) — diff still works", async () => {
|
|
207
|
+
const jsonForm = JSON.stringify({
|
|
208
|
+
App: {
|
|
209
|
+
entityTypes: {
|
|
210
|
+
User: { memberOfTypes: [], shape: { type: "Record", attributes: { name: { type: "String", required: true } } } },
|
|
211
|
+
Document: { memberOfTypes: [], shape: { type: "Record", attributes: {} } },
|
|
212
|
+
},
|
|
213
|
+
actions: {
|
|
214
|
+
READ: { appliesTo: { principalTypes: ["User"], resourceTypes: ["Document"] } },
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
const cedarForm = `
|
|
219
|
+
namespace App {
|
|
220
|
+
entity User = { name: String };
|
|
221
|
+
entity Document;
|
|
222
|
+
action READ appliesTo { principal: [User], resource: [Document] };
|
|
223
|
+
}
|
|
224
|
+
`.trim();
|
|
225
|
+
|
|
226
|
+
const result = await handleDiffSchema({ blue: jsonForm, green: cedarForm });
|
|
227
|
+
|
|
228
|
+
// Same logical content — diff should be empty
|
|
229
|
+
expect(result.entity_types.added).toHaveLength(0);
|
|
230
|
+
expect(result.entity_types.removed).toHaveLength(0);
|
|
231
|
+
expect(result.entity_types.modified).toHaveLength(0);
|
|
232
|
+
expect(result.actions.added).toHaveLength(0);
|
|
233
|
+
expect(result.actions.removed).toHaveLength(0);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe("cedar_diff_schema — falsification: diff predicts cedar_validate outcome", () => {
|
|
238
|
+
it("DSF: when diff says 'breaking' for an attribute removal, a policy referencing the attribute fails cedar_validate against green", async () => {
|
|
239
|
+
const { handleValidate } = await import("../../src/tools/validate.js");
|
|
240
|
+
|
|
241
|
+
const blue = `
|
|
242
|
+
namespace App {
|
|
243
|
+
entity User = { name: String, email: String };
|
|
244
|
+
entity Doc;
|
|
245
|
+
action READ appliesTo { principal: [User], resource: [Doc] };
|
|
246
|
+
}
|
|
247
|
+
`.trim();
|
|
248
|
+
const green = `
|
|
249
|
+
namespace App {
|
|
250
|
+
entity User = { name: String };
|
|
251
|
+
entity Doc;
|
|
252
|
+
action READ appliesTo { principal: [User], resource: [Doc] };
|
|
253
|
+
}
|
|
254
|
+
`.trim();
|
|
255
|
+
const policy = `permit (principal, action, resource) when { principal.email == "alice@x.y" };`;
|
|
256
|
+
|
|
257
|
+
// 1. Policy is valid against blue
|
|
258
|
+
const blueValidation = await handleValidate({ policies: policy, schema: blue });
|
|
259
|
+
expect(blueValidation.valid).toBe(true);
|
|
260
|
+
|
|
261
|
+
// 2. Diff classifies email removal as breaking
|
|
262
|
+
const diff = await handleDiffSchema({ blue, green });
|
|
263
|
+
const userMod = diff.entity_types.modified.find((m) => m.name === "User");
|
|
264
|
+
expect(userMod).toBeDefined();
|
|
265
|
+
const emailChange = userMod!.attribute_changes?.find((c) => c.attr === "email");
|
|
266
|
+
expect(emailChange!.change).toBe("removed");
|
|
267
|
+
expect(emailChange!.risk).toBe("breaking");
|
|
268
|
+
|
|
269
|
+
// 3. Policy is INVALID against green — falsification check passes
|
|
270
|
+
const greenValidation = await handleValidate({ policies: policy, schema: green });
|
|
271
|
+
expect(greenValidation.valid).toBe(false);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe("cedar_diff_schema — namespace changes", () => {
|
|
276
|
+
it("DS15: namespace added → namespaces_added has it; all entities in new ns reported as added", async () => {
|
|
277
|
+
const green = `
|
|
278
|
+
namespace App {
|
|
279
|
+
entity User;
|
|
280
|
+
entity Role;
|
|
281
|
+
entity Document in [Folder];
|
|
282
|
+
entity Folder;
|
|
283
|
+
action READ, WRITE appliesTo {
|
|
284
|
+
principal: [User],
|
|
285
|
+
resource: [Document]
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
namespace Audit {
|
|
290
|
+
entity Event = { kind: String };
|
|
291
|
+
action LOG appliesTo { principal: [App::User], resource: [Event] };
|
|
292
|
+
}
|
|
293
|
+
`.trim();
|
|
294
|
+
const blue = `
|
|
295
|
+
namespace App {
|
|
296
|
+
entity User in [Role] = { name: String, email: String };
|
|
297
|
+
entity Role;
|
|
298
|
+
entity Document in [Folder] = { owner: String, classification: String };
|
|
299
|
+
entity Folder;
|
|
300
|
+
action READ, WRITE appliesTo {
|
|
301
|
+
principal: [User],
|
|
302
|
+
resource: [Document]
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
`.trim();
|
|
306
|
+
// Use a simpler blue without attrs so we just check namespace addition behavior
|
|
307
|
+
const simpleBlue = `
|
|
308
|
+
namespace App {
|
|
309
|
+
entity User;
|
|
310
|
+
entity Role;
|
|
311
|
+
entity Document in [Folder];
|
|
312
|
+
entity Folder;
|
|
313
|
+
action READ, WRITE appliesTo {
|
|
314
|
+
principal: [User],
|
|
315
|
+
resource: [Document]
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
`.trim();
|
|
319
|
+
|
|
320
|
+
const result = await handleDiffSchema({ blue: simpleBlue, green });
|
|
321
|
+
|
|
322
|
+
expect(result.namespaces_added).toContain("Audit");
|
|
323
|
+
expect(result.entity_types.added).toContainEqual({ namespace: "Audit", name: "Event" });
|
|
324
|
+
expect(result.actions.added).toContainEqual({ namespace: "Audit", name: "LOG" });
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("DS16: namespace removed → namespaces_removed has it, all entities in removed ns reported as breaking removed", async () => {
|
|
328
|
+
const blue = `
|
|
329
|
+
namespace App {
|
|
330
|
+
entity User;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
namespace Audit {
|
|
334
|
+
entity Event = { kind: String };
|
|
335
|
+
action LOG appliesTo { principal: [App::User], resource: [Event] };
|
|
336
|
+
}
|
|
337
|
+
`.trim();
|
|
338
|
+
const green = `
|
|
339
|
+
namespace App {
|
|
340
|
+
entity User;
|
|
341
|
+
}
|
|
342
|
+
`.trim();
|
|
343
|
+
|
|
344
|
+
const result = await handleDiffSchema({ blue, green });
|
|
345
|
+
|
|
346
|
+
expect(result.namespaces_removed).toContain("Audit");
|
|
347
|
+
expect(result.entity_types.removed).toContainEqual(
|
|
348
|
+
expect.objectContaining({ namespace: "Audit", name: "Event", risk: "breaking" })
|
|
349
|
+
);
|
|
350
|
+
expect(result.actions.removed).toContainEqual(
|
|
351
|
+
expect.objectContaining({ namespace: "Audit", name: "LOG", risk: "breaking" })
|
|
352
|
+
);
|
|
353
|
+
expect(result.risk_level).toBe("breaking");
|
|
354
|
+
});
|
|
355
|
+
});
|