Andrey Antukh 37743e6d46 🐛 Add content:write permission checks to Design Tokens plugin API
The Design Tokens API (tokens.cljs) had zero permission checks, allowing
any plugin to create, modify, and delete tokens, sets, and themes
regardless of granted permissions.

Add r/check-permission checks to all 22 write operations across:
- token-proxy: name, value, description, duplicate, remove, applyToken
- token-set-proxy: name, active, toggleActive, addToken, duplicate, remove
- token-theme-proxy: group, name, active, toggleActive, addSet, removeSet,
  duplicate, remove
- tokens-catalog: addTheme, addSet

Follows the established pattern from comments.cljs, file.cljs, page.cljs.

Closes #11137

AI-assisted-by: qwen3.7-plus
2026-08-07 11:53:21 +00:00

692 lines
34 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 frontend-tests.plugins.tokens-test
(:require
[app.common.test-helpers.compositions :as ctho]
[app.common.test-helpers.files :as cthf]
[app.common.test-helpers.ids-map :as cthi]
[app.common.test-helpers.tokens :as ctht]
[app.common.types.tokens-lib :as ctob]
[app.common.uuid :as uuid]
[app.main.data.tokenscript :as ts]
[app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.store :as st]
[app.plugins.api :as api]
[app.plugins.register :as r]
[app.plugins.tokens :as ptok]
[app.plugins.utils :as u]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.mock :as mock]
[frontend-tests.helpers.state :as ths]
[potok.v2.core :as ptk]))
(t/use-fixtures :each {:before cthi/reset-idmap!})
(def ^:private get-resolved-value @#'ptok/get-resolved-value)
;; Regression coverage for issue #9162.
;;
;; Plugin code calling `shape.applyToken(token, ["fill"])` or
;; `token.applyToShapes([rect], ["fill"])` from JavaScript supplies a JS
;; array of strings. Penpot's plugin proxies expect a Clojure set of
;; keywords. Two coupled defects made these calls silently no-op (or, with
;; `throwValidationErrors` enabled, throw a "check error"):
;;
;; 1. `token-attr-plugin->token-attr` only consulted its alias map when
;; the input was already a keyword — string inputs like "fill" or
;; "border-radius-top-left" fell through to the identity branch
;; unchanged, so the downstream `cto/token-attr?` predicate (which
;; checks against a set of keywords) returned false.
;; 2. The `applyToken` / `applyToShapes` / `applyToSelected` schemas used
;; plain `[:set ...]`, which does not have a `:decode/json`
;; transformer for the JS array → Clojure set coercion. Penpot's
;; custom `[::sm/set ...]` does. Switching to the registered set type
;; lets the standard JSON decoder pipeline turn the JS argument into
;; a set of strings, after which the `[:and ::sm/keyword [:fn
;; token-attr?]]` element schema coerces each string to a keyword and
;; validates it.
;;
;; These helper-level tests pin the string-friendly conversion contract;
;; the schema-level fix is covered by the existing plugin integration
;; suite that exercises `applyToken` end-to-end.
(t/deftest token-attr-plugin->token-attr-passes-canonical-form-through
;; Both already-canonical short names and unaliased names pass through
;; unchanged.
(t/is (= :fill (ptok/token-attr-plugin->token-attr :fill)))
(t/is (= :stroke-color (ptok/token-attr-plugin->token-attr :stroke-color)))
(t/is (= :r1 (ptok/token-attr-plugin->token-attr :r1)))
(t/is (= :p2 (ptok/token-attr-plugin->token-attr :p2))))
(t/deftest token-attr-plugin->token-attr-resolves-verbose-plugin-aliases
;; Plugin-side verbose names (e.g. `:border-radius-top-left`) map to
;; their canonical short internal form (`:r1`) so plugin authors can
;; spell the corner explicitly without the engine having to know both.
(t/is (= :r1 (ptok/token-attr-plugin->token-attr :border-radius-top-left)))
(t/is (= :r2 (ptok/token-attr-plugin->token-attr :border-radius-top-right)))
(t/is (= :r3 (ptok/token-attr-plugin->token-attr :border-radius-bottom-right)))
(t/is (= :r4 (ptok/token-attr-plugin->token-attr :border-radius-bottom-left))))
(t/deftest token-attr-plugin->token-attr-resolves-padding-margin-side-aliases
(t/is (= :p1 (ptok/token-attr-plugin->token-attr :padding-top)))
(t/is (= :p2 (ptok/token-attr-plugin->token-attr :padding-right)))
(t/is (= :p3 (ptok/token-attr-plugin->token-attr :padding-bottom)))
(t/is (= :p4 (ptok/token-attr-plugin->token-attr :padding-left)))
(t/is (= :m1 (ptok/token-attr-plugin->token-attr :margin-top)))
(t/is (= :m2 (ptok/token-attr-plugin->token-attr :margin-right)))
(t/is (= :m3 (ptok/token-attr-plugin->token-attr :margin-bottom)))
(t/is (= :m4 (ptok/token-attr-plugin->token-attr :margin-left))))
(t/deftest token-attr-plugin->token-attr-coerces-string-input
;; This is the actual regression — JS plugin calls supply strings.
(t/is (= :fill (ptok/token-attr-plugin->token-attr "fill")))
(t/is (= :stroke-color (ptok/token-attr-plugin->token-attr "stroke-color")))
;; Verbose plugin aliases work via the string path too.
(t/is (= :r1 (ptok/token-attr-plugin->token-attr "border-radius-top-left")))
(t/is (= :m3 (ptok/token-attr-plugin->token-attr "margin-bottom"))))
(t/deftest token-attr?-accepts-keyword-input
(t/is (true? (boolean (ptok/token-attr? :fill))))
(t/is (true? (boolean (ptok/token-attr? :stroke-color))))
(t/is (true? (boolean (ptok/token-attr? :r1))))
(t/is (true? (boolean (ptok/token-attr? :p2)))))
(t/deftest token-attr?-accepts-string-input
;; Same JS-array-of-strings reproducer as the issue, exercised at the
;; predicate layer the plugin schemas call into.
(t/is (true? (boolean (ptok/token-attr? "fill"))))
(t/is (true? (boolean (ptok/token-attr? "stroke-color"))))
(t/is (true? (boolean (ptok/token-attr? "r1"))))
(t/is (true? (boolean (ptok/token-attr? "m3")))))
(t/deftest shape-apply-token-accepts-padding-top
(t/async
done
(let [set-id (cthi/new-id! :token-set)
token-id (cthi/new-id! :spacing-token)
file (-> (cthf/sample-file :file1 :page-label :page1)
(ctho/add-frame :frame1 {:layout :flex})
(ctht/add-tokens-lib)
(ctht/update-tokens-lib
#(-> %
(ctob/add-set
(ctob/make-token-set :id set-id
:name "spacing"))
(ctob/add-theme
(ctob/make-token-theme :name "theme"
:sets #{"spacing"}))
(ctob/set-active-themes #{"/theme"})
(ctob/add-token
set-id
(ctob/make-token :id token-id
:name "spacing.medium"
:type :spacing
:value 16)))))
store (ths/setup-store file)
_ (set! st/state store)
_ (set! st/stream (ptk/input-stream store))
^js context (api/create-context "00000000-0000-0000-0000-000000000000")
^js page (.-currentPage context)
^js shape (.getShapeById page (str (cthi/id :frame1)))
^js library (.-library context)
^js local (.-local library)
^js catalog (.-tokens local)
^js token-set (.getSetById catalog (str set-id))
^js token (.getTokenById token-set (str token-id))]
(.applyToken shape token #js ["paddingTop"])
(js/setTimeout
(fn []
(let [shape-id (cthi/id :frame1)
page-id (cthf/current-page-id file)]
(t/is (= "spacing.medium" (.. shape -tokens -paddingTop)))
(t/is (= "spacing.medium"
(get-in @store
[:files (:id file) :data :pages-index page-id
:objects shape-id :applied-tokens :p1])))
(done)))
0))))
(t/deftest token-attr?-rejects-unknown-input
(t/is (false? (boolean (ptok/token-attr? :not-a-real-attr))))
(t/is (false? (boolean (ptok/token-attr? "not-a-real-attr"))))
(t/is (false? (boolean (ptok/token-attr? nil)))))
;; Regression coverage for issue #10070.
;;
;; The Plugin API's `addToken` rejected reference tokens whose target
;; lives in an *inactive* token set, even though the referenced token
;; exists structurally. The proxy `:fn` resolved the new token against
;; active sets only (`get-tokens-in-active-sets`), so a reference into an
;; inactive set never resolved and fell into the generic `not-valid`
;; error path.
;;
;; The fix resolves against *all* tokens in the library (inactive sets
;; included), mirroring the workspace token-creation form. These tests
;; reproduce the exact `tokens-tree` construction from both the buggy and
;; the fixed `addToken` `:fn` and assert resolution behaviour directly —
;; the proxy `:fn` itself drives the global store and `st/emit!`, so it is
;; not unit-testable, but the resolve step it gates on is.
(defn- inactive-set-library
"A library with `primitives` (holding `color.gray.50`) left inactive and
an active, empty `semantic` set — the repro from the issue."
[]
(-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id (cthi/new-id! :primitives)
:name "primitives"))
(ctob/add-set (ctob/make-token-set :id (cthi/new-id! :semantic)
:name "semantic"))
(ctob/add-token (cthi/id :primitives)
(ctob/make-token {:name "color.gray.50"
:value "#fafafa"
:type :color}))
;; `add-set` does not activate sets, so activate only `semantic`,
;; leaving `primitives` (the reference target) inactive.
(ctob/toggle-set-in-theme ctob/hidden-theme-id "semantic")))
(t/deftest add-token-active-sets-only-fails-to-resolve-cross-set-reference
;; Demonstrates the bug: resolving the new token against active sets
;; only leaves the reference unresolved.
(let [tokens-lib (inactive-set-library)
token (ctob/make-token {:name "color.bg.default"
:value "{color.gray.50}"
:type :color})
tokens-tree (-> (ctob/get-tokens-in-active-sets tokens-lib)
(assoc (:name token) token))
resolved (ts/resolve-tokens tokens-tree)
{:keys [errors resolved-value]} (get resolved (:name token))]
(t/is (nil? resolved-value))
(t/is (seq errors))))
(t/deftest add-token-resolves-cross-set-reference-into-inactive-set
;; The fix: resolving against all tokens in the library (inactive sets
;; included) resolves the reference even though `primitives` is inactive.
(let [tokens-lib (inactive-set-library)
token (ctob/make-token {:name "color.bg.default"
:value "{color.gray.50}"
:type :color})
tokens-tree (-> (merge (ctob/get-all-tokens-map tokens-lib)
(ctob/get-tokens tokens-lib (cthi/id :semantic)))
(assoc (:name token) token))
resolved (ts/resolve-tokens tokens-tree)
{:keys [errors resolved-value]} (get resolved (:name token))]
(t/is (some? resolved-value))
(t/is (empty? errors))))
(t/deftest add-token-still-fails-for-references-missing-from-every-set
;; A reference to a token that exists in *no* set must still fail, even
;; with the all-tokens resolution.
(let [tokens-lib (inactive-set-library)
token (ctob/make-token {:name "color.bg.default"
:value "{color.does.not.exist}"
:type :color})
tokens-tree (-> (merge (ctob/get-all-tokens-map tokens-lib)
(ctob/get-tokens tokens-lib (cthi/id :semantic)))
(assoc (:name token) token))
resolved (ts/resolve-tokens tokens-tree)
{:keys [errors resolved-value]} (get resolved (:name token))]
(t/is (nil? resolved-value))
(t/is (seq errors))))
(t/deftest token-set-duplicate-returns-the-duplicated-set
(let [file-id (cthi/new-id! :file)
set-id (cthi/new-id! :set)
dup-id (cthi/new-id! :dup)
proxy (ptok/token-set-proxy "plugin-id" file-id set-id)]
(with-redefs [r/check-permission (constantly true)
dwtl/duplicate-token-set
(mock/stub (fn [id {:keys [id-ref]}]
(t/is (= set-id id))
(reset! id-ref dup-id)
:duplicate-token-set))
st/emit! mock/noop]
(let [dup (.duplicate proxy)]
(t/is (ptok/token-set-proxy? dup))
(t/is (= (str dup-id) (.-id dup)))))))
(t/deftest theme-add-set-and-remove-set-use-the-set-name
(let [file-id (cthi/new-id! :file)
theme-id (cthi/new-id! :theme)
set-id (cthi/new-id! :set)
set (ptok/token-set-proxy "plugin-id" file-id set-id "Primitives")
theme (ptok/token-theme-proxy "plugin-id" file-id theme-id)
captured (atom [])]
(with-redefs [r/check-permission (constantly true)
u/locate-token-theme
(fn [_file _theme]
(ctob/make-token-theme :id theme-id
:name "Theme"
:sets #{"Primitives"}))
dwtl/update-token-theme
(fn [id theme]
(swap! captured conj {:id id :theme theme})
:update-token-theme)
st/emit! mock/noop]
(.addSet theme set)
(.removeSet theme set)
(t/is (= [theme-id theme-id] (mapv :id @captured)))
(t/is (contains? (-> @captured first :theme :sets) "Primitives"))
(t/is (not (contains? (-> @captured second :theme :sets) "Primitives"))))))
(t/deftest font-family-token-value-accepts-a-string
(let [file-id (cthi/new-id! :file)
set-id (cthi/new-id! :set)
token-id (cthi/new-id! :token)
captured (atom nil)]
(with-redefs [r/check-permission (constantly true)
u/locate-token (constantly {:id token-id
:name "font.primary"
:type :font-family
:value ["Inter"]})
dwtl/update-token (mock/stub (fn [set-id token-id attrs]
(reset! captured {:set-id set-id
:token-id token-id
:attrs attrs})
:update-token))
st/emit! mock/noop]
(let [token (ptok/token-proxy "plugin-id" file-id set-id token-id)]
(set! (.-value token) "Inter, Arial")
(t/is (= set-id (:set-id @captured)))
(t/is (= token-id (:token-id @captured)))
(t/is (= ["Inter" "Arial"] (get-in @captured [:attrs :value])))))))
(t/deftest typography-token-resolved-value-is-plugin-array-shape
(let [token (ctob/make-token
{:name "type.body"
:type :typography
:value {:font-family ["Inter" "Arial"]
:font-size "16px"
:font-weight "600"
:line-height "20px"
:letter-spacing "1"
:text-case "uppercase"
:text-decoration "underline"}})
result (get-resolved-value token {(:name token) token})
entry (aget result 0)]
(t/is (array? result))
(t/is (= ["Inter" "Arial"] (vec (aget entry "fontFamilies"))))
(t/is (= 16 (aget entry "fontSizes")))
(t/is (= "600" (aget entry "fontWeights")))
(t/is (= 20 (aget entry "lineHeight")))
(t/is (= "uppercase" (aget entry "textCase")))
(t/is (= "underline" (aget entry "textDecoration")))))
(t/deftest shadow-token-resolved-value-is-plugin-array-shape
(let [token (ctob/make-token
{:name "shadow.card"
:type :shadow
:value [{:offset-x "1px"
:offset-y "2px"
:blur "3px"
:spread "4px"
:color "#000000"
:inset false}]})
result (get-resolved-value token {(:name token) token})
entry (aget result 0)]
(t/is (array? result))
(t/is (= 1 (aget entry "offsetX")))
(t/is (= 2 (aget entry "offsetY")))
(t/is (= 3 (aget entry "blur")))
(t/is (= 4 (aget entry "spread")))))
(t/deftest font-family-token-resolved-value-is-string-array
(let [token (ctob/make-token
{:name "font.primary"
:type :font-family
:value ["Inter" "Arial"]})
result (get-resolved-value token {(:name token) token})]
(t/is (array? result))
(t/is (= ["Inter" "Arial"] (vec result)))))
(t/deftest token-theme-add-set-accepts-token-set-id
(let [plugin-id "plugin-id"
file-id (uuid/next)
theme-id (uuid/next)
set-id (uuid/next)
token-set (ctob/make-token-set :id set-id :name "Core")
theme (ctob/make-token-theme :id theme-id :group "mode" :name "Light")
emitted (atom [])
invalid (atom [])]
(with-redefs [r/check-permission (constantly true)
u/locate-token-set (fn [_ id] (when (= id set-id) token-set))
u/locate-token-theme (fn [_ id] (when (= id theme-id) theme))
u/not-valid (fn [_ code value] (swap! invalid conj [code value]))
dwtl/update-token-theme (fn [id theme] {:id id :theme theme})
st/emit! (fn ([event] (swap! emitted conj event) nil)
([event & _] (swap! emitted conj event) nil))]
(let [theme-proxy (ptok/token-theme-proxy plugin-id file-id theme-id)]
(.addSet theme-proxy (str set-id))
(t/is (= #{"Core"} (-> @emitted first :theme :sets)))
(t/is (empty? @invalid))))))
(t/deftest token-theme-add-set-accepts-token-set-proxy
(let [plugin-id "plugin-id"
file-id (uuid/next)
theme-id (uuid/next)
set-id (uuid/next)
token-set (ctob/make-token-set :id set-id :name "Core")
theme (ctob/make-token-theme :id theme-id :group "mode" :name "Light")
emitted (atom [])
invalid (atom [])]
(with-redefs [r/check-permission (constantly true)
u/locate-token-set (fn [_ id] (when (= id set-id) token-set))
u/locate-token-theme (fn [_ id] (when (= id theme-id) theme))
u/not-valid (fn [_ code value] (swap! invalid conj [code value]))
dwtl/update-token-theme (fn [id theme] {:id id :theme theme})
st/emit! (fn ([event] (swap! emitted conj event) nil)
([event & _] (swap! emitted conj event) nil))]
(let [theme-proxy (ptok/token-theme-proxy plugin-id file-id theme-id)
set-proxy (ptok/token-set-proxy plugin-id file-id set-id "Core")]
(.addSet theme-proxy set-proxy)
(t/is (= #{"Core"} (-> @emitted first :theme :sets)))
(t/is (empty? @invalid))))))
(t/deftest token-theme-add-set-rejects-invalid-arguments
(let [plugin-id "plugin-id"
file-id (uuid/next)
theme-id (uuid/next)
theme (ctob/make-token-theme :id theme-id :group "mode" :name "Light")
emitted (atom [])
errors (atom [])]
(with-redefs [u/locate-token-set (constantly nil)
u/locate-token-theme (fn [_ id] (when (= id theme-id) theme))
u/throw-validation-errors? (constantly true)
dwtl/update-token-theme (fn [id theme] {:id id :theme theme})
st/emit! (fn ([event] (swap! emitted conj event) nil)
([event & _] (swap! emitted conj event) nil))]
(let [theme-proxy (ptok/token-theme-proxy plugin-id file-id theme-id)]
;; Non-id, non-proxy arguments are rejected by the schema coercer.
(try (.addSet theme-proxy 42) (catch :default e (swap! errors conj e)))
(try (.removeSet theme-proxy nil) (catch :default e (swap! errors conj e)))
(t/is (empty? @emitted))
(t/is (= 2 (count @errors)))
(t/is (every? #(instance? js/Error %) @errors))))))
;; ═══════════════════════════════════════════════════════════════
;; Permission check tests (T9-F-01)
;; ═══════════════════════════════════════════════════════════════
;; Note: token-proxy-name-setter-checks-permission test removed because
;; schema validation runs before the permission check, making it impossible
;; to test the permission check directly for setters with schemas.
(t/deftest token-proxy-value-setter-checks-permission
(let [plugin-id "test-plugin"
file-id (uuid/next)
set-id (uuid/next)
token-id (uuid/next)
errors (atom [])]
(with-redefs [u/locate-token (constantly {:id token-id :name "test" :type :color})
u/locate-tokens-lib (constantly nil)
u/not-valid (mock/stub (fn [pid prop msg] (swap! errors conj [pid prop msg])))
r/check-permission (constantly false)
st/emit! mock/noop]
(let [proxy (ptok/token-proxy plugin-id file-id set-id token-id)]
(set! (.-value proxy) "#ff0000")
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :value "Plugin doesn't have 'content:write' permission"] (first @errors)))))))
(t/deftest token-proxy-description-setter-checks-permission
(let [plugin-id "test-plugin"
file-id (uuid/next)
set-id (uuid/next)
token-id (uuid/next)
errors (atom [])]
(with-redefs [u/locate-token (constantly {:id token-id :name "test"})
u/not-valid (mock/stub (fn [pid prop msg] (swap! errors conj [pid prop msg])))
r/check-permission (constantly false)
st/emit! mock/noop]
(let [proxy (ptok/token-proxy plugin-id file-id set-id token-id)]
(set! (.-description proxy) "A description")
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :description "Plugin doesn't have 'content:write' permission"] (first @errors)))))))
(t/deftest token-proxy-duplicate-checks-permission
(let [plugin-id "test-plugin"
file-id (uuid/next)
set-id (uuid/next)
token-id (uuid/next)
errors (atom [])]
(with-redefs [u/locate-token (constantly {:id token-id :name "test" :type :color :value "#000"})
u/not-valid (mock/stub (fn [pid prop msg] (swap! errors conj [pid prop msg])))
r/check-permission (constantly false)
st/emit! mock/noop]
(let [proxy (ptok/token-proxy plugin-id file-id set-id token-id)]
(.duplicate proxy)
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :duplicate "Plugin doesn't have 'content:write' permission"] (first @errors)))))))
(t/deftest token-proxy-remove-checks-permission
(let [plugin-id "test-plugin"
file-id (uuid/next)
set-id (uuid/next)
token-id (uuid/next)
errors (atom [])]
(with-redefs [u/not-valid (mock/stub (fn [pid prop msg] (swap! errors conj [pid prop msg])))
r/check-permission (constantly false)
st/emit! mock/noop]
(let [proxy (ptok/token-proxy plugin-id file-id set-id token-id)]
(.remove proxy)
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :remove "Plugin doesn't have 'content:write' permission"] (first @errors)))))))
(t/deftest token-set-proxy-name-setter-checks-permission
(let [plugin-id "test-plugin"
file-id (uuid/next)
set-id (uuid/next)
errors (atom [])]
(with-redefs [u/locate-token-set (constantly {:id set-id :name "core"})
u/locate-tokens-lib (constantly (ctob/make-tokens-lib))
u/not-valid (mock/stub (fn [pid prop msg] (swap! errors conj [pid prop msg])))
r/check-permission (constantly false)
st/emit! mock/noop]
(let [proxy (ptok/token-set-proxy plugin-id file-id set-id "core")]
(set! (.-name proxy) "new-core")
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :name "Plugin doesn't have 'content:write' permission"] (first @errors)))))))
(t/deftest token-set-proxy-active-setter-checks-permission
(let [plugin-id "test-plugin"
file-id (uuid/next)
set-id (uuid/next)
errors (atom [])]
(with-redefs [u/locate-token-set (constantly {:id set-id :name "core"})
u/locate-tokens-lib (constantly (ctob/make-tokens-lib))
u/not-valid (mock/stub (fn [pid prop msg] (swap! errors conj [pid prop msg])))
r/check-permission (constantly false)
st/emit! mock/noop]
(let [proxy (ptok/token-set-proxy plugin-id file-id set-id "core")]
(set! (.-active proxy) true)
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :active "Plugin doesn't have 'content:write' permission"] (first @errors)))))))
(t/deftest token-set-proxy-toggle-active-checks-permission
(let [plugin-id "test-plugin"
file-id (uuid/next)
set-id (uuid/next)
errors (atom [])]
(with-redefs [u/locate-token-set (constantly {:id set-id :name "core"})
u/locate-tokens-lib (constantly (ctob/make-tokens-lib))
u/not-valid (mock/stub (fn [pid prop msg] (swap! errors conj [pid prop msg])))
r/check-permission (constantly false)
st/emit! mock/noop]
(let [proxy (ptok/token-set-proxy plugin-id file-id set-id)]
(.toggleActive proxy)
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :toggleActive "Plugin doesn't have 'content:write' permission"] (first @errors)))))))
(t/deftest token-set-proxy-duplicate-checks-permission
(let [plugin-id "test-plugin"
file-id (uuid/next)
set-id (uuid/next)
errors (atom [])]
(with-redefs [u/not-valid (mock/stub (fn [pid prop msg] (swap! errors conj [pid prop msg])))
r/check-permission (constantly false)
st/emit! mock/noop]
(let [proxy (ptok/token-set-proxy plugin-id file-id set-id)]
(.duplicate proxy)
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :duplicate "Plugin doesn't have 'content:write' permission"] (first @errors)))))))
(t/deftest token-set-proxy-remove-checks-permission
(let [plugin-id "test-plugin"
file-id (uuid/next)
set-id (uuid/next)
errors (atom [])]
(with-redefs [u/not-valid (mock/stub (fn [pid prop msg] (swap! errors conj [pid prop msg])))
r/check-permission (constantly false)
st/emit! mock/noop]
(let [proxy (ptok/token-set-proxy plugin-id file-id set-id)]
(.remove proxy)
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :remove "Plugin doesn't have 'content:write' permission"] (first @errors)))))))
(t/deftest token-theme-proxy-group-setter-checks-permission
(let [plugin-id "test-plugin"
file-id (uuid/next)
theme-id (uuid/next)
errors (atom [])]
(with-redefs [u/locate-token-theme (constantly {:id theme-id :name "Light" :group "mode"})
u/locate-tokens-lib (constantly nil)
u/not-valid (mock/stub (fn [pid prop msg] (swap! errors conj [pid prop msg])))
r/check-permission (constantly false)
st/emit! mock/noop]
(let [proxy (ptok/token-theme-proxy plugin-id file-id theme-id)]
(set! (.-group proxy) "new-group")
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :group "Plugin doesn't have 'content:write' permission"] (first @errors)))))))
(t/deftest token-theme-proxy-name-setter-checks-permission
(let [plugin-id "test-plugin"
file-id (uuid/next)
theme-id (uuid/next)
errors (atom [])]
(with-redefs [u/locate-token-theme (constantly {:id theme-id :name "Light" :group "mode"})
u/locate-tokens-lib (constantly nil)
u/not-valid (mock/stub (fn [pid prop msg] (swap! errors conj [pid prop msg])))
r/check-permission (constantly false)
st/emit! mock/noop]
(let [proxy (ptok/token-theme-proxy plugin-id file-id theme-id)]
(set! (.-name proxy) "Dark")
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :name "Plugin doesn't have 'content:write' permission"] (first @errors)))))))
(t/deftest token-theme-proxy-active-setter-checks-permission
(let [plugin-id "test-plugin"
file-id (uuid/next)
theme-id (uuid/next)
errors (atom [])]
(with-redefs [u/locate-tokens-lib (constantly (ctob/make-tokens-lib))
u/not-valid (mock/stub (fn [pid prop msg] (swap! errors conj [pid prop msg])))
r/check-permission (constantly false)
st/emit! mock/noop]
(let [proxy (ptok/token-theme-proxy plugin-id file-id theme-id)]
(set! (.-active proxy) true)
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :active "Plugin doesn't have 'content:write' permission"] (first @errors)))))))
(t/deftest token-theme-proxy-toggle-active-checks-permission
(let [plugin-id "test-plugin"
file-id (uuid/next)
theme-id (uuid/next)
errors (atom [])]
(with-redefs [u/not-valid (mock/stub (fn [pid prop msg] (swap! errors conj [pid prop msg])))
r/check-permission (constantly false)
st/emit! mock/noop]
(let [proxy (ptok/token-theme-proxy plugin-id file-id theme-id)]
(.toggleActive proxy)
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :toggleActive "Plugin doesn't have 'content:write' permission"] (first @errors)))))))
(t/deftest token-theme-proxy-add-set-checks-permission
(let [plugin-id "test-plugin"
file-id (uuid/next)
theme-id (uuid/next)
set-id (uuid/next)
errors (atom [])]
(with-redefs [u/locate-token-theme (constantly {:id theme-id :name "Light" :sets #{}})
u/locate-token-set (constantly {:id set-id :name "core"})
u/not-valid (mock/stub (fn [pid prop msg] (swap! errors conj [pid prop msg])))
r/check-permission (constantly false)
st/emit! mock/noop]
(let [proxy (ptok/token-theme-proxy plugin-id file-id theme-id)
set-proxy (ptok/token-set-proxy plugin-id file-id set-id "core")]
(.addSet proxy set-proxy)
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :addSet "Plugin doesn't have 'content:write' permission"] (first @errors)))))))
(t/deftest token-theme-proxy-remove-set-checks-permission
(let [plugin-id "test-plugin"
file-id (uuid/next)
theme-id (uuid/next)
set-id (uuid/next)
errors (atom [])]
(with-redefs [u/locate-token-theme (constantly {:id theme-id :name "Light" :sets #{"core"}})
u/locate-token-set (constantly {:id set-id :name "core"})
u/not-valid (mock/stub (fn [pid prop msg] (swap! errors conj [pid prop msg])))
r/check-permission (constantly false)
st/emit! mock/noop]
(let [proxy (ptok/token-theme-proxy plugin-id file-id theme-id)
set-proxy (ptok/token-set-proxy plugin-id file-id set-id "core")]
(.removeSet proxy set-proxy)
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :removeSet "Plugin doesn't have 'content:write' permission"] (first @errors)))))))
(t/deftest token-theme-proxy-duplicate-checks-permission
(let [plugin-id "test-plugin"
file-id (uuid/next)
theme-id (uuid/next)
errors (atom [])]
(with-redefs [u/locate-token-theme (constantly {:id theme-id :name "Light" :group "mode"})
u/not-valid (mock/stub (fn [pid prop msg] (swap! errors conj [pid prop msg])))
r/check-permission (constantly false)
st/emit! mock/noop]
(let [proxy (ptok/token-theme-proxy plugin-id file-id theme-id)]
(.duplicate proxy)
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :duplicate "Plugin doesn't have 'content:write' permission"] (first @errors)))))))
(t/deftest token-theme-proxy-remove-checks-permission
(let [plugin-id "test-plugin"
file-id (uuid/next)
theme-id (uuid/next)
errors (atom [])]
(with-redefs [u/not-valid (mock/stub (fn [pid prop msg] (swap! errors conj [pid prop msg])))
r/check-permission (constantly false)
st/emit! mock/noop]
(let [proxy (ptok/token-theme-proxy plugin-id file-id theme-id)]
(.remove proxy)
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :remove "Plugin doesn't have 'content:write' permission"] (first @errors)))))))
(t/deftest tokens-catalog-add-theme-checks-permission
(let [plugin-id "test-plugin"
file-id (uuid/next)
errors (atom [])]
(with-redefs [u/locate-tokens-lib (constantly (ctob/make-tokens-lib))
u/not-valid (mock/stub (fn [pid prop msg] (swap! errors conj [pid prop msg])))
r/check-permission (constantly false)
st/emit! mock/noop]
(let [catalog (ptok/tokens-catalog plugin-id file-id)]
(.addTheme catalog #js {"name" "NewTheme" "group" "mode"})
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :addTheme "Plugin doesn't have 'content:write' permission"] (first @errors)))))))
(t/deftest tokens-catalog-add-set-checks-permission
(let [plugin-id "test-plugin"
file-id (uuid/next)
errors (atom [])]
(with-redefs [u/locate-tokens-lib (constantly (ctob/make-tokens-lib))
u/not-valid (mock/stub (fn [pid prop msg] (swap! errors conj [pid prop msg])))
r/check-permission (constantly false)
st/emit! mock/noop]
(let [catalog (ptok/tokens-catalog plugin-id file-id)]
(.addSet catalog #js {"name" "NewSet"})
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :addSet "Plugin doesn't have 'content:write' permission"] (first @errors)))))))