mirror of
https://github.com/penpot/penpot.git
synced 2026-09-10 20:59:12 -04:00
Compare commits
4
Commits
develop
...
issue-11617
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96965da8a4 | ||
|
|
154f4d28e6 | ||
|
|
3a15d70f6e | ||
|
|
fc11fbb8cf |
No files matched your search
@@ -7,11 +7,14 @@
|
||||
(ns app.rpc.commands.viewer
|
||||
(:require
|
||||
[app.binfile.common :as bfc]
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.schema :as sm]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.features.fdata :as fdata]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.cond :as-alias cond]
|
||||
@@ -22,12 +25,81 @@
|
||||
|
||||
;; --- QUERY: View Only Bundle
|
||||
|
||||
(def ^:private view-only-data-keys
|
||||
"Minimal `:data` keys a view-only bundle exposes, for the primary file
|
||||
and for each linked library alike. Keep both scopes on this single
|
||||
definition so they cannot drift apart."
|
||||
[:id :options :pages :pages-index :components])
|
||||
|
||||
(defn- remove-not-allowed-pages
|
||||
[data allowed]
|
||||
(-> data
|
||||
(update :pages (fn [pages] (filterv #(contains? allowed %) pages)))
|
||||
(update :pages-index select-keys allowed)))
|
||||
|
||||
(defn- find-library-refs
|
||||
"Return `{lib-id #{component-id}}` referenced by the given shapes."
|
||||
[shapes]
|
||||
(reduce (fn [acc shape]
|
||||
(if (and (map? shape)
|
||||
(some? (:component-file shape))
|
||||
(some? (:component-id shape)))
|
||||
(update acc (:component-file shape) (fnil conj #{}) (:component-id shape))
|
||||
acc))
|
||||
{}
|
||||
shapes))
|
||||
|
||||
(defn- component-shapes
|
||||
"Shapes owned by a library component: the subtree rooted at its main
|
||||
instance. Stored components carry no `:objects`; they point at the main
|
||||
instance through `:main-instance-id`/`:main-instance-page` instead. Looks
|
||||
the page up in the component's own library first and in any other loaded
|
||||
library as fallback (cloned or moved components); ties break by library
|
||||
id order so the fallback is deterministic. Returns nil when the main
|
||||
instance cannot be resolved."
|
||||
[libs-by-id lib-id {:keys [main-instance-id main-instance-page]}]
|
||||
(when (and (some? main-instance-id) (some? main-instance-page))
|
||||
(let [objects (or (get-in libs-by-id [lib-id :data :pages-index main-instance-page :objects])
|
||||
(some (fn [[_ lib]]
|
||||
(get-in lib [:data :pages-index main-instance-page :objects]))
|
||||
(sort-by key libs-by-id)))]
|
||||
(when (and (some? objects) (some? (get objects main-instance-id)))
|
||||
(cfh/get-children-with-self objects main-instance-id)))))
|
||||
|
||||
(defn- collect-used-library-components
|
||||
"Walk the objects of the (already page-scoped) primary file data and of
|
||||
the referenced library components themselves, and return a map of
|
||||
`{lib-id #{component-id}}` with every library component the allowed
|
||||
pages need. Follows nested references to a fixpoint so trimming never
|
||||
drops a component that is only reached through another component."
|
||||
[primary-data libs-by-id]
|
||||
(let [root-shapes (concat (mapcat (comp vals :objects) (vals (:pages-index primary-data)))
|
||||
(mapcat (comp vals :objects) (vals (:components primary-data))))]
|
||||
(loop [seen {}]
|
||||
(let [nested (mapcat (fn [[lib-id comp-ids]]
|
||||
(mapcat #(component-shapes libs-by-id lib-id
|
||||
(get-in libs-by-id [lib-id :data :components %]))
|
||||
comp-ids))
|
||||
seen)
|
||||
merged (merge-with into seen (find-library-refs (concat root-shapes nested)))]
|
||||
(if (= merged seen)
|
||||
seen
|
||||
(recur merged))))))
|
||||
|
||||
(defn- trim-library-data
|
||||
"Reduce a linked library to the minimum a share-link viewer needs: keep
|
||||
the narrow `:data` keys, drop the library's own pages (a share link
|
||||
never grants pages of a library), and keep only the components
|
||||
referenced by the allowed pages of the primary file. Envelope metadata
|
||||
outside `:data` (name, project, sync state) is intentionally preserved:
|
||||
the viewer client needs it and it carries no design content."
|
||||
[used-refs {:keys [id] :as lib}]
|
||||
(-> lib
|
||||
(update :data select-keys view-only-data-keys)
|
||||
(assoc-in [:data :pages] [])
|
||||
(assoc-in [:data :pages-index] {})
|
||||
(update-in [:data :components] select-keys (get used-refs id #{}))))
|
||||
|
||||
(defn obfuscate-email
|
||||
"Obfuscate the `email` for share-link members so the viewer only sees a
|
||||
partially redacted address. Accepts any string shape (including nil,
|
||||
@@ -83,12 +155,25 @@
|
||||
(update :data remove-not-allowed-pages (:pages perms))
|
||||
|
||||
:always
|
||||
(update :data select-keys [:id :options :pages :pages-index :components]))
|
||||
(update :data select-keys view-only-data-keys))
|
||||
|
||||
libs (->> (bfc/get-file-libraries conn file-id)
|
||||
(mapv (fn [{:keys [id] :as lib}]
|
||||
(merge lib (bfc/get-file cfg id)))))
|
||||
|
||||
;; A share link never grants pages of a linked library, so trim
|
||||
;; each library to the components the allowed pages reference.
|
||||
;; File data may hold lazy pointer-mapped pages/components, so
|
||||
;; realize each file with its own loader before walking it.
|
||||
;; Membership bundles keep the full libraries.
|
||||
libs (if (= :share-link (:type perms))
|
||||
(let [file' (fdata/realize-pointers cfg file)
|
||||
libs' (mapv #(fdata/realize-pointers cfg %) libs)
|
||||
libs-by-id (d/index-by :id libs')
|
||||
used (collect-used-library-components (:data file') libs-by-id)]
|
||||
(mapv #(trim-library-data used %) libs'))
|
||||
libs)
|
||||
|
||||
links (cond->> (->> (db/query conn :share-link {:file-id file-id})
|
||||
(mapv (fn [row]
|
||||
(-> row
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
(ns backend-tests.rpc-viewer-test
|
||||
(:require
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.rpc :as-alias rpc]
|
||||
@@ -17,6 +18,25 @@
|
||||
(t/use-fixtures :once th/state-init)
|
||||
(t/use-fixtures :each th/database-reset)
|
||||
|
||||
(def ^:private safe-library-envelope-keys
|
||||
"Top-level keys a trimmed library may expose next to `:data`. Anything
|
||||
else is design content leaking through the envelope (see F3)."
|
||||
#{:backend :comment-thread-seqn :created-at :features :has-media-trimmed
|
||||
:id :is-indirect :is-shared :migrations :modified-at :name :project-id
|
||||
:revn :synced-at :team-id :vern :version})
|
||||
|
||||
(defn- update-file!
|
||||
[& {:keys [profile-id file-id changes]}]
|
||||
(let [out (th/command! {::th/type :update-file
|
||||
::rpc/profile-id profile-id
|
||||
:id file-id
|
||||
:session-id (uuid/random)
|
||||
:revn 0
|
||||
:vern 0
|
||||
:changes changes})]
|
||||
(t/is (nil? (:error out)))
|
||||
(:result out)))
|
||||
|
||||
(t/deftest obfuscate-email-happy-path
|
||||
(t/is (= "a****@****.com" (viewer/obfuscate-email "alice@example.com")))
|
||||
(t/is (= "a****@****.example.com" (viewer/obfuscate-email "alice@sub.example.com")))
|
||||
@@ -205,3 +225,640 @@
|
||||
(t/is (= 2 (count share-links)))
|
||||
(t/is (some #(= link-a-id (:id %)) share-links))
|
||||
(t/is (some #(= link-b-id (:id %)) share-links))))))
|
||||
|
||||
(t/deftest trim-library-data-unit
|
||||
(let [lib-id (uuid/random)
|
||||
comp-a (uuid/random)
|
||||
comp-b (uuid/random)
|
||||
comp-c (uuid/random)
|
||||
page-id (uuid/random)
|
||||
|
||||
mk-shape (fn [id comp] {:id id :type :rect :component-id comp :component-file lib-id})
|
||||
shape-a (mk-shape (uuid/random) comp-a)
|
||||
|
||||
primary {:pages [page-id]
|
||||
:pages-index {page-id {:id page-id :objects {(:id shape-a) shape-a}}}
|
||||
:components {}}
|
||||
|
||||
lib {:id lib-id
|
||||
:synced-at "now"
|
||||
:data {:id lib-id
|
||||
:options {}
|
||||
:pages [page-id]
|
||||
:pages-index {page-id {:id page-id}}
|
||||
:components {comp-a {:id comp-a :name "a" :objects {}}
|
||||
comp-b {:id comp-b :name "b" :objects {}}}}}]
|
||||
|
||||
(t/testing "keeps referenced components and drops library pages"
|
||||
(let [used (#'viewer/collect-used-library-components primary {lib-id lib})
|
||||
trimmed (#'viewer/trim-library-data used lib)]
|
||||
(t/is (= #{comp-a} (get used lib-id)))
|
||||
(t/is (= #{comp-a} (set (keys (get-in trimmed [:data :components])))))
|
||||
(t/is (= [] (get-in trimmed [:data :pages])))
|
||||
(t/is (= {} (get-in trimmed [:data :pages-index])))
|
||||
(t/is (= lib-id (:id trimmed)))
|
||||
(t/is (= "now" (:synced-at trimmed)))))
|
||||
|
||||
(t/testing "no references keeps no components"
|
||||
(let [trimmed (#'viewer/trim-library-data {} lib)]
|
||||
(t/is (= {} (get-in trimmed [:data :components])))
|
||||
(t/is (= [] (get-in trimmed [:data :pages])))))
|
||||
|
||||
(t/testing "follows nested component references through the main instance"
|
||||
;; Stored components carry no `:objects`; nested references resolve
|
||||
;; through the main-instance subtree on the library page.
|
||||
(let [main-used (uuid/random)
|
||||
main-nested (uuid/random)
|
||||
child (assoc (mk-shape (uuid/random) comp-c) :parent-id main-used)
|
||||
lib2 (assoc lib :data
|
||||
{:id lib-id
|
||||
:options {}
|
||||
:pages [page-id]
|
||||
:pages-index {page-id {:id page-id
|
||||
:objects {main-used {:id main-used
|
||||
:type :frame
|
||||
:shapes [(:id child)]}
|
||||
(:id child) child
|
||||
main-nested {:id main-nested
|
||||
:type :frame}}}}
|
||||
:components {comp-a {:id comp-a
|
||||
:name "a"
|
||||
:main-instance-id main-used
|
||||
:main-instance-page page-id}
|
||||
comp-b {:id comp-b
|
||||
:name "b"}
|
||||
comp-c {:id comp-c
|
||||
:name "c"
|
||||
:main-instance-id main-nested
|
||||
:main-instance-page page-id}}})
|
||||
used (#'viewer/collect-used-library-components primary {lib-id lib2})
|
||||
trimmed (#'viewer/trim-library-data used lib2)]
|
||||
(t/is (= #{comp-a comp-c} (get used lib-id)))
|
||||
(t/is (= #{comp-a comp-c} (set (keys (get-in trimmed [:data :components])))))
|
||||
(t/is (not (contains? (get-in trimmed [:data :components]) comp-b)))))))
|
||||
|
||||
(t/deftest share-link-bundle-trims-linked-libraries
|
||||
(let [owner (th/create-profile* 1 {:is-active true})
|
||||
stranger (th/create-profile* 2 {:is-active true})
|
||||
proj-id (:default-project-id owner)
|
||||
|
||||
lib (th/create-file* 1 {:profile-id (:id owner)
|
||||
:project-id proj-id
|
||||
:is-shared true})
|
||||
|
||||
;; A second page that only exists inside the library; the main
|
||||
;; file never references it, so no share link on the main file
|
||||
;; should ever expose it.
|
||||
canary (uuid/random)
|
||||
|
||||
_ (th/command! {::th/type :update-file
|
||||
::rpc/profile-id (:id owner)
|
||||
:id (:id lib)
|
||||
:session-id (uuid/random)
|
||||
:revn 0
|
||||
:vern 0
|
||||
:changes [{:type :add-page
|
||||
:id canary
|
||||
:page {:id canary
|
||||
:name "Private canary"
|
||||
:options {}
|
||||
:objects {}}}]})
|
||||
|
||||
file (th/create-file* 2 {:profile-id (:id owner)
|
||||
:project-id proj-id
|
||||
:is-shared false})
|
||||
|
||||
_ (th/link-file-to-library* {:file-id (:id file)
|
||||
:library-id (:id lib)})
|
||||
|
||||
slink (th/command! {::th/type :create-share-link
|
||||
::rpc/profile-id (:id owner)
|
||||
:file-id (:id file)
|
||||
:pages #{(get-in file [:data :pages 0])}
|
||||
:who-comment "team"
|
||||
:who-inspect "all"})
|
||||
slink-id (get-in slink [:result :id])]
|
||||
|
||||
(t/testing "control: non-member cannot read the library directly"
|
||||
(let [out (th/command! {::th/type :get-file
|
||||
::rpc/profile-id (:id stranger)
|
||||
:id (:id lib)
|
||||
:components-v2 true})
|
||||
error-data (ex-data (:error out))]
|
||||
(t/is (th/ex-info? (:error out)))
|
||||
(t/is (= :not-found (:type error-data)))))
|
||||
|
||||
(t/testing "anonymous bundle omits unreferenced library pages"
|
||||
(let [out (th/command! {::th/type :get-view-only-bundle
|
||||
:share-id slink-id
|
||||
:file-id (:id file)})
|
||||
result (:result out)
|
||||
lib-pages (into #{} (mapcat #(get-in % [:data :pages] []))
|
||||
(:libraries result))]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (not (contains? lib-pages canary)))
|
||||
(t/is (every? #(= #{:id :options :pages :pages-index :components}
|
||||
(set (keys (:data %))))
|
||||
(:libraries result)))
|
||||
(t/is (every? #(every? safe-library-envelope-keys
|
||||
(keys (dissoc % :data)))
|
||||
(:libraries result)))))))
|
||||
|
||||
(t/deftest share-link-bundle-library-edge-cases
|
||||
(let [owner (th/create-profile* 1 {:is-active true})
|
||||
proj-id (:default-project-id owner)
|
||||
|
||||
add-canary (fn [file n]
|
||||
(let [canary (uuid/random)]
|
||||
(th/command! {::th/type :update-file
|
||||
::rpc/profile-id (:id owner)
|
||||
:id (:id file)
|
||||
:session-id (uuid/random)
|
||||
:revn 0
|
||||
:vern 0
|
||||
:changes [{:type :add-page
|
||||
:id canary
|
||||
:page {:id canary
|
||||
:name n
|
||||
:options {}
|
||||
:objects {}}}]})
|
||||
canary))
|
||||
|
||||
lib2 (th/create-file* 1 {:profile-id (:id owner)
|
||||
:project-id proj-id
|
||||
:is-shared true})
|
||||
canary2 (add-canary lib2 "Private canary 2")
|
||||
|
||||
lib1 (th/create-file* 2 {:profile-id (:id owner)
|
||||
:project-id proj-id
|
||||
:is-shared true})
|
||||
canary1 (add-canary lib1 "Private canary 1")
|
||||
|
||||
_ (th/link-file-to-library* {:file-id (:id lib1)
|
||||
:library-id (:id lib2)})
|
||||
|
||||
file (th/create-file* 3 {:profile-id (:id owner)
|
||||
:project-id proj-id
|
||||
:is-shared false})
|
||||
|
||||
_ (th/link-file-to-library* {:file-id (:id file)
|
||||
:library-id (:id lib1)})
|
||||
|
||||
full (th/command! {::th/type :create-share-link
|
||||
::rpc/profile-id (:id owner)
|
||||
:file-id (:id file)
|
||||
:pages #{(get-in file [:data :pages 0])}
|
||||
:who-comment "team"
|
||||
:who-inspect "all"})
|
||||
full-id (get-in full [:result :id])
|
||||
|
||||
empty (th/command! {::th/type :create-share-link
|
||||
::rpc/profile-id (:id owner)
|
||||
:file-id (:id file)
|
||||
:pages #{}
|
||||
:who-comment "team"
|
||||
:who-inspect "team"})
|
||||
empty-id (get-in empty [:result :id])
|
||||
|
||||
bundle (fn [& {:as params}]
|
||||
(th/command! (merge {::th/type :get-view-only-bundle
|
||||
:file-id (:id file)}
|
||||
params)))
|
||||
lib-pages (fn [result]
|
||||
(into #{} (mapcat #(get-in % [:data :pages] []))
|
||||
(:libraries result)))]
|
||||
|
||||
(t/testing "indirect libraries are trimmed too"
|
||||
(let [out (bundle :share-id full-id)
|
||||
result (:result out)
|
||||
pages (lib-pages result)]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (= 2 (count (:libraries result))))
|
||||
(t/is (not (contains? pages canary1)))
|
||||
(t/is (not (contains? pages canary2)))))
|
||||
|
||||
(t/testing "empty-pages link exposes no library content"
|
||||
(let [out (bundle :share-id empty-id)
|
||||
result (:result out)]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (every? #(= {} (get-in % [:data :components])) (:libraries result)))
|
||||
(t/is (every? #(= [] (get-in % [:data :pages])) (:libraries result)))))
|
||||
|
||||
(t/testing "member bundle keeps full libraries"
|
||||
(let [out (bundle ::rpc/profile-id (:id owner))
|
||||
result (:result out)
|
||||
pages (lib-pages result)]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (contains? pages canary1))
|
||||
(t/is (contains? pages canary2))))
|
||||
|
||||
(t/testing "file without libraries returns no libraries"
|
||||
(let [plain (th/create-file* 4 {:profile-id (:id owner)
|
||||
:project-id proj-id
|
||||
:is-shared false})
|
||||
link (th/command! {::th/type :create-share-link
|
||||
::rpc/profile-id (:id owner)
|
||||
:file-id (:id plain)
|
||||
:pages #{(get-in plain [:data :pages 0])}
|
||||
:who-comment "team"
|
||||
:who-inspect "all"})
|
||||
out (th/command! {::th/type :get-view-only-bundle
|
||||
:share-id (get-in link [:result :id])
|
||||
:file-id (:id plain)})]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (= [] (:libraries (:result out))))))))
|
||||
|
||||
(t/deftest share-link-bundle-keeps-used-library-components
|
||||
(let [owner (th/create-profile* 1 {:is-active true})
|
||||
proj-id (:default-project-id owner)
|
||||
|
||||
lib (th/create-file* 1 {:profile-id (:id owner)
|
||||
:project-id proj-id
|
||||
:is-shared true})
|
||||
lib-page (first (get-in lib [:data :pages]))
|
||||
|
||||
c-nested (uuid/random)
|
||||
c-used (uuid/random)
|
||||
c-unused (uuid/random)
|
||||
n-main (uuid/random)
|
||||
u-main (uuid/random)
|
||||
u-child (uuid/random)
|
||||
x-main (uuid/random)
|
||||
inst (uuid/random)
|
||||
|
||||
frame (fn [id parent frame-id extra]
|
||||
(cts/setup-shape
|
||||
(merge {:id id
|
||||
:name "Board"
|
||||
:frame-id frame-id
|
||||
:parent-id parent
|
||||
:type :frame}
|
||||
extra)))
|
||||
|
||||
;; Nested component, referenced only through c-used.
|
||||
_ (update-file!
|
||||
:profile-id (:id owner)
|
||||
:file-id (:id lib)
|
||||
:changes [{:type :add-obj
|
||||
:page-id lib-page
|
||||
:id n-main
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:components-v2 true
|
||||
:obj (frame n-main uuid/zero uuid/zero
|
||||
{:main-instance true
|
||||
:component-root true
|
||||
:component-file (:id lib)
|
||||
:component-id c-nested})}
|
||||
{:type :add-component
|
||||
:path ""
|
||||
:name "nested"
|
||||
:main-instance-id n-main
|
||||
:main-instance-page lib-page
|
||||
:id c-nested
|
||||
:anotation nil}])
|
||||
|
||||
;; Used component, instancing the nested one.
|
||||
_ (update-file!
|
||||
:profile-id (:id owner)
|
||||
:file-id (:id lib)
|
||||
:changes [{:type :add-obj
|
||||
:page-id lib-page
|
||||
:id u-main
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:components-v2 true
|
||||
:obj (frame u-main uuid/zero uuid/zero
|
||||
{:main-instance true
|
||||
:component-root true
|
||||
:component-file (:id lib)
|
||||
:component-id c-used})}
|
||||
{:type :add-obj
|
||||
:page-id lib-page
|
||||
:id u-child
|
||||
:parent-id u-main
|
||||
:frame-id u-main
|
||||
:components-v2 true
|
||||
:obj (frame u-child u-main u-main
|
||||
{:main-instance false
|
||||
:component-root true
|
||||
:component-file (:id lib)
|
||||
:component-id c-nested})}
|
||||
{:type :add-component
|
||||
:path ""
|
||||
:name "used"
|
||||
:main-instance-id u-main
|
||||
:main-instance-page lib-page
|
||||
:id c-used
|
||||
:anotation nil}])
|
||||
|
||||
;; Unreferenced component, never instanced anywhere.
|
||||
_ (update-file!
|
||||
:profile-id (:id owner)
|
||||
:file-id (:id lib)
|
||||
:changes [{:type :add-obj
|
||||
:page-id lib-page
|
||||
:id x-main
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:components-v2 true
|
||||
:obj (frame x-main uuid/zero uuid/zero
|
||||
{:main-instance true
|
||||
:component-root true
|
||||
:component-file (:id lib)
|
||||
:component-id c-unused})}
|
||||
{:type :add-component
|
||||
:path ""
|
||||
:name "unused"
|
||||
:main-instance-id x-main
|
||||
:main-instance-page lib-page
|
||||
:id c-unused
|
||||
:anotation nil}])
|
||||
|
||||
file (th/create-file* 2 {:profile-id (:id owner)
|
||||
:project-id proj-id
|
||||
:is-shared false})
|
||||
main-page (first (get-in file [:data :pages]))
|
||||
|
||||
;; Instance the used component on the main file.
|
||||
_ (update-file!
|
||||
:profile-id (:id owner)
|
||||
:file-id (:id file)
|
||||
:changes [{:type :add-obj
|
||||
:page-id main-page
|
||||
:id inst
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:components-v2 true
|
||||
:obj (frame inst uuid/zero uuid/zero
|
||||
{:main-instance false
|
||||
:component-root true
|
||||
:component-file (:id lib)
|
||||
:component-id c-used})}])
|
||||
|
||||
_ (th/link-file-to-library* {:file-id (:id file)
|
||||
:library-id (:id lib)})
|
||||
|
||||
slink (th/command! {::th/type :create-share-link
|
||||
::rpc/profile-id (:id owner)
|
||||
:file-id (:id file)
|
||||
:pages #{main-page}
|
||||
:who-comment "team"
|
||||
:who-inspect "all"})
|
||||
slink-id (get-in slink [:result :id])]
|
||||
|
||||
(t/testing "anonymous bundle keeps referenced components and drops the rest"
|
||||
(let [out (th/command! {::th/type :get-view-only-bundle
|
||||
:share-id slink-id
|
||||
:file-id (:id file)})
|
||||
result (:result out)
|
||||
libs (:libraries result)]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (= 1 (count libs)))
|
||||
(t/is (= #{c-used c-nested}
|
||||
(set (keys (get-in (first libs) [:data :components])))))
|
||||
(t/is (= [] (get-in (first libs) [:data :pages])))))))
|
||||
|
||||
(t/deftest share-link-bundle-drops-components-from-disallowed-pages
|
||||
(let [owner (th/create-profile* 1 {:is-active true})
|
||||
proj-id (:default-project-id owner)
|
||||
|
||||
lib (th/create-file* 1 {:profile-id (:id owner)
|
||||
:project-id proj-id
|
||||
:is-shared true})
|
||||
lib-page (first (get-in lib [:data :pages]))
|
||||
|
||||
comp (uuid/random)
|
||||
main (uuid/random)
|
||||
|
||||
_ (update-file!
|
||||
:profile-id (:id owner)
|
||||
:file-id (:id lib)
|
||||
:changes [{:type :add-obj
|
||||
:page-id lib-page
|
||||
:id main
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:components-v2 true
|
||||
:obj (cts/setup-shape
|
||||
{:id main
|
||||
:name "Board"
|
||||
:frame-id uuid/zero
|
||||
:parent-id uuid/zero
|
||||
:type :frame
|
||||
:main-instance true
|
||||
:component-root true
|
||||
:component-file (:id lib)
|
||||
:component-id comp})}
|
||||
{:type :add-component
|
||||
:path ""
|
||||
:name "Board"
|
||||
:main-instance-id main
|
||||
:main-instance-page lib-page
|
||||
:id comp
|
||||
:anotation nil}])
|
||||
|
||||
file (th/create-file* 2 {:profile-id (:id owner)
|
||||
:project-id proj-id
|
||||
:is-shared false})
|
||||
page-a (first (get-in file [:data :pages]))
|
||||
page-b (uuid/random)
|
||||
inst (uuid/random)
|
||||
|
||||
;; Second page, outside the share link scope.
|
||||
_ (update-file!
|
||||
:profile-id (:id owner)
|
||||
:file-id (:id file)
|
||||
:changes [{:type :add-page
|
||||
:id page-b
|
||||
:page {:id page-b
|
||||
:name "Hidden"
|
||||
:options {}
|
||||
:objects {}}}])
|
||||
|
||||
;; Instance the library component only on the disallowed page.
|
||||
_ (update-file!
|
||||
:profile-id (:id owner)
|
||||
:file-id (:id file)
|
||||
:changes [{:type :add-obj
|
||||
:page-id page-b
|
||||
:id inst
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:components-v2 true
|
||||
:obj (cts/setup-shape
|
||||
{:id inst
|
||||
:name "Board"
|
||||
:frame-id uuid/zero
|
||||
:parent-id uuid/zero
|
||||
:type :frame
|
||||
:main-instance false
|
||||
:component-root true
|
||||
:component-file (:id lib)
|
||||
:component-id comp})}])
|
||||
|
||||
_ (th/link-file-to-library* {:file-id (:id file)
|
||||
:library-id (:id lib)})
|
||||
|
||||
slink (th/command! {::th/type :create-share-link
|
||||
::rpc/profile-id (:id owner)
|
||||
:file-id (:id file)
|
||||
:pages #{page-a}
|
||||
:who-comment "team"
|
||||
:who-inspect "all"})]
|
||||
|
||||
(t/testing "references from disallowed pages seed nothing"
|
||||
(let [out (th/command! {::th/type :get-view-only-bundle
|
||||
:share-id (get-in slink [:result :id])
|
||||
:file-id (:id file)})
|
||||
result (:result out)
|
||||
libs (:libraries result)]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (= 1 (count libs)))
|
||||
(t/is (= {} (get-in (first libs) [:data :components])))
|
||||
(t/is (= [] (get-in (first libs) [:data :pages])))))))
|
||||
|
||||
(t/deftest share-link-bundle-keeps-cross-library-nested-components
|
||||
(let [owner (th/create-profile* 1 {:is-active true})
|
||||
proj-id (:default-project-id owner)
|
||||
|
||||
lib2 (th/create-file* 1 {:profile-id (:id owner)
|
||||
:project-id proj-id
|
||||
:is-shared true})
|
||||
lib2-page (first (get-in lib2 [:data :pages]))
|
||||
|
||||
comp-b (uuid/random)
|
||||
b-main (uuid/random)
|
||||
|
||||
_ (update-file!
|
||||
:profile-id (:id owner)
|
||||
:file-id (:id lib2)
|
||||
:changes [{:type :add-obj
|
||||
:page-id lib2-page
|
||||
:id b-main
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:components-v2 true
|
||||
:obj (cts/setup-shape
|
||||
{:id b-main
|
||||
:name "Leaf"
|
||||
:frame-id uuid/zero
|
||||
:parent-id uuid/zero
|
||||
:type :frame
|
||||
:main-instance true
|
||||
:component-root true
|
||||
:component-file (:id lib2)
|
||||
:component-id comp-b})}
|
||||
{:type :add-component
|
||||
:path ""
|
||||
:name "leaf"
|
||||
:main-instance-id b-main
|
||||
:main-instance-page lib2-page
|
||||
:id comp-b
|
||||
:anotation nil}])
|
||||
|
||||
lib1 (th/create-file* 2 {:profile-id (:id owner)
|
||||
:project-id proj-id
|
||||
:is-shared true})
|
||||
lib1-page (first (get-in lib1 [:data :pages]))
|
||||
|
||||
_ (th/link-file-to-library* {:file-id (:id lib1)
|
||||
:library-id (:id lib2)})
|
||||
|
||||
comp-a (uuid/random)
|
||||
a-main (uuid/random)
|
||||
a-child (uuid/random)
|
||||
|
||||
;; Component whose main instance embeds an instance from lib2.
|
||||
_ (update-file!
|
||||
:profile-id (:id owner)
|
||||
:file-id (:id lib1)
|
||||
:changes [{:type :add-obj
|
||||
:page-id lib1-page
|
||||
:id a-main
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:components-v2 true
|
||||
:obj (cts/setup-shape
|
||||
{:id a-main
|
||||
:name "Wrapper"
|
||||
:frame-id uuid/zero
|
||||
:parent-id uuid/zero
|
||||
:type :frame
|
||||
:main-instance true
|
||||
:component-root true
|
||||
:component-file (:id lib1)
|
||||
:component-id comp-a})}
|
||||
{:type :add-obj
|
||||
:page-id lib1-page
|
||||
:id a-child
|
||||
:parent-id a-main
|
||||
:frame-id a-main
|
||||
:components-v2 true
|
||||
:obj (cts/setup-shape
|
||||
{:id a-child
|
||||
:name "Leaf"
|
||||
:frame-id a-main
|
||||
:parent-id a-main
|
||||
:type :frame
|
||||
:main-instance false
|
||||
:component-root true
|
||||
:component-file (:id lib2)
|
||||
:component-id comp-b})}
|
||||
{:type :add-component
|
||||
:path ""
|
||||
:name "wrapper"
|
||||
:main-instance-id a-main
|
||||
:main-instance-page lib1-page
|
||||
:id comp-a
|
||||
:anotation nil}])
|
||||
|
||||
file (th/create-file* 3 {:profile-id (:id owner)
|
||||
:project-id proj-id
|
||||
:is-shared false})
|
||||
main-page (first (get-in file [:data :pages]))
|
||||
inst (uuid/random)
|
||||
|
||||
_ (update-file!
|
||||
:profile-id (:id owner)
|
||||
:file-id (:id file)
|
||||
:changes [{:type :add-obj
|
||||
:page-id main-page
|
||||
:id inst
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:components-v2 true
|
||||
:obj (cts/setup-shape
|
||||
{:id inst
|
||||
:name "Wrapper"
|
||||
:frame-id uuid/zero
|
||||
:parent-id uuid/zero
|
||||
:type :frame
|
||||
:main-instance false
|
||||
:component-root true
|
||||
:component-file (:id lib1)
|
||||
:component-id comp-a})}])
|
||||
|
||||
_ (th/link-file-to-library* {:file-id (:id file)
|
||||
:library-id (:id lib1)})
|
||||
|
||||
slink (th/command! {::th/type :create-share-link
|
||||
::rpc/profile-id (:id owner)
|
||||
:file-id (:id file)
|
||||
:pages #{main-page}
|
||||
:who-comment "team"
|
||||
:who-inspect "all"})]
|
||||
|
||||
(t/testing "nested references resolve across libraries"
|
||||
(let [out (th/command! {::th/type :get-view-only-bundle
|
||||
:share-id (get-in slink [:result :id])
|
||||
:file-id (:id file)})
|
||||
result (:result out)
|
||||
by-id (into {} (map (juxt :id identity)) (:libraries result))]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (= 2 (count (:libraries result))))
|
||||
(t/is (= #{comp-a}
|
||||
(set (keys (get-in by-id [(:id lib1) :data :components])))))
|
||||
(t/is (= #{comp-b}
|
||||
(set (keys (get-in by-id [(:id lib2) :data :components])))))
|
||||
(t/is (= [] (get-in by-id [(:id lib2) :data :pages])))))))
|
||||
Reference in new issue
Block a user