mirror of
https://github.com/penpot/penpot.git
synced 2026-05-30 12:18:13 +00:00
* ⚡ Improve performance and fix orphan detection in validate-file - Add `*ref-shape-cache*` dynamic var to memoize `find-ref-shape` lookups per page, avoiding repeated O(depth) ancestor walks. - Add `*children-sets*` pre-computed maps for O(1) parent-child containment checks, replacing linear `some` scans. - Short-circuit `inside-component-main?` when the shape context already implies a main component. - Use single-pass reduce with early exit for duplicate detection (children, swap slots) instead of count/distinct or frequencies. - Guard `check-missing-slot` to skip expensive `find-near-match` when the shape already has a swap slot. - Refactor variant-set validation to use `run!` with direct `get`. - Refactor `check-ref-cycles` to use a single `reduce-kv` pass. - Fix `get-orphan-shapes`: the original `map` pipeline produced nils so orphan shapes were never validated; rewrite with `reduce-kv` for correct results. - Add `validate-file-affected!` for change-scoped validation, replacing full file validation in `process-changes-and-validate` to only validate pages and components touched by the changes. Signed-off-by: Andrey Antukh <niwi@niwi.nz> * ✨ Improved validation --------- Signed-off-by: Andrey Antukh <niwi@niwi.nz> Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
450 lines
22 KiB
Clojure
450 lines
22 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
|
||
|
||
(ns common-tests.files.validate-test
|
||
"Exhaustive tests for the change-scoped partial validation functions in
|
||
app.common.files.validate:
|
||
|
||
- validate-file-affected – returns nil or list of errors
|
||
- validate-file-affected! – same but raises on non-empty errors
|
||
|
||
The tests verify the scoping logic implemented by extract-affected-ids
|
||
(tested indirectly) by injecting controlled broken states into specific
|
||
pages / components and confirming that only the expected entities are
|
||
validated."
|
||
(:require
|
||
[app.common.files.validate :as cfv]
|
||
[app.common.test-helpers.files :as thf]
|
||
[app.common.test-helpers.ids-map :as thi]
|
||
[app.common.types.file :as ctf]
|
||
[app.common.types.pages-list :as ctpl]
|
||
[app.common.types.shape-tree :as ctst]
|
||
[app.common.uuid :as uuid]
|
||
[clojure.test :as t]))
|
||
|
||
(t/use-fixtures :each thi/test-fixture)
|
||
|
||
;; ----------------------------------------------------------------
|
||
;; Test-local helpers
|
||
;; ----------------------------------------------------------------
|
||
|
||
(defn- inject-broken-child
|
||
"Add a reference to a non-existent shape ID to the root shape's
|
||
children list on the page identified by `page-label`.
|
||
|
||
This causes `check-parent-children` to report a :child-not-found
|
||
error whenever that page is validated."
|
||
[file page-label]
|
||
(let [page-id (thi/id page-label)
|
||
missing-id (uuid/next)]
|
||
(ctf/update-file-data
|
||
file
|
||
(fn [file-data]
|
||
(ctpl/update-page
|
||
file-data
|
||
page-id
|
||
(fn [page]
|
||
(let [root (ctst/get-shape page uuid/zero)]
|
||
(ctst/set-shape page (update root :shapes conj missing-id)))))))))
|
||
|
||
(defn- inject-broken-component
|
||
"Add a deleted component with `:objects nil` to the file.
|
||
|
||
This causes `check-component` to report a
|
||
:component-nil-objects-not-allowed error whenever that component is
|
||
validated. The component id is registered under `comp-label` in the
|
||
ids-map so callers can look it up with `(thi/id comp-label)`."
|
||
[file comp-label]
|
||
(let [comp-id (thi/new-id! comp-label)]
|
||
(ctf/update-file-data
|
||
file
|
||
(fn [file-data]
|
||
(assoc-in file-data [:components comp-id]
|
||
{:id comp-id
|
||
:name "broken-component"
|
||
:objects nil
|
||
:deleted true
|
||
:main-instance-id (uuid/next)
|
||
:main-instance-page (uuid/next)})))))
|
||
|
||
;; ----------------------------------------------------------------
|
||
;; 1. Feature gate
|
||
;; ----------------------------------------------------------------
|
||
|
||
(t/deftest validate-file-affected-no-feature
|
||
(t/testing "returns nil when file does not have the components/v2 feature"
|
||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||
(assoc :features #{}))
|
||
page-id (thi/id :page1)
|
||
changes [{:type :add-obj :page-id page-id :id (uuid/next)}]]
|
||
(t/is (nil? (cfv/validate-file-affected file {} changes))))))
|
||
|
||
;; ----------------------------------------------------------------
|
||
;; 2. Empty changes – nothing to validate
|
||
;; ----------------------------------------------------------------
|
||
|
||
(t/deftest validate-file-affected-empty-changes
|
||
(t/testing "returns nil when the changes list is empty"
|
||
(let [file (thf/sample-file :file1 :page-label :page1)]
|
||
(t/is (nil? (cfv/validate-file-affected file {} []))))))
|
||
|
||
;; ----------------------------------------------------------------
|
||
;; 3. Page-level scoping – add-obj / mod-obj / fix-obj key: :page-id
|
||
;; ----------------------------------------------------------------
|
||
|
||
(t/deftest validate-file-affected-page-scoping-misses-untouched-page
|
||
(t/testing "add-obj on page1 does not validate broken page2"
|
||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||
(thf/add-sample-page :page2))
|
||
file' (inject-broken-child file :page2)
|
||
page1-id (thi/id :page1)
|
||
;; The change only touches page1
|
||
changes [{:type :add-obj :page-id page1-id :id (uuid/next)}]]
|
||
(t/is (nil? (cfv/validate-file-affected file' {} changes)))))
|
||
|
||
(t/testing "mod-obj on page1 does not validate broken page2"
|
||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||
(thf/add-sample-page :page2))
|
||
file' (inject-broken-child file :page2)
|
||
page1-id (thi/id :page1)
|
||
changes [{:type :mod-obj :page-id page1-id :id uuid/zero
|
||
:operations [{:type :set :attr :name :val "root"}]}]]
|
||
(t/is (nil? (cfv/validate-file-affected file' {} changes)))))
|
||
|
||
(t/testing "fix-obj on page1 does not validate broken page2"
|
||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||
(thf/add-sample-page :page2))
|
||
file' (inject-broken-child file :page2)
|
||
page1-id (thi/id :page1)
|
||
changes [{:type :fix-obj :page-id page1-id :id (uuid/next)
|
||
:operations []}]]
|
||
(t/is (nil? (cfv/validate-file-affected file' {} changes))))))
|
||
|
||
(t/deftest validate-file-affected-page-scoping-catches-error-on-touched-page
|
||
(t/testing "add-obj on broken page1 surfaces the error"
|
||
(let [file (thf/sample-file :file1 :page-label :page1)
|
||
file' (inject-broken-child file :page1)
|
||
page1-id (thi/id :page1)
|
||
changes [{:type :add-obj :page-id page1-id :id (uuid/next)}]]
|
||
(let [errors (cfv/validate-file-affected file' {} changes)]
|
||
(t/is (seq errors))
|
||
(t/is (some #(= :child-not-found (:code %)) errors)))))
|
||
|
||
(t/testing "mod-obj on broken page1 surfaces the error"
|
||
(let [file (thf/sample-file :file1 :page-label :page1)
|
||
file' (inject-broken-child file :page1)
|
||
page1-id (thi/id :page1)
|
||
changes [{:type :mod-obj :page-id page1-id :id uuid/zero
|
||
:operations [{:type :set :attr :name :val "root"}]}]]
|
||
(let [errors (cfv/validate-file-affected file' {} changes)]
|
||
(t/is (seq errors))
|
||
(t/is (some #(= :child-not-found (:code %)) errors)))))
|
||
|
||
(t/testing "reg-objects on broken page1 surfaces the error"
|
||
(let [file (thf/sample-file :file1 :page-label :page1)
|
||
file' (inject-broken-child file :page1)
|
||
page1-id (thi/id :page1)
|
||
changes [{:type :reg-objects :page-id page1-id :shapes []}]]
|
||
(let [errors (cfv/validate-file-affected file' {} changes)]
|
||
(t/is (seq errors))
|
||
(t/is (some #(= :child-not-found (:code %)) errors)))))
|
||
|
||
(t/testing "mov-objects on broken page1 surfaces the error"
|
||
(let [file (thf/sample-file :file1 :page-label :page1)
|
||
file' (inject-broken-child file :page1)
|
||
page1-id (thi/id :page1)
|
||
changes [{:type :mov-objects :page-id page1-id :parent-id uuid/zero :shapes []}]]
|
||
(let [errors (cfv/validate-file-affected file' {} changes)]
|
||
(t/is (seq errors))
|
||
(t/is (some #(= :child-not-found (:code %)) errors))))))
|
||
|
||
;; ----------------------------------------------------------------
|
||
;; 4. add-page / mod-page – scoped by :id (not :page-id)
|
||
;; ----------------------------------------------------------------
|
||
|
||
(t/deftest validate-file-affected-add-page-scoped-by-id
|
||
(t/testing "add-page with :id=page2 validates page2 (broken → errors)"
|
||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||
(thf/add-sample-page :page2))
|
||
file' (inject-broken-child file :page2)
|
||
page2-id (thi/id :page2)
|
||
changes [{:type :add-page :id page2-id}]]
|
||
(let [errors (cfv/validate-file-affected file' {} changes)]
|
||
(t/is (seq errors))
|
||
(t/is (some #(= :child-not-found (:code %)) errors)))))
|
||
|
||
(t/testing "add-page with :id=page1 does not validate broken page2"
|
||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||
(thf/add-sample-page :page2))
|
||
file' (inject-broken-child file :page2)
|
||
page1-id (thi/id :page1)
|
||
changes [{:type :add-page :id page1-id}]]
|
||
(t/is (nil? (cfv/validate-file-affected file' {} changes)))))
|
||
|
||
(t/testing "mod-page with :id=page2 validates page2 (broken → errors)"
|
||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||
(thf/add-sample-page :page2))
|
||
file' (inject-broken-child file :page2)
|
||
page2-id (thi/id :page2)
|
||
changes [{:type :mod-page :id page2-id}]]
|
||
(let [errors (cfv/validate-file-affected file' {} changes)]
|
||
(t/is (seq errors))
|
||
(t/is (some #(= :child-not-found (:code %)) errors))))))
|
||
|
||
;; ----------------------------------------------------------------
|
||
;; 5. del-obj – shape-level op scoped by :page-id
|
||
;; del-page / del-component / mov-page / purge-component – no-ops
|
||
;; ----------------------------------------------------------------
|
||
|
||
(t/deftest validate-file-affected-del-obj-scopes-page
|
||
(t/testing "del-obj scopes its :page-id just like add-obj / mod-obj"
|
||
;; del-obj is in the same shape-level ops bucket and scopes by :page-id.
|
||
(let [file (thf/sample-file :file1 :page-label :page1)
|
||
file' (inject-broken-child file :page1)
|
||
page1-id (thi/id :page1)
|
||
changes [{:type :del-obj :page-id page1-id :id (uuid/next)}]]
|
||
;; page1 has an error and del-obj touches page1 → error surfaced
|
||
(let [errors (cfv/validate-file-affected file' {} changes)]
|
||
(t/is (seq errors))
|
||
(t/is (some #(= :child-not-found (:code %)) errors)))))
|
||
|
||
(t/testing "del-obj on page1 does not validate broken page2"
|
||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||
(thf/add-sample-page :page2))
|
||
file' (inject-broken-child file :page2)
|
||
page1-id (thi/id :page1)
|
||
changes [{:type :del-obj :page-id page1-id :id (uuid/next)}]]
|
||
(t/is (nil? (cfv/validate-file-affected file' {} changes))))))
|
||
|
||
(t/deftest validate-file-affected-noop-change-types
|
||
(t/testing "del-page produces no affected entries"
|
||
(let [file (thf/sample-file :file1 :page-label :page1)
|
||
file' (inject-broken-child file :page1)
|
||
page1-id (thi/id :page1)
|
||
changes [{:type :del-page :id page1-id}]]
|
||
(t/is (nil? (cfv/validate-file-affected file' {} changes)))))
|
||
|
||
(t/testing "del-component produces no affected entries"
|
||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||
(inject-broken-component :comp1))
|
||
comp-id (thi/id :comp1)
|
||
changes [{:type :del-component :id comp-id}]]
|
||
(t/is (nil? (cfv/validate-file-affected file {} changes)))))
|
||
|
||
(t/testing "mov-page produces no affected entries"
|
||
(let [file (thf/sample-file :file1 :page-label :page1)
|
||
file' (inject-broken-child file :page1)
|
||
page1-id (thi/id :page1)
|
||
changes [{:type :mov-page :id page1-id}]]
|
||
(t/is (nil? (cfv/validate-file-affected file' {} changes)))))
|
||
|
||
(t/testing "purge-component produces no affected entries"
|
||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||
(inject-broken-component :comp1))
|
||
comp-id (thi/id :comp1)
|
||
changes [{:type :purge-component :id comp-id}]]
|
||
(t/is (nil? (cfv/validate-file-affected file {} changes)))))
|
||
|
||
(t/testing "add-color (library change) produces no affected entries"
|
||
(let [file (thf/sample-file :file1 :page-label :page1)
|
||
file' (inject-broken-child file :page1)
|
||
changes [{:type :add-color :id (uuid/next) :color {}}]]
|
||
(t/is (nil? (cfv/validate-file-affected file' {} changes)))))
|
||
|
||
(t/testing "mod-color (library change) produces no affected entries"
|
||
(let [file (thf/sample-file :file1 :page-label :page1)
|
||
file' (inject-broken-child file :page1)
|
||
changes [{:type :mod-color :id (uuid/next) :color {}}]]
|
||
(t/is (nil? (cfv/validate-file-affected file' {} changes))))))
|
||
|
||
;; ----------------------------------------------------------------
|
||
;; 6. add-component / mod-component – scoped by :id (component-id)
|
||
;; ----------------------------------------------------------------
|
||
|
||
(t/deftest validate-file-affected-add-component-scoped-by-id
|
||
(t/testing "add-component :id=comp1 validates comp1 (broken → errors)"
|
||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||
(inject-broken-component :comp1))
|
||
comp-id (thi/id :comp1)
|
||
changes [{:type :add-component :id comp-id}]]
|
||
(let [errors (cfv/validate-file-affected file {} changes)]
|
||
(t/is (seq errors))
|
||
(t/is (some #(= :component-nil-objects-not-allowed (:code %)) errors)))))
|
||
|
||
(t/testing "add-component :id=other-id does not validate broken comp1"
|
||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||
(inject-broken-component :comp1))
|
||
_ (thi/id :comp1)
|
||
changes [{:type :add-component :id (uuid/next)}]]
|
||
(t/is (nil? (cfv/validate-file-affected file {} changes)))))
|
||
|
||
(t/testing "mod-component :id=comp1 validates comp1 (broken → errors)"
|
||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||
(inject-broken-component :comp1))
|
||
comp-id (thi/id :comp1)
|
||
changes [{:type :mod-component :id comp-id}]]
|
||
(let [errors (cfv/validate-file-affected file {} changes)]
|
||
(t/is (seq errors))
|
||
(t/is (some #(= :component-nil-objects-not-allowed (:code %)) errors))))))
|
||
|
||
;; ----------------------------------------------------------------
|
||
;; 7. restore-component – scopes BOTH :id (component) and :page-id
|
||
;; ----------------------------------------------------------------
|
||
|
||
(t/deftest validate-file-affected-restore-component-scopes-page
|
||
(t/testing "restore-component touches its :page-id (broken page → errors)"
|
||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||
(thf/add-sample-page :page2))
|
||
file' (inject-broken-child file :page1)
|
||
page1-id (thi/id :page1)
|
||
changes [{:type :restore-component :id (uuid/next) :page-id page1-id}]]
|
||
(let [errors (cfv/validate-file-affected file' {} changes)]
|
||
(t/is (seq errors))
|
||
(t/is (some #(= :child-not-found (:code %)) errors)))))
|
||
|
||
(t/testing "restore-component does not validate a page it does not reference"
|
||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||
(thf/add-sample-page :page2))
|
||
file' (inject-broken-child file :page2)
|
||
page1-id (thi/id :page1)
|
||
changes [{:type :restore-component :id (uuid/next) :page-id page1-id}]]
|
||
;; page2 has an error but the change only touches page1
|
||
(t/is (nil? (cfv/validate-file-affected file' {} changes))))))
|
||
|
||
(t/deftest validate-file-affected-restore-component-scopes-component
|
||
(t/testing "restore-component touches its component :id (broken component → errors)"
|
||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||
(inject-broken-component :comp1))
|
||
comp-id (thi/id :comp1)
|
||
page1-id (thi/id :page1)
|
||
changes [{:type :restore-component :id comp-id :page-id page1-id}]]
|
||
(let [errors (cfv/validate-file-affected file {} changes)]
|
||
(t/is (seq errors))
|
||
(t/is (some #(= :component-nil-objects-not-allowed (:code %)) errors))))))
|
||
|
||
;; ----------------------------------------------------------------
|
||
;; 8. Mixed changes – union of affected entities
|
||
;; ----------------------------------------------------------------
|
||
|
||
(t/deftest validate-file-affected-mixed-changes-union
|
||
(t/testing "two changes on different pages: both pages are validated"
|
||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||
(thf/add-sample-page :page2))
|
||
;; page2 is broken; page1 is clean
|
||
file' (inject-broken-child file :page2)
|
||
page1-id (thi/id :page1)
|
||
page2-id (thi/id :page2)
|
||
;; Both pages are touched
|
||
changes [{:type :add-obj :page-id page1-id :id (uuid/next)}
|
||
{:type :mod-obj :page-id page2-id :id uuid/zero
|
||
:operations []}]]
|
||
(let [errors (cfv/validate-file-affected file' {} changes)]
|
||
;; page2 is validated → error surfaced
|
||
(t/is (seq errors))
|
||
(t/is (some #(= :child-not-found (:code %)) errors)))))
|
||
|
||
(t/testing "del-page (true no-op) mixed with add-obj on page1: only page1 is validated"
|
||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||
(thf/add-sample-page :page2))
|
||
;; page2 is broken; page1 is clean
|
||
file' (inject-broken-child file :page2)
|
||
page1-id (thi/id :page1)
|
||
page2-id (thi/id :page2)
|
||
;; del-page on page2 is a no-op; add-obj on page1 scopes page1 only
|
||
changes [{:type :del-page :id page2-id}
|
||
{:type :add-obj :page-id page1-id :id (uuid/next)}]]
|
||
;; page2's error is NOT surfaced because del-page produces no scope
|
||
(t/is (nil? (cfv/validate-file-affected file' {} changes)))))
|
||
|
||
(t/testing "duplicate page-ids in changes are deduplicated (page validated once)"
|
||
(let [file (thf/sample-file :file1 :page-label :page1)
|
||
file' (inject-broken-child file :page1)
|
||
page1-id (thi/id :page1)
|
||
;; Three changes all touching the same page
|
||
changes [{:type :add-obj :page-id page1-id :id (uuid/next)}
|
||
{:type :mod-obj :page-id page1-id :id uuid/zero :operations []}
|
||
{:type :del-obj :page-id page1-id :id (uuid/next)}]]
|
||
;; del-obj excluded, add-obj + mod-obj both scope page1; error surfaced once
|
||
(let [errors (cfv/validate-file-affected file' {} changes)]
|
||
(t/is (seq errors))
|
||
;; There should be exactly one :child-not-found (not duplicated)
|
||
(t/is (= 1 (count (filter #(= :child-not-found (:code %)) errors))))))))
|
||
|
||
;; ----------------------------------------------------------------
|
||
;; 9. reorder-children and component-context changes
|
||
;; ----------------------------------------------------------------
|
||
|
||
(t/deftest validate-file-affected-reorder-children
|
||
(t/testing "reorder-children on broken page surfaces the error"
|
||
(let [file (thf/sample-file :file1 :page-label :page1)
|
||
file' (inject-broken-child file :page1)
|
||
page1-id (thi/id :page1)
|
||
changes [{:type :reorder-children :page-id page1-id :id uuid/zero :shapes []}]]
|
||
(let [errors (cfv/validate-file-affected file' {} changes)]
|
||
(t/is (seq errors))
|
||
(t/is (some #(= :child-not-found (:code %)) errors))))))
|
||
|
||
(t/deftest validate-file-affected-obj-in-component
|
||
(t/testing "add-obj with :component-id (not :page-id) scopes a component"
|
||
;; When a shape change carries :component-id instead of :page-id it
|
||
;; means the change happened inside a deleted component's object tree.
|
||
;; extract-affected-ids routes it to :component-ids.
|
||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||
(inject-broken-component :comp1))
|
||
comp-id (thi/id :comp1)
|
||
changes [{:type :add-obj :component-id comp-id :id (uuid/next)}]]
|
||
(let [errors (cfv/validate-file-affected file {} changes)]
|
||
(t/is (seq errors))
|
||
(t/is (some #(= :component-nil-objects-not-allowed (:code %)) errors)))))
|
||
|
||
(t/testing "add-obj with :component-id does not scope an unrelated component"
|
||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||
(inject-broken-component :comp1))
|
||
_ (thi/id :comp1)
|
||
changes [{:type :add-obj :component-id (uuid/next) :id (uuid/next)}]]
|
||
(t/is (nil? (cfv/validate-file-affected file {} changes))))))
|
||
|
||
;; ----------------------------------------------------------------
|
||
;; 10. validate-file-affected! – raises vs returns nil
|
||
;; ----------------------------------------------------------------
|
||
|
||
(t/deftest validate-file-affected!-returns-nil-on-clean-file
|
||
(t/testing "returns nil when the file has no errors in the touched page"
|
||
(let [file (thf/sample-file :file1 :page-label :page1)
|
||
page1-id (thi/id :page1)
|
||
changes [{:type :add-obj :page-id page1-id :id (uuid/next)}]]
|
||
(t/is (nil? (cfv/validate-file-affected! file {} changes))))))
|
||
|
||
(t/deftest validate-file-affected!-returns-nil-empty-changes
|
||
(t/testing "returns nil when there are no changes (no pages validated)"
|
||
(let [file (thf/sample-file :file1 :page-label :page1)]
|
||
(t/is (nil? (cfv/validate-file-affected! file {} []))))))
|
||
|
||
(t/deftest validate-file-affected!-raises-on-error
|
||
(t/testing "raises an exception when the touched page has validation errors"
|
||
(let [file (thf/sample-file :file1 :page-label :page1)
|
||
file' (inject-broken-child file :page1)
|
||
page1-id (thi/id :page1)
|
||
changes [{:type :mod-obj :page-id page1-id :id uuid/zero
|
||
:operations []}]]
|
||
(t/is (thrown? #?(:clj Exception :cljs js/Error)
|
||
(cfv/validate-file-affected! file' {} changes)))))
|
||
|
||
(t/testing "raised exception is of :validation type with :referential-integrity code"
|
||
(let [file (thf/sample-file :file1 :page-label :page1)
|
||
file' (inject-broken-child file :page1)
|
||
page1-id (thi/id :page1)
|
||
changes [{:type :mod-obj :page-id page1-id :id uuid/zero
|
||
:operations []}]]
|
||
(try
|
||
(cfv/validate-file-affected! file' {} changes)
|
||
(t/is false "expected exception to be thrown")
|
||
(catch #?(:clj clojure.lang.ExceptionInfo :cljs cljs.core.ExceptionInfo) e
|
||
(let [data (ex-data e)]
|
||
(t/is (= :validation (:type data)))
|
||
(t/is (= :referential-integrity (:code data)))
|
||
(t/is (seq (:details data)))))))))
|