mirror of
https://github.com/penpot/penpot.git
synced 2026-11-05 01:20:38 +00:00
update-file checked the schema of the whole file on every save, which loads and decodes every page even when the changes touch one. On files with several pages that was most of the time of a save. validate-file-schema-affected! checks the file data with the pages index reduced to the pages the changes modify; the data outside pages is still checked whole, and a change type it does not know makes it check the whole file. update-file uses it for the strict and the soft schema check. A save of 10 commits on a file of 10 pages of 2k shapes goes from ~1.1 s to ~240 ms; a single page file costs the same.
519 lines
25 KiB
Clojure
519 lines
25 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 SUBSIDIARY SL
|
||
|
||
(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
|
||
- validate-file-schema-affected! – schema check of the touched pages
|
||
|
||
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-invalid-page
|
||
"Give the page identified by `page-label` a name that is not a string,
|
||
so the page fails the file schema check."
|
||
[file page-label]
|
||
(ctf/update-file-data
|
||
file
|
||
(fn [file-data]
|
||
(ctpl/update-page file-data (thi/id page-label) #(assoc % :name 42)))))
|
||
|
||
(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)))))))))
|
||
|
||
;; ----------------------------------------------------------------
|
||
;; 11. validate-file-schema-affected! – schema of the touched pages
|
||
;; ----------------------------------------------------------------
|
||
|
||
(defn- schema-error?
|
||
[file changes]
|
||
(try
|
||
(cfv/validate-file-schema-affected! file changes)
|
||
false
|
||
(catch #?(:clj clojure.lang.ExceptionInfo :cljs cljs.core.ExceptionInfo) _
|
||
true)))
|
||
|
||
(t/deftest validate-file-schema-affected!-returns-the-file
|
||
(let [file (thf/sample-file :file1 :page-label :page1)
|
||
changes [{:type :mod-obj :page-id (thi/id :page1) :id uuid/zero :operations []}]]
|
||
(t/is (= file (cfv/validate-file-schema-affected! file changes)))))
|
||
|
||
(t/deftest validate-file-schema-affected!-checks-touched-pages
|
||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||
(thf/add-sample-page :page2)
|
||
(inject-invalid-page :page2))]
|
||
|
||
(t/testing "a shape change on the invalid page raises"
|
||
(t/is (schema-error? file [{:type :mod-obj :page-id (thi/id :page2) :id uuid/zero :operations []}])))
|
||
|
||
(t/testing "mod-page on the invalid page raises"
|
||
(t/is (schema-error? file [{:type :mod-page :id (thi/id :page2) :name "page2"}])))
|
||
|
||
(t/testing "page plugin data on the invalid page raises"
|
||
(t/is (schema-error? file [{:type :set-plugin-data :object-type :page :object-id (thi/id :page2)
|
||
:namespace "ns" :key "k" :value "v"}])))
|
||
|
||
(t/testing "a guide on the invalid page raises"
|
||
(t/is (schema-error? file [{:type :set-guide :page-id (thi/id :page2) :id (uuid/next) :params nil}])))))
|
||
|
||
(t/deftest validate-file-schema-affected!-skips-untouched-pages
|
||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||
(thf/add-sample-page :page2)
|
||
(inject-invalid-page :page2))]
|
||
|
||
(t/testing "a shape change on another page does not check the invalid page"
|
||
(t/is (not (schema-error? file [{:type :mod-obj :page-id (thi/id :page1) :id uuid/zero :operations []}]))))
|
||
|
||
(t/testing "a file level change does not check the invalid page"
|
||
(t/is (not (schema-error? file [{:type :del-color :id (uuid/next)}]))))))
|
||
|
||
(t/deftest validate-file-schema-affected!-checks-the-rest-of-the-file
|
||
(let [color-id (uuid/next)
|
||
file (-> (thf/sample-file :file1 :page-label :page1)
|
||
(ctf/update-file-data #(assoc-in % [:colors color-id] {:id color-id :name 42})))]
|
||
(t/is (schema-error? file [{:type :mod-obj :page-id (thi/id :page1) :id uuid/zero :operations []}]))))
|
||
|
||
(t/deftest validate-file-schema-affected!-checks-everything-for-unknown-changes
|
||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||
(thf/add-sample-page :page2)
|
||
(inject-invalid-page :page2))]
|
||
(t/is (schema-error? file [{:type :mod-obj :page-id (thi/id :page1) :id uuid/zero :operations []}
|
||
{:type :some-future-change}]))))
|