Andrey Antukh 937b3fc65f
🐛 Add missing permission checks to plugin API (tokens, shapes, variants, flows, layouts, user identity) (#11139)
* 🐛 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

* 🐛 Add permission checks to shape proxy interactions, detach, export, and variants

The shape proxy (shape.cljs) had multiple operations missing permission
checks, plus a cond ordering bug that bypassed the existing content:write
check for text shapes in commit-fills!.

Fix commit-fills! cond ordering: move permission check before the
text-shape branch so text shapes are also protected.

Add content:write permission checks to:
- interaction-proxy: :trigger, :delay, :action setters, :remove method
- shape-proxy: :addInteraction, :removeInteraction, :detach
- shape-proxy: :applyToken, :switchVariant, :combineAsVariants

Add content:read permission check to:
- shape-proxy: :export (read/extraction operation)

Follows the established pattern from :resize, :rotate, :blocked setters.

Relates to #11137

AI-assisted-by: qwen3.7-plus

* 🐛 Add library:write permission checks to variant plugin API

The library.cljs variant operations (variant-proxy and
lib-component-proxy) had seven mutating operations that
did not check the library:write permission, allowing
any plugin to create, modify, and delete component
variants regardless of granted permissions.

Add r/check-permission checks to all 7 operations:
- variant-proxy: addVariant, addProperty,
  removeProperty, renameProperty
- lib-component-proxy: transformInVariant, addVariant,
  setVariantProperty

Follows the established pattern from the :name and
:path setters in the same file.

Relates to #11137

AI-assisted-by: qwen3.7-plus

* 🐛 Add content:write permission checks to flow and flex layout plugin API

Add permission checks to prototype flow and flex layout operations
that were missing them, allowing plugins to modify flows and layout
structure without explicit user permission.

Changes:
- page.cljs: Add content:write checks to flow-proxy (name,
  startingBoard setters, remove) and page-proxy (createFlow,
  removeFlow)
- flex.cljs: Add content:write checks to flex-layout-proxy
  (remove, appendChild)

Follows the established pattern from tokens.cljs, shape.cljs,
and library.cljs.

Relates to #11137

AI-assisted-by: qwen3.7-plus

* 🐛 Add user:read permission checks to plugin API

Add permission checks to user identity accessors that were bypassing
the consent model, allowing plugins to access user data regardless
of whether the user granted user:read permission.

Changes:
- api.cljs: Add user:read checks to getCurrentUser and getActiveUsers
- comments.cljs: Add user:read checks to comment-proxy and
  comment-thread-proxy owner/user getters
- file.cljs: Add user:read check to file-version-proxy createdBy getter

When user:read permission is not granted:
- getCurrentUser() returns null
- getActiveUsers() returns empty array
- owner/user/createdBy getters return null

Follows the established pattern from other permission checks in the
plugin API.

Relates to #11137

AI-assisted-by: qwen3.7-plus

* 🐛 Fix problem with token API

---------

Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-09-08 09:24:45 +02:00

710 lines
35 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-add-token-checks-permission
(let [plugin-id "test-plugin"
file-id (uuid/next)
set-id (uuid/next)
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id set-id :name "core")))
errors (atom [])]
(with-redefs [u/locate-token-set (constantly {:id set-id :name "core"})
u/locate-tokens-lib (constantly 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")]
(t/is (fn? (.-addToken proxy)))
(.addToken proxy #js {"type" "color" "name" "color.test" "value" "#FF0000"})
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :addToken "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)))))))