mirror of
https://github.com/penpot/penpot.git
synced 2026-08-10 06:48:49 +00:00
Nothing checked that the eleven Cypher templates `app.graph.sync` emits still bind against the DDL the schema registry generates. A renamed column, a dropped table or a reserved word emitted unquoted surfaced only when a live session ran the statement, and by then the batch's earlier mutations had committed. `backend-tests.graph-binder-gate-test` opens a `:memory:` database, creates the live schema on it, and *prepares* one instance of each template without executing any of them. 14 tests, 51 assertions: the eleven templates, label coverage over all twelve registered node tables, and two assertions on the gate itself, that a `RETURN` reads as read-only and a `SET` does not, and that an unbindable statement is reported rather than thrown. It was not green on HEAD: it caught `set-document-revision-statement` writing a column that no longer exists, fixed in the previous commit. Red on both injected templates tried. No `:jvm-opts` change: CI's `-M:dev:test` already carries the native access flags the engine needs. AI-assisted-by: mixed models
166 lines
6.6 KiB
Clojure
166 lines
6.6 KiB
Clojure
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
;;
|
|
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
|
|
|
(ns backend-tests.graph-binder-gate-test
|
|
"Binder gate for the incremental-sync statement templates.
|
|
|
|
Every template `app.graph.sync` emits is *prepared* — parsed and bound by
|
|
the engine against the live DDL — and never executed. A parse or bind
|
|
failure (a renamed column, a reserved-word label emitted unquoted, a dropped
|
|
table) turns the gate red here, before the statement can reach a live
|
|
session.
|
|
|
|
One instance per template is the gate; per-column type coverage belongs to
|
|
beadpot's schema diff, not here. The templates are `defn-`, so they are
|
|
reached through their vars."
|
|
(:require
|
|
[app.graph.ladybug :as ladybug]
|
|
[app.graph.schema.nodes :as nodes]
|
|
[app.graph.sync]
|
|
[clojure.test :as t]))
|
|
|
|
(def ^:private create-node-statement #'app.graph.sync/create-node-statement)
|
|
(def ^:private delete-node-statement #'app.graph.sync/delete-node-statement)
|
|
(def ^:private create-edge-statement #'app.graph.sync/create-edge-statement)
|
|
(def ^:private delete-edge-statement #'app.graph.sync/delete-edge-statement)
|
|
(def ^:private set-edge-position-statement #'app.graph.sync/set-edge-position-statement)
|
|
(def ^:private create-instance-of-statement #'app.graph.sync/create-instance-of-statement)
|
|
(def ^:private delete-instance-of-statement #'app.graph.sync/delete-instance-of-statement)
|
|
(def ^:private set-node-attr-statement #'app.graph.sync/set-node-attr-statement)
|
|
(def ^:private set-page-name-statement #'app.graph.sync/set-page-name-statement)
|
|
(def ^:private remove-node-attr-statement #'app.graph.sync/remove-node-attr-statement)
|
|
(def ^:private set-document-revision-statement #'app.graph.sync/set-document-revision-statement)
|
|
|
|
;; Dummy identities. Fixed rather than generated: a gate failure should read
|
|
;; the same on every run.
|
|
(def ^:private doc-id #uuid "00000000-0000-0000-0000-0000000000d0")
|
|
(def ^:private page-id #uuid "00000000-0000-0000-0000-0000000000a0")
|
|
(def ^:private shape-id #uuid "00000000-0000-0000-0000-0000000000b0")
|
|
(def ^:private frame-id #uuid "00000000-0000-0000-0000-0000000000c0")
|
|
(def ^:private component-id #uuid "00000000-0000-0000-0000-0000000000e0")
|
|
|
|
(def ^:private child-edge
|
|
{:from-table "Rectangle" :from-id shape-id
|
|
:to-table "Page" :to-id page-id
|
|
:position 3})
|
|
|
|
(def ^:private ^:dynamic *conn* nil)
|
|
|
|
(defn- with-graph-connection
|
|
"Open a `:memory:` database, create the live schema, run the tests on it.
|
|
|
|
Nothing is executed against it — the gate only prepares — but the DDL has to
|
|
be there for the binder to resolve tables and columns against."
|
|
[next]
|
|
(ladybug/with-connection! ":memory:"
|
|
(fn [conn]
|
|
(ladybug/exec-on-connection! conn (nodes/ddl-statements))
|
|
(binding [*conn* conn]
|
|
(next)))))
|
|
|
|
(t/use-fixtures :once with-graph-connection)
|
|
|
|
(defn- gate
|
|
"Assert `statement` binds, and that the engine agrees on read/write."
|
|
[label statement read-only?]
|
|
(let [result (ladybug/validate-on-connection! *conn* statement)]
|
|
(t/is (:ok? result)
|
|
(str label " does not bind: " (:error result) "\n " statement))
|
|
(when (:ok? result)
|
|
(t/is (= read-only? (:read-only? result))
|
|
(str label " read-only? " (:read-only? result) ", expected " read-only?)))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; the eleven sync templates
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(t/deftest create-node-binds
|
|
(gate "create-node-statement"
|
|
(create-node-statement "Rectangle" {:id shape-id
|
|
:name "a shape"
|
|
:opacity 1.0
|
|
:hidden false})
|
|
false))
|
|
|
|
(t/deftest delete-node-binds
|
|
(gate "delete-node-statement"
|
|
(delete-node-statement "Rectangle" shape-id)
|
|
false))
|
|
|
|
(t/deftest create-edge-binds
|
|
(gate "create-edge-statement"
|
|
(create-edge-statement child-edge)
|
|
false))
|
|
|
|
(t/deftest delete-edge-binds
|
|
(gate "delete-edge-statement"
|
|
(delete-edge-statement (dissoc child-edge :position))
|
|
false))
|
|
|
|
(t/deftest set-edge-position-binds
|
|
(gate "set-edge-position-statement"
|
|
(set-edge-position-statement child-edge)
|
|
false))
|
|
|
|
(t/deftest create-instance-of-binds
|
|
(gate "create-instance-of-statement"
|
|
(create-instance-of-statement frame-id component-id)
|
|
false))
|
|
|
|
(t/deftest delete-instance-of-binds
|
|
(gate "delete-instance-of-statement"
|
|
(delete-instance-of-statement frame-id)
|
|
false))
|
|
|
|
(t/deftest set-node-attr-binds
|
|
(gate "set-node-attr-statement"
|
|
(set-node-attr-statement "Rectangle" shape-id :name "a shape")
|
|
false))
|
|
|
|
(t/deftest set-page-name-binds
|
|
(gate "set-page-name-statement"
|
|
(set-page-name-statement page-id "a page")
|
|
false))
|
|
|
|
(t/deftest remove-node-attr-binds
|
|
(gate "remove-node-attr-statement"
|
|
(remove-node-attr-statement "Rectangle" shape-id :name)
|
|
false))
|
|
|
|
(t/deftest set-document-revision-binds
|
|
(gate "set-document-revision-statement"
|
|
(set-document-revision-statement doc-id 42)
|
|
false))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; label quoting across the registry
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(t/deftest every-node-label-binds
|
|
;; `Group` and `Boolean` are reserved words: unquoted they do not parse.
|
|
;; One MATCH per registered table is the cheapest way to keep `match-label`
|
|
;; honest as tables come and go.
|
|
(doseq [table (map :table nodes/node-types)]
|
|
(gate (str "delete-node-statement on " table)
|
|
(delete-node-statement table shape-id)
|
|
false)))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; the gate itself
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(t/deftest read-only-discriminates
|
|
;; Without this the `read-only? false` assertions above would hold for a
|
|
;; `validate-on-connection!` that always answered false.
|
|
(gate "a read query" "MATCH (n:Rectangle) RETURN count(n);" true))
|
|
|
|
(t/deftest bad-statement-is-reported-not-thrown
|
|
(let [result (ladybug/validate-on-connection!
|
|
*conn* "MATCH (n:Rectangle) SET n.no_such_column = 1;")]
|
|
(t/is (false? (:ok? result)))
|
|
(t/is (string? (:error result)))
|
|
(t/is (nil? (:read-only? result)))))
|