penpot/frontend/test/frontend_tests/plugins/read_only_test.cljs
alonso.torres 4c9d30ef6a ✨ Make plugin writes fail while the workspace is read-only
A read-only workspace (saves failing, a user who cannot edit, a version
preview) only hid the editing tools: plugins, the MCP among them, still
changed shapes, and those edits were never saved. Every plugin write now
throws a read-only error, whatever the throwValidationErrors flag, the
same way a write to a removed shape does.

`check-permission` with a write permission runs the new `check-writable`,
which covers most writers. Writers without a permission check (most
context methods such as create*, group, align or flatten, the z-order
methods, applyTypography on a text range, removing a ruler guide) call it
themselves, and shape setters get it through `check-shape-writable`.
Reads, comments and undo blocks keep working, so a block opened before a
lock still closes.

The MCP instructions tell the agent what the read-only error means.
2026-11-03 11:17:34 +01:00

94 lines
3.4 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 frontend-tests.plugins.read-only-test
"Plugin writes in a read-only workspace."
(:require
[app.common.test-helpers.files :as cthf]
[app.main.data.helpers :as dsh]
[app.main.store :as st]
[app.plugins.api :as api]
[app.plugins.register :as r]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.state :as ths]
[frontend-tests.helpers.wasm :as thw]))
;; Its validation flag is off, so only errors that always throw reach it
(def ^:private plugin-id "00000000-0000-0000-0000-000000000000")
(defn- set-read-only
[store read-only?]
(swap! store assoc-in [:workspace-global :read-only?] read-only?))
(defn- shape-count
[store]
(count (dsh/lookup-page-objects @store)))
(defn- with-context
"Calls `f` with a store holding a sample file and a plugin context on it."
[f]
(thw/with-wasm-mocks*
(fn []
(let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))]
(set! st/state store)
(f store (api/create-context plugin-id))))))
(t/deftest writes-throw-in-a-read-only-workspace
(with-context
(fn [store ^js context]
(let [^js board (.createBoard context)]
(set-read-only store true)
(t/testing "a shape setter"
(t/is (thrown-with-msg? js/Error #"read-only" (set! (.-name board) "Renamed")))
(t/is (not= "Renamed" (.-name board))))
(t/testing "a shape method"
(t/is (thrown-with-msg? js/Error #"read-only" (.bringToFront board))))
(t/testing "a context method without a permission check"
(let [before (shape-count store)]
(t/is (thrown-with-msg? js/Error #"read-only" (.createRectangle context)))
(t/is (= before (shape-count store)))))
(t/testing "a context method with a permission check"
(t/is (thrown-with-msg? js/Error #"read-only" (.createPage (.-currentFile context)))))))))
(t/deftest writes-work-again-once-the-workspace-is-editable
(with-context
(fn [store ^js context]
(let [^js board (.createBoard context)]
(set-read-only store true)
(set-read-only store false)
(set! (.-name board) "Renamed")
(t/is (= "Renamed" (.-name board)))))))
(t/deftest a-save-failure-lock-says-why-the-workspace-is-read-only
(with-context
(fn [store ^js context]
(swap! store update :workspace-global assoc
:read-only? true
:persistence-lock {:read-only? false})
(t/is (thrown-with-msg? js/Error #"cannot be saved" (.createBoard context))))))
(t/deftest reads-and-undo-blocks-work-in-a-read-only-workspace
(with-context
(fn [store ^js context]
(let [^js board (.createBoard context)]
(set-read-only store true)
(t/is (string? (.-name board)))
(let [block (.undoBlockBegin (.-history context))]
(t/is (some? block))
(t/is (nil? (.undoBlockFinish (.-history context) block))))))))
(t/deftest read-only-keeps-permission-checks-of-reads
(with-context
(fn [store _]
(set-read-only store true)
(t/is (true? (r/check-permission plugin-id "content:read")))
(t/is (true? (r/has-permission? plugin-id "content:write")))
(t/is (thrown? js/Error (r/check-permission plugin-id "library:write"))))))