import test from "node:test"; import assert from "node:assert/strict"; import { validate, type SchemaSnapshot } from "../cypher-validate.js"; const snapshot: SchemaSnapshot = { labels: new Set(["Person", "Organization", "Task", "KnowledgeDocument"]), relationshipTypes: new Set(["KNOWS", "PARTICIPANT", "REFERENCES", "MENTIONS", "PART_OF"]), }; test("write mode allows CREATE node + relationship statement", () => { const result = validate( "MATCH (a:Person), (b:Organization) WHERE a.name = $a AND b.name = $b CREATE (a)-[:KNOWS]->(b)", snapshot, { mode: "write" }, ); assert.equal(result.ok, true, "expected valid write"); assert.deepEqual(result.unknown, []); assert.deepEqual(result.forbidden, []); }); test("write mode allows MERGE with SET clause", () => { const result = validate( "MATCH (a:Person {name: $name}) MERGE (a)-[r:KNOWS]->(b:Person {name: $other}) SET r.createdAt = datetime()", snapshot, { mode: "write" }, ); assert.equal(result.ok, true); }); test("write mode allows DETACH DELETE", () => { const result = validate( "MATCH (n:Person) WHERE n.archived = true DETACH DELETE n", snapshot, { mode: "write" }, ); assert.equal(result.ok, true); }); test("write mode allows REMOVE label + SET label (re-label)", () => { // Allow renaming labels — the new label is unknown to the schema until the // write commits, but the validator must not block legitimate normalisation. const result = validate( "MATCH (n:Person) WHERE n.legacy = true REMOVE n:Person SET n:LegacyPerson", snapshot, { mode: "write" }, ); // LegacyPerson is unknown — fail-soft for write mode (audit emits warning, not reject). // The token check is shared with read-mode; the write mode adds DDL/admin reject layer. // For this test we accept the write-mode-specific outcome: no forbidden patterns. assert.deepEqual(result.forbidden, []); }); test("write mode allows CALL apoc.refactor.mergeNodes", () => { const result = validate( "MATCH (a:Person), (b:Person) WHERE a.email = b.email AND id(a) < id(b) WITH a, b CALL apoc.refactor.mergeNodes([a, b]) YIELD node RETURN node", snapshot, { mode: "write" }, ); assert.deepEqual(result.forbidden, []); }); test("write mode REJECTS DROP DATABASE", () => { const result = validate("DROP DATABASE neo4j", snapshot, { mode: "write" }); assert.equal(result.ok, false); assert.equal(result.forbidden.length, 1); assert.equal(result.forbidden[0].kind, "drop-database"); }); test("write mode REJECTS CREATE INDEX", () => { const result = validate( "CREATE INDEX person_name_idx FOR (n:Person) ON (n.name)", snapshot, { mode: "write" }, ); assert.equal(result.ok, false); const f = result.forbidden.find((x) => x.kind === "create-index"); assert.ok(f, "expected create-index forbidden"); }); test("write mode REJECTS CREATE CONSTRAINT", () => { const result = validate( "CREATE CONSTRAINT person_email_unique FOR (n:Person) REQUIRE n.email IS UNIQUE", snapshot, { mode: "write" }, ); assert.equal(result.ok, false); const f = result.forbidden.find((x) => x.kind === "create-constraint"); assert.ok(f); }); test("write mode REJECTS CALL dbms.security.*", () => { const result = validate( "CALL dbms.security.createUser('attacker', 'pwd', false)", snapshot, { mode: "write" }, ); assert.equal(result.ok, false); const f = result.forbidden.find((x) => x.kind === "call-dbms"); assert.ok(f); }); test("write mode REJECTS CALL db.create*", () => { const result = validate( "CALL db.createLabel('Foo')", snapshot, { mode: "write" }, ); assert.equal(result.ok, false); const f = result.forbidden.find((x) => x.kind === "call-db-create"); assert.ok(f); }); test("write mode allows CALL db.labels (read-only db.* call)", () => { // db.labels is a read primitive — not a create/drop. Validator only // forbids db.create* family; CALL db.labels remains allowed. const result = validate("CALL db.labels()", snapshot, { mode: "write" }); assert.deepEqual(result.forbidden, []); }); test("read mode does NOT apply forbidden checks (DDL pattern in MATCH still allowed)", () => { // String literals containing forbidden tokens never trip — DDL forbidden check // is mode-gated. Read-mode WHERE clauses comparing on a string with the // word DROP must not reject. const result = validate( "MATCH (n:Person) WHERE n.bio CONTAINS 'DROP DATABASE' RETURN n", snapshot, { mode: "read" }, ); assert.deepEqual(result.forbidden, []); assert.equal(result.ok, true); }); test("write mode strips string literals before forbidden check", () => { // The forbidden check must use stripStringLiterals so a string property // value like 'DROP DATABASE archive' does not trip the rejection. const result = validate( "MATCH (n:Person) WHERE n.bio CONTAINS 'DROP DATABASE' SET n.flagged = true", snapshot, { mode: "write" }, ); assert.deepEqual(result.forbidden, [], "string-literal content must not trip DDL check"); }); test("default mode is read (backward compatibility)", () => { // Existing call sites pass no mode option — must remain read-mode behavior. const result = validate("MATCH (n:Person) RETURN n", snapshot); assert.equal(result.ok, true); assert.equal(Array.isArray(result.forbidden), true); assert.equal(result.forbidden.length, 0); });