mirror of
https://github.com/penpot/penpot.git
synced 2026-08-06 21:08:34 +00:00
🎉 Add link preview metadata for shared links
This commit is contained in:
parent
c119622ad9
commit
3d93700614
22
backend/resources/app/templates/unfurl.tmpl
Normal file
22
backend/resources/app/templates/unfurl.tmpl
Normal file
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>{{title}}</title>
|
||||
<meta name="robots" content="noindex" />
|
||||
<meta name="description" content="{{description}}" />
|
||||
<meta property="og:site_name" content="Penpot" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<meta property="og:title" content="{{title}}" />
|
||||
<meta property="og:description" content="{{description}}" />
|
||||
<meta property="og:image" content="{{image}}" />
|
||||
<meta name="twitter:title" content="{{title}}" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:description" content="{{description}}" />
|
||||
<meta name="twitter:image" content="{{image}}" />
|
||||
</head>
|
||||
<body>
|
||||
<script>location.replace("/" + location.hash);</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -21,6 +21,7 @@
|
||||
[app.http.middleware :as mw]
|
||||
[app.http.security :as sec]
|
||||
[app.http.session :as session]
|
||||
[app.http.unfurl :as-alias unfurl]
|
||||
[app.http.websocket :as-alias ws]
|
||||
[app.main :as-alias main]
|
||||
[app.metrics :as mtx]
|
||||
@ -149,6 +150,7 @@
|
||||
[::rpc/routes schema:routes]
|
||||
[::oidc/routes schema:routes]
|
||||
[::assets/routes schema:routes]
|
||||
[::unfurl/routes schema:routes]
|
||||
[::debug/routes schema:routes]
|
||||
[::mtx/routes schema:routes]
|
||||
[::awsns/routes schema:routes]
|
||||
@ -177,6 +179,7 @@
|
||||
|
||||
(::mtx/routes cfg)
|
||||
(::assets/routes cfg)
|
||||
(::unfurl/routes cfg)
|
||||
(::debug/routes cfg)
|
||||
|
||||
["/webhooks"
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.http.access-token :as actoken]
|
||||
[app.http.session :as session]
|
||||
@ -74,11 +75,19 @@
|
||||
(:s3 :assets-s3) (serve-object-from-s3 cfg obj)
|
||||
(:fs :assets-fs) (serve-object-from-fs cfg obj)))
|
||||
|
||||
(defn- public-bucket?
|
||||
[bucket]
|
||||
(or (contains? public-buckets bucket)
|
||||
;; Dashboard file thumbnails become public when link unfurling
|
||||
;; is enabled, so link preview crawlers can fetch them.
|
||||
(and (= "file-thumbnail" bucket)
|
||||
(contains? cf/flags :link-unfurl))))
|
||||
|
||||
(defn- requires-auth?
|
||||
"Check if the storage object requires authentication based on its bucket."
|
||||
[obj]
|
||||
(let [bucket (-> obj meta :bucket)]
|
||||
(not (contains? public-buckets bucket))))
|
||||
(not (public-bucket? bucket))))
|
||||
|
||||
(defn- authenticated?
|
||||
"Check if the request has an authenticated profile, either via session
|
||||
|
||||
85
backend/src/app/http/unfurl.clj
Normal file
85
backend/src/app/http/unfurl.clj
Normal file
@ -0,0 +1,85 @@
|
||||
;; 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 app.http.unfurl
|
||||
"Link unfurl (Open Graph metadata) related handlers.
|
||||
|
||||
Serves a minimal HTML page with Open Graph metadata used by link
|
||||
preview crawlers (Slack, Discord, Twitter, ...). The reverse proxy
|
||||
routes crawler requests for the application root to this endpoint,
|
||||
preserving the query string params that the frontend mirrors on
|
||||
navigation (`file-id`, `project-id` and `team-id`)."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.util.template :as tmpl]
|
||||
[clojure.java.io :as io]
|
||||
[integrant.core :as ig]
|
||||
[yetti.response :as-alias yres]))
|
||||
|
||||
(def ^:private default-context
|
||||
{:title "Penpot | Full-stack design"
|
||||
:description "Penpot is the open-source design platform for teams that build digital products at scale."})
|
||||
|
||||
(def ^:private sql:get-file
|
||||
"SELECT f.name, ft.media_id
|
||||
FROM file AS f
|
||||
LEFT JOIN file_thumbnail AS ft
|
||||
ON (ft.file_id = f.id AND ft.deleted_at IS NULL)
|
||||
WHERE f.id = ?
|
||||
AND f.deleted_at IS NULL
|
||||
ORDER BY ft.revn DESC NULLS LAST
|
||||
LIMIT 1")
|
||||
|
||||
(defn- resolve-image-uri
|
||||
[media-id]
|
||||
(str (cf/get :public-uri) "/assets/by-id/" media-id))
|
||||
|
||||
(defn- resolve-default-image-uri
|
||||
[]
|
||||
(str (cf/get :public-uri) "/images/penpot-link-preview.png"))
|
||||
|
||||
(defn- get-file-context
|
||||
"Return the unfurl context for a file link: the file name as title
|
||||
and, when available, the last dashboard thumbnail as image."
|
||||
[pool file-id]
|
||||
(when-let [{:keys [name media-id]} (db/exec-one! pool [sql:get-file file-id])]
|
||||
(cond-> (assoc default-context :title (str name " | Penpot"))
|
||||
(some? media-id)
|
||||
(assoc :image (resolve-image-uri media-id)))))
|
||||
|
||||
(defn- get-context
|
||||
[pool params]
|
||||
(let [file-id (some-> (:file-id params) d/parse-uuid)
|
||||
project-id (some-> (:project-id params) d/parse-uuid)
|
||||
team-id (some-> (:team-id params) d/parse-uuid)]
|
||||
(cond
|
||||
(some? file-id) (get-file-context pool file-id)
|
||||
(some? project-id) (assoc default-context :title "Project | Penpot")
|
||||
(some? team-id) (assoc default-context :title "Team dashboard | Penpot"))))
|
||||
|
||||
(defn- handler
|
||||
[{:keys [::db/pool]} request]
|
||||
(let [context (when (contains? cf/flags :link-unfurl)
|
||||
(get-context pool (:query-params request)))
|
||||
context (-> (or context default-context)
|
||||
(update :image #(or % (resolve-default-image-uri))))]
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "text/html; charset=utf-8"
|
||||
"cache-control" "no-store, no-cache, max-age=0"}
|
||||
::yres/body (-> (io/resource "app/templates/unfurl.tmpl")
|
||||
(tmpl/render context))}))
|
||||
|
||||
;; --- Initialization
|
||||
|
||||
(defmethod ig/assert-key ::routes
|
||||
[_ params]
|
||||
(assert (db/pool? (::db/pool params)) "expect valid database pool"))
|
||||
|
||||
(defmethod ig/init-key ::routes
|
||||
[_ cfg]
|
||||
["/unfurl" {:handler (partial handler cfg)}])
|
||||
@ -23,6 +23,7 @@
|
||||
[app.http.management :as mgmt]
|
||||
[app.http.session :as session]
|
||||
[app.http.session.tasks :as-alias session.tasks]
|
||||
[app.http.unfurl :as-alias http.unfurl]
|
||||
[app.http.websocket :as http.ws]
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
[app.metrics :as-alias mtx]
|
||||
@ -278,9 +279,13 @@
|
||||
::mgmt/routes (ig/ref ::mgmt/routes)
|
||||
::http.debug/routes (ig/ref ::http.debug/routes)
|
||||
::http.assets/routes (ig/ref ::http.assets/routes)
|
||||
::http.unfurl/routes (ig/ref ::http.unfurl/routes)
|
||||
::http.ws/routes (ig/ref ::http.ws/routes)
|
||||
::http.awsns/routes (ig/ref ::http.awsns/routes)}
|
||||
|
||||
::http.unfurl/routes
|
||||
{::db/pool (ig/ref ::db/pool)}
|
||||
|
||||
::http.debug/routes
|
||||
{::db/pool (ig/ref ::db/pool)
|
||||
::session/manager (ig/ref ::session/manager)
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
(:require
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.http :as-alias http]
|
||||
[app.http.access-token :as actoken]
|
||||
@ -137,6 +138,26 @@
|
||||
;; Tests: objects-handler — non-public buckets (auth required)
|
||||
;; ----------------------------------------------------------------
|
||||
|
||||
(t/deftest objects-handler-file-thumbnail-bucket-link-unfurl-flag
|
||||
;; Objects in the file-thumbnail bucket are public only when the
|
||||
;; link-unfurl flag is enabled.
|
||||
(let [storage (-> (:app.storage/storage th/*system*)
|
||||
(configure-storage-backend))
|
||||
cfg (make-handler-cfg storage)
|
||||
object (create-storage-object! storage "file-thumbnail" "thumbnail data")
|
||||
request {:path-params {:id (str (:id object))}}]
|
||||
|
||||
(t/testing "flag enabled"
|
||||
(with-redefs [cf/flags (conj cf/flags :link-unfurl)]
|
||||
(let [response (assets/objects-handler cfg request)]
|
||||
(t/is (not= 401 (::yres/status response)))
|
||||
(t/is (not= 404 (::yres/status response))))))
|
||||
|
||||
(t/testing "flag disabled"
|
||||
(with-redefs [cf/flags (disj cf/flags :link-unfurl)]
|
||||
(let [response (assets/objects-handler cfg request)]
|
||||
(t/is (= 401 (::yres/status response))))))))
|
||||
|
||||
(t/deftest objects-handler-non-public-bucket-no-auth
|
||||
;; Objects in non-public buckets should return 401 without authentication.
|
||||
(let [storage (-> (:app.storage/storage th/*system*)
|
||||
@ -198,10 +219,12 @@
|
||||
cfg (make-handler-cfg storage)
|
||||
profile (th/create-profile* 1)]
|
||||
|
||||
;; NOTE: file-thumbnail is not included here because it is public
|
||||
;; when the link-unfurl flag is enabled; see
|
||||
;; objects-handler-file-thumbnail-bucket-link-unfurl-flag.
|
||||
(doseq [bucket ["profile"
|
||||
"tempfile"
|
||||
"file-data"
|
||||
"file-thumbnail"
|
||||
"file-change"]]
|
||||
(t/testing (str "bucket: " bucket)
|
||||
(let [object (create-storage-object! storage bucket "some data")
|
||||
|
||||
98
backend/test/backend_tests/http_unfurl_test.clj
Normal file
98
backend/test/backend_tests/http_unfurl_test.clj
Normal file
@ -0,0 +1,98 @@
|
||||
;; 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 backend-tests.http-unfurl-test
|
||||
(:require
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.http.unfurl :as unfurl]
|
||||
[app.storage :as sto]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
[cuerdas.core :as str]
|
||||
[yetti.response :as-alias yres]))
|
||||
|
||||
(t/use-fixtures :once th/state-init)
|
||||
(t/use-fixtures :each (th/serial
|
||||
th/database-reset
|
||||
th/clean-storage))
|
||||
|
||||
(def ^:private default-title
|
||||
"Penpot | Full-stack design")
|
||||
|
||||
(defn- run-handler
|
||||
[query-params]
|
||||
(let [cfg {::db/pool (:app.db/pool th/*system*)}]
|
||||
(#'unfurl/handler cfg {:query-params query-params})))
|
||||
|
||||
(defn- create-file-thumbnail!
|
||||
[file-id]
|
||||
(let [storage (::sto/storage th/*system*)
|
||||
object (sto/put-object! storage {::sto/content (sto/content "thumbnail data")
|
||||
:bucket "file-thumbnail"
|
||||
:content-type "image/png"})]
|
||||
(db/insert! (:app.db/pool th/*system*) :file-thumbnail
|
||||
{:file-id file-id
|
||||
:revn 1
|
||||
:media-id (:id object)})
|
||||
object))
|
||||
|
||||
(t/deftest unfurl-without-params
|
||||
(let [response (run-handler {})]
|
||||
(t/is (= 200 (::yres/status response)))
|
||||
(t/is (str/includes? (::yres/body response) default-title))
|
||||
(t/is (str/includes? (::yres/body response) "/images/penpot-link-preview.png"))))
|
||||
|
||||
(t/deftest unfurl-file-without-thumbnail
|
||||
(let [profile (th/create-profile* 1)
|
||||
file (th/create-file* 1 {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)})
|
||||
response (run-handler {:file-id (str (:id file))})]
|
||||
(t/is (= 200 (::yres/status response)))
|
||||
(t/is (str/includes? (::yres/body response) (str (:name file) " | Penpot")))
|
||||
(t/is (str/includes? (::yres/body response) "/images/penpot-link-preview.png"))))
|
||||
|
||||
(t/deftest unfurl-file-with-thumbnail
|
||||
(let [profile (th/create-profile* 1)
|
||||
file (th/create-file* 1 {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)})
|
||||
object (create-file-thumbnail! (:id file))
|
||||
response (run-handler {:file-id (str (:id file))})]
|
||||
(t/is (= 200 (::yres/status response)))
|
||||
(t/is (str/includes? (::yres/body response) (str (:name file) " | Penpot")))
|
||||
(t/is (str/includes? (::yres/body response) (str "/assets/by-id/" (:id object))))))
|
||||
|
||||
(t/deftest unfurl-non-existent-file
|
||||
(let [response (run-handler {:file-id (str (uuid/next))})]
|
||||
(t/is (= 200 (::yres/status response)))
|
||||
(t/is (str/includes? (::yres/body response) default-title))))
|
||||
|
||||
(t/deftest unfurl-invalid-file-id
|
||||
(let [response (run-handler {:file-id "not-a-uuid"})]
|
||||
(t/is (= 200 (::yres/status response)))
|
||||
(t/is (str/includes? (::yres/body response) default-title))))
|
||||
|
||||
(t/deftest unfurl-team-link
|
||||
(let [response (run-handler {:team-id (str (uuid/next))})]
|
||||
(t/is (= 200 (::yres/status response)))
|
||||
(t/is (str/includes? (::yres/body response) "Team dashboard | Penpot"))))
|
||||
|
||||
(t/deftest unfurl-project-link
|
||||
(let [response (run-handler {:team-id (str (uuid/next))
|
||||
:project-id (str (uuid/next))})]
|
||||
(t/is (= 200 (::yres/status response)))
|
||||
(t/is (str/includes? (::yres/body response) "Project | Penpot"))))
|
||||
|
||||
(t/deftest unfurl-flag-disabled
|
||||
(let [profile (th/create-profile* 1)
|
||||
file (th/create-file* 1 {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)})]
|
||||
(with-redefs [cf/flags (disj cf/flags :link-unfurl)]
|
||||
(let [response (run-handler {:file-id (str (:id file))})]
|
||||
(t/is (= 200 (::yres/status response)))
|
||||
(t/is (str/includes? (::yres/body response) default-title))
|
||||
(t/is (not (str/includes? (::yres/body response) (:name file))))))))
|
||||
@ -170,7 +170,12 @@
|
||||
:mcp
|
||||
:background-blur
|
||||
:available-viewer-wasm
|
||||
:stroke-path})
|
||||
:stroke-path
|
||||
|
||||
;; Enables serving link preview (Open Graph) metadata for shared
|
||||
;; links; exposes file names and dashboard thumbnails to anyone
|
||||
;; that knows the file id.
|
||||
:link-unfurl})
|
||||
|
||||
(def all-flags
|
||||
(set/union email login varia))
|
||||
@ -204,7 +209,8 @@
|
||||
:enable-render-wasm-info
|
||||
:enable-available-viewer-wasm
|
||||
:enable-background-blur
|
||||
:enable-token-combobox])
|
||||
:enable-token-combobox
|
||||
:enable-link-unfurl])
|
||||
|
||||
(defn parse
|
||||
[& flags]
|
||||
|
||||
@ -49,6 +49,13 @@ http {
|
||||
'' close;
|
||||
}
|
||||
|
||||
# Link preview crawlers; their requests for the application root
|
||||
# are served with dynamic Open Graph metadata from the backend.
|
||||
map $http_user_agent $penpot_unfurl_agent {
|
||||
default 0;
|
||||
~*(slackbot|discordbot|twitterbot|facebookexternalhit|facebookcatalog|whatsapp|telegrambot|linkedinbot|skypeuripreview|pinterestbot|redditbot|embedly|iframely|mastodon|bluesky) 1;
|
||||
}
|
||||
|
||||
proxy_cache_path /tmp/cache/ levels=2:2 keys_zone=penpot:20m;
|
||||
proxy_cache_methods GET HEAD;
|
||||
proxy_cache_valid any 48h;
|
||||
@ -115,6 +122,10 @@ http {
|
||||
add_header x-internal-redirect "$upstream_http_x_accel_redirect";
|
||||
}
|
||||
|
||||
location = /unfurl {
|
||||
proxy_pass http://127.0.0.1:6060/unfurl$is_args$args;
|
||||
}
|
||||
|
||||
# On production, this is controlled by ELB
|
||||
location /api/export {
|
||||
proxy_pass http://127.0.0.1:6061;
|
||||
@ -291,6 +302,10 @@ http {
|
||||
return 301 " /404";
|
||||
}
|
||||
|
||||
if ($penpot_unfurl_agent) {
|
||||
rewrite ^/$ /unfurl last;
|
||||
}
|
||||
|
||||
include /home/penpot/penpot/docker/devenv/files/nginx-security-headers.conf;
|
||||
add_header Cache-Control "no-store" always;
|
||||
try_files $uri /index.html$is_args$args /index.html =404;
|
||||
|
||||
@ -57,6 +57,13 @@ http {
|
||||
'' close;
|
||||
}
|
||||
|
||||
# Link preview crawlers; their requests for the application root
|
||||
# are served with dynamic Open Graph metadata from the backend.
|
||||
map $http_user_agent $penpot_unfurl_agent {
|
||||
default 0;
|
||||
~*(slackbot|discordbot|twitterbot|facebookexternalhit|facebookcatalog|whatsapp|telegrambot|linkedinbot|skypeuripreview|pinterestbot|redditbot|embedly|iframely|mastodon|bluesky) 1;
|
||||
}
|
||||
|
||||
proxy_cache_path /tmp/cache/ levels=2:2 keys_zone=penpot:20m;
|
||||
proxy_cache_methods GET HEAD;
|
||||
proxy_cache_valid any 48h;
|
||||
@ -125,6 +132,10 @@ http {
|
||||
add_header x-internal-redirect "$upstream_http_x_accel_redirect";
|
||||
}
|
||||
|
||||
location = /unfurl {
|
||||
proxy_pass $PENPOT_BACKEND_URI/unfurl$is_args$args;
|
||||
}
|
||||
|
||||
location /api/export {
|
||||
proxy_pass $PENPOT_EXPORTER_URI;
|
||||
}
|
||||
@ -172,6 +183,10 @@ http {
|
||||
return 301 " /404";
|
||||
}
|
||||
|
||||
if ($penpot_unfurl_agent) {
|
||||
rewrite ^/$ /unfurl last;
|
||||
}
|
||||
|
||||
include /etc/nginx/nginx-security-headers.conf;
|
||||
add_header Cache-Control "no-store, no-cache, max-age=0" always;
|
||||
try_files $uri /index.html$is_args$args /index.html =404;
|
||||
|
||||
@ -65,6 +65,23 @@
|
||||
|
||||
;; --- Navigate (Event)
|
||||
|
||||
(defn match->context-params
|
||||
"Extract the params that give sharing context to the current URL.
|
||||
|
||||
They are mirrored on the query string (before the fragment) because
|
||||
the fragment is never sent to the server; this way shared links
|
||||
carry enough context for rendering link preview (unfurl) metadata."
|
||||
[match]
|
||||
(let [path-params (dm/get-in match [:params :path])
|
||||
query-params (get match :query-params)
|
||||
file-id (or (get query-params :file-id) (get path-params :file-id))
|
||||
team-id (or (get query-params :team-id) (get path-params :team-id))
|
||||
project-id (or (get query-params :project-id) (get path-params :project-id))]
|
||||
(cond
|
||||
(some? file-id) {:file-id file-id}
|
||||
(some? project-id) {:team-id team-id :project-id project-id}
|
||||
(some? team-id) {:team-id team-id})))
|
||||
|
||||
(defn navigated
|
||||
[match send-event-info?]
|
||||
(ptk/reify ::navigated
|
||||
@ -85,7 +102,16 @@
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc :route match)
|
||||
(dissoc :exception)))))
|
||||
(dissoc :exception)))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
(let [query (some-> (match->context-params match)
|
||||
(u/map->query-string))
|
||||
href (dm/str (.-pathname globals/location)
|
||||
(if (some? query) (dm/str "?" query) "")
|
||||
(.-hash globals/location))]
|
||||
(.replaceState js/history nil "" href)))))
|
||||
|
||||
(defn navigate
|
||||
[id params & {:keys [::replace ::new-window] :as options}]
|
||||
|
||||
41
frontend/test/frontend_tests/router_test.cljs
Normal file
41
frontend/test/frontend_tests/router_test.cljs
Normal file
@ -0,0 +1,41 @@
|
||||
;; 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.router-test
|
||||
(:require
|
||||
[app.main.router :as rt]
|
||||
[cljs.test :as t :include-macros true]))
|
||||
|
||||
(t/deftest match-context-params-file-link
|
||||
;; Workspace and viewer links only mirror the file-id.
|
||||
(let [match {:query-params {:team-id "team-1"
|
||||
:file-id "file-1"
|
||||
:page-id "page-1"}}]
|
||||
(t/is (= {:file-id "file-1"}
|
||||
(rt/match->context-params match)))))
|
||||
|
||||
(t/deftest match-context-params-file-link-path-params
|
||||
;; Legacy routes carry the ids as path params.
|
||||
(let [match {:params {:path {:project-id "project-1"
|
||||
:file-id "file-1"}}}]
|
||||
(t/is (= {:file-id "file-1"}
|
||||
(rt/match->context-params match)))))
|
||||
|
||||
(t/deftest match-context-params-project-link
|
||||
(let [match {:query-params {:team-id "team-1"
|
||||
:project-id "project-1"}}]
|
||||
(t/is (= {:team-id "team-1"
|
||||
:project-id "project-1"}
|
||||
(rt/match->context-params match)))))
|
||||
|
||||
(t/deftest match-context-params-team-link
|
||||
(let [match {:query-params {:team-id "team-1"}}]
|
||||
(t/is (= {:team-id "team-1"}
|
||||
(rt/match->context-params match)))))
|
||||
|
||||
(t/deftest match-context-params-no-context
|
||||
(let [match {:query-params {:token "some-token"}}]
|
||||
(t/is (nil? (rt/match->context-params match)))))
|
||||
@ -46,6 +46,7 @@
|
||||
[frontend-tests.plugins.utils-test]
|
||||
[frontend-tests.plugins.value-objects-test]
|
||||
[frontend-tests.render-wasm.process-objects-test]
|
||||
[frontend-tests.router-test]
|
||||
[frontend-tests.svg-fills-test]
|
||||
[frontend-tests.tokens.import-export-test]
|
||||
[frontend-tests.tokens.logic.token-actions-test]
|
||||
@ -120,6 +121,7 @@
|
||||
'frontend-tests.plugins.utils-test
|
||||
'frontend-tests.plugins.value-objects-test
|
||||
'frontend-tests.render-wasm.process-objects-test
|
||||
'frontend-tests.router-test
|
||||
'frontend-tests.svg-fills-test
|
||||
'frontend-tests.tokens.import-export-test
|
||||
'frontend-tests.tokens.logic.token-actions-test
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user