Files
metabase/test/metabase/embedding_rest/api/preview_embed_test.clj
Alexander Polyankin 2350e6a952 Remove unused data from embed and public endpoints (#80510)
* Remove unused data from embed and public endpoints

Public and static-embed card/dashboard/document endpoints now return a
minimal placeholder dataset_query (a blank native query against the same
database) instead of selecting individual query keys. The frontend only
needs the database id here. Parameter combining and :param_fields
hydration in public-card now run before the card is trimmed since both
read template tags from the full query.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Add tests for embed card and dashboard dataset_query

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Assert strictly on dataset_query in public and embed tests

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Handle native queries in Table viz

* Fix column shadowing in HeaderCellWithColumnInfo

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Use param_fields for public question parameters

Public and static-embed card payloads carry param_fields instead of the
real query, so PublicOrEmbeddedQuestion now builds UI parameters from
them via getCardUiParametersFromParamFields, mirroring the saved-mode
dashboard path. The query-based helpers are renamed to the FromQuery /
FromParamFields scheme on both the card and dashboard sides, and the
ParameterWithTarget casts are dropped now that getParameterTargetField
accepts an optional target.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Use param_fields for embed setup card parameters

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Use param_fields for guest embed SDK question parameters

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Route card UI parameters by param_fields presence

Restores getCardUiParameters as the entry point: it delegates to
getCardUiParametersFromParamFields when the card carries param_fields
(public and static-embed payloads) and to the query-based variant
otherwise. The SDK SQL parameter surfaces use it since they serve both
guest and authenticated contexts. Also reverts the dashboard helper
renames back to getSaved/getUnsavedDashboardUiParameters.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fix preview embed card param values

The preview-embed card values and remapping endpoints get their card via
card-for-unsigned-token -> public-card, which now replaces the query with
a blank native placeholder, so field-filter parameters could no longer be
resolved and the response schema rejected the nil result. Bind
qp.perms/*param-values-query* around the card fetch, the escape hatch
remove-card-non-public-columns already honors, matching the public
sharing values endpoints.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Add preview embed test for remapped parameter fields

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix

* vscode fix

* Backfill template tag parameters in param_fields UI parameters

GET /api/card/:id returns parameters as stored, unlike the public and
embed endpoints which combine them with template tags, so
getCardUiParametersFromParamFields now derives parameters via
getParametersFromCard like the query-based variant always did. Guest
embed payloads are unaffected: their parameters arrive already combined
and the blank query derives nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Use getCardUiParameters dispatcher in SDK load-question

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Guard applyTemplateTagParameters by param_fields presence

Cards whose payloads carry param_fields have parameters already combined
with template tags by the backend, so re-applying from the query would
wipe them. Keying the guard on the card payload keeps it in sync with
how the card was loaded, replacing the isGuestEmbed flag checks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Use getCardUiParameters everywhere and build test queries via Lib

Question.parameters and the query builder call sites go back to the
getCardUiParameters entry point, which now routes by param_fields
presence, leaving those files unchanged relative to master. The
template tag fallback test builds its query with
Lib.createTestJsNativeQuery instead of an inline legacy map.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Make getCardUiParameters the only entry point

The FromQuery and FromParamFields variants are private to cards.ts now;
all consumers call getCardUiParameters, which routes by param_fields
presence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Build preview embed remapped-fields test query via lib

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Restore null fallback in useAvailableParameters

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fix

* Preserve query type in stripped public and embed card queries

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Add placeholder aggregations to stripped MBQL queries

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Keep parameter template tags in stripped native queries and revert FE workarounds

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Export parameter-template-tag? via import-vars

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Add test for :param_fields hydration on GET /api/card/:id

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 12:19:27 -04:00

830 lines
58 KiB
Clojure

(ns ^:mb/driver-tests metabase.embedding-rest.api.preview-embed-test
{:clj-kondo/config '{:linters {:deprecated-var {:exclude {metabase.test.data/mbql-query {:namespaces [metabase.embedding-rest.api.preview-embed-test]}
metabase.test.data/query {:namespaces [metabase.embedding-rest.api.preview-embed-test]}
metabase.test.data/run-mbql-query {:namespaces [metabase.embedding-rest.api.preview-embed-test]}}}}}}
(:require
[buddy.sign.jwt :as jwt]
[clojure.set :as set]
[clojure.test :refer :all]
[metabase.dashboards-rest.api-test :as api.dashboard-test]
[metabase.embedding-rest.api.embed-test :as embed-test]
[metabase.embedding-rest.api.preview-embed :as api.preview-embed]
[metabase.lib.core :as lib]
[metabase.lib.metadata :as lib.metadata]
[metabase.queries-rest.api.card-test :as api.card-test]
[metabase.query-processor.pivot.test-util :as api.pivots]
[metabase.test :as mt]
[metabase.tiles.api-test :as tiles.api-test]
[metabase.util :as u]
[metabase.util.json :as json]
[metabase.util.random :as u.random]
[toucan2.core :as t2]))
;;; --------------------------------------- GET /api/preview_embed/card/:token ---------------------------------------
(defn- card-url [card & [additional-token-params]]
(str "preview_embed/card/" (embed-test/card-token card (merge {:_embedding_params {}} additional-token-params))))
(deftest card-test
(testing "GET /api/preview_embed/card/:token"
(embed-test/with-embedding-enabled-and-new-secret-key!
(embed-test/with-temp-card [card]
(testing "it should be possible to use this endpoint successfully if all the conditions are met"
(is (=? embed-test/successful-card-info
(mt/user-http-request :crowberto :get 200 (card-url card)))))
(testing "if the user is not an admin this endpoint should fail"
(is (= "You don't have permissions to do that."
(mt/user-http-request :rasta :get 403 (card-url card)))))
(testing "check that the endpoint doesn't work if embedding isn't enabled"
(mt/with-temporary-setting-values [enable-embedding-static false]
(is (= "Embedding is not enabled."
(embed-test/with-temp-card [card]
(mt/user-http-request :crowberto :get 400 (card-url card)))))))
(testing "check that if embedding is enabled globally requests fail if they are signed with the wrong key"
(is (= "Message seems corrupt or manipulated"
(mt/user-http-request :crowberto :get 400 (embed-test/with-new-secret-key! (card-url card))))))
(testing "Check that only ENABLED params that ARE NOT PRESENT IN THE JWT come back"
(embed-test/with-temp-card [card {:dataset_query
{:database (mt/id)
:type :native
:native {:query "SELECT 1;"
:template-tags {:a {:type "date", :name "a", :display_name "a" :id "a"}
:b {:type "date", :name "b", :display_name "b" :id "b"}
:c {:type "date", :name "c", :display_name "c" :id "c"}
:d {:type "date", :name "d", :display_name "d" :id "d"}}}}}]
(is (= [{:id "d"
:type "date/single"
:target ["variable" ["template-tag" "d"]]
:name "d"
:slug "d"
:default nil
:required false}]
(-> (mt/user-http-request :crowberto :get 200 (card-url card {:_embedding_params {:a "locked"
:b "disabled"
:c "enabled"
:d "enabled"}
:params {:c 100}}))
:parameters)))))))))
;;; ------------------------------------ GET /api/preview_embed/card/:token/query ------------------------------------
(defn- card-query-url [card & [additional-token-params]]
(str "preview_embed/card/"
(embed-test/card-token card (merge {:_embedding_params {}} additional-token-params))
"/query"))
(deftest query-test
(testing "GET /api/preview_embed/card/:token/query"
(embed-test/with-embedding-enabled-and-new-secret-key!
(embed-test/with-temp-card [card]
(testing "It should be possible to run a Card successfully if you jump through the right hoops..."
#_{:clj-kondo/ignore [:deprecated-var]}
(embed-test/test-query-results
(mt/user-http-request :crowberto :get 202 (card-query-url card))))
(testing "if the user is not an admin this endpoint should fail"
(is (= "You don't have permissions to do that."
(mt/user-http-request :rasta :get 403 (card-query-url card)))))
(testing "check that the endpoint doesn't work if embedding isn't enabled"
(mt/with-temporary-setting-values [enable-embedding-static false]
(is (= "Embedding is not enabled."
(mt/user-http-request :crowberto :get 400 (card-query-url card))))))
(testing "check that if embedding is enabled globally requests fail if they are signed with the wrong key"
(is (= "Message seems corrupt or manipulated"
(mt/user-http-request :crowberto :get 400 (embed-test/with-new-secret-key! (card-query-url card))))))))))
(deftest query-locked-params-test
(testing "GET /api/preview_embed/card/:token/query"
(testing "LOCKED params"
(embed-test/with-embedding-enabled-and-new-secret-key!
(embed-test/with-temp-card [card]
(testing "check that if embedding is enabled globally fail if the token is missing a `:locked` parameter"
(is (= "You must specify a value for :venue_id in the JWT."
(mt/user-http-request :crowberto :get 400 (card-query-url card {:_embedding_params {:venue_id "locked"}})))))
(testing "if `:locked` param is supplied, request should succeed"
#_{:clj-kondo/ignore [:deprecated-var]}
(embed-test/test-query-results
(mt/user-http-request :crowberto :get 202 (card-query-url card {:_embedding_params {:venue_id "locked"}
:params {:venue_id 100}}))))
(testing "if `:locked` parameter is present in URL params, request should fail"
(is (= "You can only specify a value for :venue_id in the JWT."
(mt/user-http-request :crowberto :get 400 (str (card-query-url card {:_embedding_params {:venue_id "locked"}
:params {:venue_id 100}})
"?venue_id=200"))))))))))
(deftest query-disabled-params-test
(testing "GET /api/preview_embed/card/:token/query"
(testing "DISABLED params"
(embed-test/with-embedding-enabled-and-new-secret-key!
(embed-test/with-temp-card [card]
(testing "check that if embedding is enabled globally and for the object requests fail if they pass a `:disabled` parameter"
(is (= "You're not allowed to specify a value for :venue_id."
(mt/user-http-request :crowberto :get 400 (card-query-url card {:_embedding_params {:venue_id "disabled"}
:params {:venue_id 100}})))))
(testing "If a `:disabled` param is passed in the URL the request should fail"
(is (= "You're not allowed to specify a value for :venue_id."
(mt/user-http-request :crowberto :get 400 (str (card-query-url card {:_embedding_params {:venue_id "disabled"}})
"?venue_id=200"))))))))))
(deftest query-enabled-params-test
(testing "GET /api/preview_embed/card/:token/query"
(testing "ENABLED params"
(embed-test/with-embedding-enabled-and-new-secret-key!
(embed-test/with-temp-card [card]
(testing "If `:enabled` param is present in both JWT and the URL, the request should fail"
(is (= "You can't specify a value for :venue_id if it's already set in the JWT."
(mt/user-http-request :crowberto :get 400 (str (card-query-url card {:_embedding_params {:venue_id "enabled"}
:params {:venue_id 100}})
"?venue_id=200")))))
(testing "If an `:enabled` param is present in the JWT, that's ok"
#_{:clj-kondo/ignore [:deprecated-var]}
(embed-test/test-query-results
(mt/user-http-request :crowberto :get 202 (card-query-url card {:_embedding_params {:venue_id "enabled"}
:params {:venue_id "enabled"}}))))
(testing "If an `:enabled` param is present in URL params but *not* the JWT, that's ok"
#_{:clj-kondo/ignore [:deprecated-var]}
(embed-test/test-query-results
(mt/user-http-request :crowberto :get 202 (str (card-query-url card {:_embedding_params {:venue_id "enabled"}})
"?venue_id=200")))))))))
(deftest default-value-card-query-test
(testing "GET /api/preview_embed/card/:token/query with default values for params"
(embed-test/with-embedding-enabled-and-new-secret-key!
(testing "if the param is enabled"
(mt/with-temp
[:model/Card card (assoc (embed-test/card-with-date-field-filter-default) :embedding_params {:date "enabled"})]
(testing "the default should apply if no param value is provided"
(is (= [[107]]
(mt/rows (mt/user-http-request :crowberto :get 202 (card-query-url card)))))
(testing "check this is the same result as when a default value is provided"
(is (= [[107]]
(mt/rows (mt/user-http-request :crowberto :get 202 (str (card-query-url card {:_embedding_params {:date "enabled"}})
"?date=Q1-2014")))))))
(testing "an empty value should apply if provided as an empty string in the query params"
(is (= [[1000]]
(mt/rows (mt/user-http-request :crowberto :get 202 (str (card-query-url card {:_embedding_params {:date "enabled"}})
"?date="))))))
(testing "an empty value should apply if provided as nil in the JWT params"
(is (= [[1000]]
(mt/rows (mt/user-http-request :crowberto :get 202 (card-query-url card {:_embedding_params {:date "enabled"}
:params {:date nil}}))))))))
(testing "if the param is disabled"
(mt/with-temp
[:model/Card card (assoc (embed-test/card-with-date-field-filter-default) :embedding_params {:date "disabled"})]
(testing "the default should apply if no param is provided"
(is (= [[107]]
(mt/rows (mt/user-http-request :crowberto :get 202 (card-query-url card))))))
(testing "you can't apply an empty param value if the parameter is disabled"
(is (= "You're not allowed to specify a value for :date."
(mt/user-http-request :crowberto :get 400 (str (card-query-url card {:_embedding_params {:date "disabled"}}) "?date=")))))))
(testing "if the param is locked"
(mt/with-temp
[:model/Card card (assoc (embed-test/card-with-date-field-filter-default) :embedding_params {:date "locked"})]
(testing "an empty value with `nil` as the param's value is invalid and should result in an error"
(is (= "You must specify a value for :date in the JWT."
(mt/user-http-request :crowberto :get 400 (card-query-url card {:_embedding_params {:date "locked"}
:params {:date nil}}))))
(testing "check this is different to when a non-nil value is provided"
(is (= [[138]]
(mt/rows (mt/user-http-request :crowberto :get 202 (card-query-url card {:_embedding_params {:date "locked"}
:params {:date "Q2-2014"}})))))))
(testing "an empty string value is invalid and should result in an error"
(is (= "You must specify a value for :date in the JWT."
(mt/user-http-request :crowberto :get 400 (card-query-url card {:_embedding_params {:date "locked"}
:params {:date ""}}))))))))))
(deftest query-max-results-constraint-test
(testing "GET /api/preview_embed/card/:token/query"
(testing "Only 2000 results returned when there are many more"
(let [orders-row-count (count
(mt/rows
(mt/dataset test-data
(mt/process-query
(mt/query orders)))))
expected-row-count 1]
(with-redefs [api.preview-embed/max-results expected-row-count]
(mt/dataset test-data
(embed-test/with-embedding-enabled-and-new-secret-key!
(let [sample-db-orders-question (mt/query orders)]
(embed-test/with-temp-card [card {:dataset_query sample-db-orders-question}]
(let [limited (count
(mt/rows
(mt/user-http-request :crowberto :get 202 (card-query-url card))))]
(is (= expected-row-count limited))
(is (not= expected-row-count orders-row-count))))))))))))
;;; ------------------------------------ GET /api/preview_embed/dashboard/:token -------------------------------------
(defn- dashboard-url [dashboard & [additional-token-params]]
(str "preview_embed/dashboard/" (embed-test/dash-token dashboard (merge {:_embedding_params {}}
additional-token-params))))
(deftest dashboard-test
(testing "GET /api/preview_embed/dashboard/:token"
(embed-test/with-embedding-enabled-and-new-secret-key!
(mt/with-temp [:model/Dashboard dash]
(testing "it should be possible to call this endpoint successfully..."
(is (=? embed-test/successful-dashboard-info
(mt/user-http-request :crowberto :get 200 (dashboard-url dash)))))
(testing "...but if the user is not an admin this endpoint should fail"
(is (= "You don't have permissions to do that."
(mt/user-http-request :rasta :get 403 (dashboard-url dash)))))
(testing "check that the endpoint doesn't work if embedding isn't enabled"
(mt/with-temporary-setting-values [enable-embedding-static false]
(is (= "Embedding is not enabled."
(mt/user-http-request :crowberto :get 400 (dashboard-url dash))))))
(testing "check that if embedding is enabled globally requests fail if they are signed with the wrong key"
(is (= "Message seems corrupt or manipulated"
(mt/user-http-request :crowberto :get 400 (embed-test/with-new-secret-key! (dashboard-url dash))))))))))
(deftest only-enabled-params-not-in-jwt-test
(testing "Check that only ENABLED params that ARE NOT PRESENT IN THE JWT come back"
(embed-test/with-embedding-enabled-and-new-secret-key!
(mt/with-temp [:model/Dashboard dash {:parameters [{:id "_a", :slug "a", :name "a", :type "date"}
{:id "_b", :slug "b", :name "b", :type "date"}
{:id "_c", :slug "c", :name "c", :type "date"}
{:id "_d", :slug "d", :name "d", :type "date"}]}]
(is (=? [{:id "_d", :slug "d", :name "d", :type "date"}]
(:parameters (mt/user-http-request :crowberto :get 200 (dashboard-url dash
{:params {:c 100}
:_embedding_params {:a "locked"
:b "disabled"
:c "enabled"
:d "enabled"}})))))))))
;;; ------------------ GET /api/preview_embed/dashboard/:token/dashcard/:dashcard-id/card/:card-id -------------------
(defn- dashcard-url [dashcard & [additional-token-params]]
(str "preview_embed/dashboard/" (embed-test/dash-token (:dashboard_id dashcard) (merge {:_embedding_params {}}
additional-token-params))
"/dashcard/" (u/the-id dashcard)
"/card/" (:card_id dashcard)))
(deftest dashcard-test
(testing "/api/preview_embed/dashboard/:token/dashcard/:dashcard-id/card/:card-id"
(embed-test/with-embedding-enabled-and-new-secret-key!
(embed-test/with-temp-dashcard [dashcard]
(testing "It should be possible to run a Card successfully if you jump through the right hoops..."
#_{:clj-kondo/ignore [:deprecated-var]}
(embed-test/test-query-results
(mt/user-http-request :crowberto :get 202 (dashcard-url dashcard))))
(testing "...but if the user is not an admin this endpoint should fail"
(is (= "You don't have permissions to do that."
(mt/user-http-request :rasta :get 403 (dashcard-url dashcard)))))
(testing "check that the endpoint doesn't work if embedding isn't enabled"
(is (= "Embedding is not enabled."
(mt/with-temporary-setting-values [enable-embedding-static false]
(mt/user-http-request :crowberto :get 400 (dashcard-url dashcard))))))
(testing "check that if embedding is enabled globally requests fail if they are signed with the wrong key"
(is (= "Message seems corrupt or manipulated"
(mt/user-http-request :crowberto :get 400 (embed-test/with-new-secret-key! (dashcard-url dashcard))))))))))
(deftest dashcard-locked-params-test
(testing "/api/preview_embed/dashboard/:token/dashcard/:dashcard-id/card/:card-id"
(testing "LOCKED params"
(embed-test/with-embedding-enabled-and-new-secret-key!
(embed-test/with-temp-dashcard [dashcard]
(testing "check that if embedding is enabled globally fail if the token is missing a `:locked` parameter"
(is (= "You must specify a value for :venue_id in the JWT."
(mt/user-http-request :crowberto :get 400 (dashcard-url dashcard
{:_embedding_params {:venue_id "locked"}})))))
(testing "If `:locked` param is supplied, request should succeed"
(is (=? {:status "completed"
:data {:rows [[1]]}}
(mt/user-http-request :crowberto :get 202
(dashcard-url dashcard {:_embedding_params {:venue_id "locked"}, :params {:venue_id 100}})))))
(testing "If `:locked` parameter is present in URL params, request should fail"
(is (= "You can only specify a value for :venue_id in the JWT."
(mt/user-http-request :crowberto :get 400 (str (dashcard-url dashcard
{:_embedding_params {:venue_id "locked"}, :params {:venue_id 100}})
"?venue_id=200"))))))))))
(deftest dashcard-disabled-params-test
(testing "/api/preview_embed/dashboard/:token/dashcard/:dashcard-id/card/:card-id"
(testing "DISABLED params"
(embed-test/with-embedding-enabled-and-new-secret-key!
(embed-test/with-temp-dashcard [dashcard]
(testing "check that if embedding is enabled globally and for the object requests fail if they pass a `:disabled` parameter"
(is (= "You're not allowed to specify a value for :venue_id."
(mt/user-http-request :crowberto :get 400 (dashcard-url dashcard
{:_embedding_params {:venue_id "disabled"}, :params {:venue_id 100}})))))
(testing "If a `:disabled` param is passed in the URL the request should fail"
(is (= "You're not allowed to specify a value for :venue_id."
(mt/user-http-request :crowberto :get 400 (str (dashcard-url dashcard {:_embedding_params {:venue_id "disabled"}})
"?venue_id=200"))))))))))
(deftest dashcard-disabled-params-test-2
(testing "/api/preview_embed/dashboard/:token/dashcard/:dashcard-id/card/:card-id"
(testing "ENABLED params"
(embed-test/with-embedding-enabled-and-new-secret-key!
(embed-test/with-temp-dashcard [dashcard]
(testing "If `:enabled` param is present in both JWT and the URL, the request should fail"
(is (= "You can't specify a value for :venue_id if it's already set in the JWT."
(mt/user-http-request :crowberto :get 400 (str (dashcard-url dashcard {:_embedding_params {:venue_id "enabled"}
:params {:venue_id 100}})
"?venue_id=200")))))
(testing "If an `:enabled` param is present in the JWT, that's ok"
(is (=? {:status "completed"
:data {:rows [[1]]}}
(mt/user-http-request :crowberto :get 202 (dashcard-url dashcard {:_embedding_params {:venue_id "enabled"}
:params {:venue_id 100}})))))
(testing "If an `:enabled` param is present in URL params but *not* the JWT, that's ok"
(is (=? {:status "completed"
:data {:rows [[0]]}}
(mt/user-http-request :crowberto :get 202 (str (dashcard-url dashcard {:_embedding_params {:venue_id "enabled"}})
"?venue_id=200"))))))))))
(deftest dashcard-editable-query-params-test
(testing (str "Check that editable query params work correctly and keys get coverted from strings to keywords, even "
"if they're something that our middleware doesn't normally assume is implicitly convertable to a "
"keyword. See `ring.middleware.keyword-params/keyword-syntax?` (#6783)")
(embed-test/with-embedding-enabled-and-new-secret-key!
(embed-test/with-temp-dashcard [dashcard {:dash {:enable_embedding true
:parameters [{:id "_SECOND_DATE_SEEN_"
:slug "2nd_date_seen"
:name "Second Date Seen"
:type "date"}
{:id "_NUM_BIRDS_"
:slug "num_birds"
:name "Number of Birds"
:type "number"}]}}]
(is (=? {:status "completed"}
(mt/user-http-request :crowberto :get 202 (str (dashcard-url dashcard
{:_embedding_params {:num_birds :locked
:2nd_date_seen :enabled}
:params {:num_birds 2}})
"?2nd_date_seen=2018-02-14"))))))))
(deftest editable-params-should-not-be-invalid-test
(testing "Make sure that editable params do not result in \"Invalid Parameter\" exceptions (#7212)"
(embed-test/with-embedding-enabled-and-new-secret-key!
(mt/with-temp [:model/Card card {:dataset_query {:database (mt/id)
:type :native
:native {:query "SELECT {{num}} AS num"
:template-tags {:num {:id "X"
:name "num"
:display_name "Num"
:type "number"
:required true
:default "1"}}}}}]
(embed-test/with-temp-dashcard [dashcard {:dash {:parameters [{:name "Num"
:slug "num"
:id "_NUM_"
:type "category"}]}
:dashcard {:card_id (u/the-id card)
:parameter_mappings [{:card_id (u/the-id card)
:target [:variable
[:template-tag :num]]
:parameter_id "_NUM_"}]}}]
(is (= [[50]]
(mt/rows (mt/user-http-request :crowberto :get 202
(str (dashcard-url dashcard {:_embedding_params {:num "enabled"}})
"?num=50"))))))))))
(deftest postgres-convert-parameters-to-numbers-test
(mt/test-driver :postgres
(testing "Make sure that ID params correctly get converted to numbers as needed (Postgres-specific)..."
(embed-test/with-embedding-enabled-and-new-secret-key!
(mt/with-temp [:model/Card card {:dataset_query (mt/mbql-query venues
{:aggregation [:count]})}]
(embed-test/with-temp-dashcard [dashcard {:dash {:parameters [{:name "Venue ID"
:slug "venue_id"
:id "_VENUE_ID_"
:type "id"}]}
:dashcard {:card_id (u/the-id card)
:parameter_mappings [{:parameter_id "_VENUE_ID_"
:card_id (u/the-id card)
:target [:dimension
[:field
(mt/id :venues :id) nil]]}]}}]
(is (= [[1]]
(mt/rows (mt/user-http-request :crowberto :get (str (dashcard-url dashcard {:_embedding_params {:venue_id "enabled"}})
"?venue_id=1")))))))))))
;; pivot tables
(defn- pivot-card-query-url [card & [additional-token-params]]
(str "preview_embed/pivot/card/"
(embed-test/card-token card (merge {:_embedding_params {}} additional-token-params))
"/query"))
(deftest pivot-query-test
(mt/test-drivers (api.pivots/applicable-drivers)
(mt/dataset test-data
(testing "GET /api/preview_embed/pivot/card/:token/query"
(testing "successful preview"
(let [result (embed-test/with-embedding-enabled-and-new-secret-key!
(embed-test/with-temp-card [card (api.pivots/pivot-card)]
(mt/user-http-request :crowberto :get 202 (pivot-card-query-url card))))
rows (mt/rows result)]
(is (nil? (:row_count result))) ;; row_count isn't included in public endpoints
(is (= "completed" (:status result)))
(is (= 6 (count (get-in result [:data :cols]))))
(is (= 1144 (count rows)))))
(testing "should fail if user is not an admin"
(is (= "You don't have permissions to do that."
(embed-test/with-embedding-enabled-and-new-secret-key!
(embed-test/with-temp-card [card (api.pivots/pivot-card)]
(mt/user-http-request :rasta :get 403 (pivot-card-query-url card)))))))
(testing "should fail if embedding is disabled"
(is (= "Embedding is not enabled."
(mt/with-temporary-setting-values [enable-embedding false]
(embed-test/with-new-secret-key!
(embed-test/with-temp-card [card (api.pivots/pivot-card)]
(mt/user-http-request :crowberto :get 400 (pivot-card-query-url card))))))))
(testing "should fail if embedding is enabled and the wrong key is used"
(is (= "Message seems corrupt or manipulated"
(embed-test/with-embedding-enabled-and-new-secret-key!
(embed-test/with-temp-card [card (api.pivots/pivot-card)]
(mt/user-http-request :crowberto :get 400 (embed-test/with-new-secret-key! (pivot-card-query-url card))))))))))))
(defn- pivot-dashcard-url [dashcard & [additional-token-params]]
(str "preview_embed/pivot/dashboard/"
(embed-test/dash-token (:dashboard_id dashcard) (merge {:_embedding_params {}}
additional-token-params))
"/dashcard/" (u/the-id dashcard)
"/card/" (:card_id dashcard)))
(deftest pivot-card-id-test
(mt/test-drivers (api.pivots/applicable-drivers)
(mt/dataset test-data
(testing "GET /api/preview_embed/pivot/dashboard/:token/dashcard/:dashcard-id/card/:card-id"
(embed-test/with-embedding-enabled-and-new-secret-key!
(embed-test/with-temp-dashcard [dashcard {:dash {:parameters []}
:card (api.pivots/pivot-card)
:dashcard {:parameter_mappings []}}]
(testing "successful preview"
(let [result (mt/user-http-request :crowberto :get 202 (pivot-dashcard-url dashcard))
rows (mt/rows result)]
(is (nil? (:row_count result))) ;; row_count isn't included in public endpoints
(is (= "completed" (:status result)))
(is (= 6 (count (get-in result [:data :cols]))))
(is (= 1144 (count rows)))))
(testing "should fail if user is not an admin"
(is (= "You don't have permissions to do that."
(mt/user-http-request :rasta :get 403 (pivot-dashcard-url dashcard)))))
(testing "should fail if embedding is disabled"
(is (= "Embedding is not enabled."
(mt/with-temporary-setting-values [enable-embedding-static false]
(embed-test/with-new-secret-key!
(mt/user-http-request :crowberto :get 400 (pivot-dashcard-url dashcard)))))))
(testing "should fail if embedding is enabled and the wrong key is used"
(is (= "Message seems corrupt or manipulated"
(mt/user-http-request :crowberto :get 400 (embed-test/with-new-secret-key! (pivot-dashcard-url dashcard))))))))))))
(deftest handle-single-params-for-operator-filters-test
(testing "Query endpoints should work with a single URL parameter for an operator filter (#20438)"
(mt/dataset test-data
(embed-test/with-embedding-enabled-and-new-secret-key!
(mt/with-temp [:model/Card {card-id :id, :as card} {:dataset_query (mt/native-query
{:query "SELECT count(*) AS count FROM PUBLIC.PEOPLE WHERE true [[AND {{NAME}}]]"
:template-tags {"NAME"
{:name "NAME"
:display-name "Name"
:id "_name_"
:type :dimension
:dimension [:field (mt/id :people :name) nil]
:widget-type :string/=
:default nil}}})
:enable_embedding true
:embedding_params {"NAME" "enabled"}}]
(testing "Card"
(let [url (card-query-url card {:_embedding_params {:NAME "enabled"}})]
(is (= [[1]]
(mt/rows (mt/user-http-request :crowberto :get 202 url :NAME "Hudson Borer"))
(mt/rows (mt/user-http-request :crowberto :get 202 url :NAME "Hudson Borer" :NAME "x"))))))
(testing "Dashcard"
(mt/with-temp [:model/Dashboard {dashboard-id :id} {:enable_embedding true
:embedding_params {:name "enabled"}
:parameters [{:name "Name"
:slug "name"
:id "_name_"
:type "string/="
:sectionId "string"}]}
:model/DashboardCard dashcard {:card_id card-id
:dashboard_id dashboard-id
:parameter_mappings [{:parameter_id "_name_"
:card_id card-id
:type "string/="
:target [:dimension [:template-tag "NAME"]]}]}]
(let [url (dashcard-url dashcard {:_embedding_params {:name "enabled"}})]
(is (= [[1]]
(mt/rows (mt/user-http-request :crowberto :get 202 url :name "Hudson Borer"))
(mt/rows (mt/user-http-request :crowberto :get 202 url :name "Hudson Borer" :name "x"))))))))))))
;;; ------------------------------------------------ Chain filtering -------------------------------------------------
(defn random-embedding-secret-key [] (u.random/secure-hex 32))
(def ^:dynamic *secret-key* nil)
(defn sign [claims] (jwt/sign claims *secret-key*))
(defn do-with-new-secret-key! [f]
(binding [*secret-key* (random-embedding-secret-key)]
(mt/with-temporary-setting-values [embedding-secret-key *secret-key*]
(f))))
(defmacro with-new-secret-key! {:style/indent 0} [& body]
`(do-with-new-secret-key! (fn [] ~@body)))
(defmacro with-embedding-enabled-and-new-secret-key! {:style/indent 0} [& body]
`(mt/with-temporary-setting-values [~'enable-embedding-static true]
(with-new-secret-key!
~@body)))
(defn dash-token
[dash-or-id & [additional-token-params]]
(sign (merge {:resource {:dashboard (u/the-id dash-or-id)}
:params {}}
additional-token-params)))
;;; ----------------------------- GET /api/preview_embed/card/:token/params/:param-key/values --------------------------
(deftest card-params-values-test
(testing "GET /api/preview_embed/card/:token/params/:param-key/values"
;; Card endpoint uses check-and-unsign which requires enable-embedding-static,
;; unlike dashboard endpoints which skip the check in preview mode
(embed-test/with-embedding-enabled-and-new-secret-key!
(mt/with-temp [:model/Card card {:dataset_query (mt/mbql-query venues)
:parameters [{:name "Static Category"
:slug "static_category"
:id "_STATIC_CATEGORY_"
:type "category"
:values_source_type "static-list"
:values_source_config {:values ["African" "American" "Asian"]}}]}]
(let [signed-token (embed-test/card-token card {:_embedding_params {:static_category "enabled"}})]
(testing "Should work if the param we're fetching values for is enabled"
(is (= {:values [["African"] ["American"] ["Asian"]]
:has_more_values false}
(mt/user-http-request :crowberto :get 200
(format "preview_embed/card/%s/params/%s/values"
signed-token "_STATIC_CATEGORY_"))))))))))
(deftest card-params-values-field-filter-test
(testing "GET /api/preview_embed/card/:token/params/:param-key/values with a field filter parameter"
(embed-test/with-embedding-enabled-and-new-secret-key!
(api.card-test/with-card-param-values-fixtures [{:keys [field-filter-card param-keys]}]
(let [embedding-params (zipmap (map (comp keyword :slug) (:parameters field-filter-card))
(repeat "enabled"))
signed-token (embed-test/card-token field-filter-card
{:_embedding_params embedding-params})
response (mt/user-http-request :crowberto :get 200
(format "preview_embed/card/%s/params/%s/values"
signed-token (:field-values param-keys)))]
(is (false? (:has_more_values response)))
(is (set/subset? #{["20th Century Cafe"] ["33 Taps"]}
(-> response :values set))))))))
(deftest card-params-values-remapped-fields-test
(testing "preview embed card values/remapping endpoints work for parameters mapped to remapped fields"
(embed-test/with-embedding-enabled-and-new-secret-key!
(mt/with-column-remappings [orders.quantity {5 "N5"}
orders.product_id products.title]
(mt/with-temp [:model/Card card
(let [mp (mt/metadata-provider)]
{:dataset_query
(-> (lib/native-query mp "SELECT * FROM ORDERS JOIN PEOPLE ON ORDERS.USER_ID = PEOPLE.ID WHERE {{quantity}} AND {{product_id_fk}} AND {{user_id_pk}}")
(lib/with-template-tags
{"quantity" {:id "quantity"
:name "quantity"
:display-name "Internal"
:type :dimension
:widget-type :number/=
:dimension (lib/ref (lib.metadata/field mp (mt/id :orders :quantity)))}
"product_id_fk" {:id "product_id_fk"
:name "product_id_fk"
:display-name "FK"
:type :dimension
:widget-type :id
:dimension (lib/ref (lib.metadata/field mp (mt/id :orders :product_id)))}
"user_id_pk" {:id "user_id_pk"
:name "user_id_pk"
:display-name "PK->Name"
:type :dimension
:widget-type :id
:dimension (lib/ref (lib.metadata/field mp (mt/id :people :id)))}}))
:parameters [{:id "quantity", :name "Internal", :slug "quantity", :type "number/="
:target ["dimension" ["template-tag" "quantity"]]}
{:id "product_id_fk", :name "FK", :slug "product_id_fk", :type "id"
:target ["dimension" ["template-tag" "product_id_fk"]]}
{:id "user_id_pk", :name "PK->Name", :slug "user_id_pk", :type "id"
:target ["dimension" ["template-tag" "user_id_pk"]]}]})]
(let [token (embed-test/card-token card {:_embedding_params {:quantity "enabled"
:product_id_fk "enabled"
:user_id_pk "enabled"}})]
(testing "values for internally-remapped param"
(is (map? (mt/user-http-request :crowberto :get 200
(format "preview_embed/card/%s/params/quantity/values" token)))))
(testing "values for FK-remapped param"
(is (map? (mt/user-http-request :crowberto :get 200
(format "preview_embed/card/%s/params/product_id_fk/values" token)))))
(testing "remapping for PK param"
(is (some? (mt/user-http-request :crowberto :get 200
(format "preview_embed/card/%s/params/user_id_pk/remapping" token)
:value "1"))))))))))
(deftest params-with-static-list-test
(testing "embedding with parameter that has source is a static list"
(with-embedding-enabled-and-new-secret-key!
(api.dashboard-test/with-chain-filter-fixtures [{:keys [dashboard]}]
(t2/update! :model/Dashboard (u/the-id dashboard) {:enable_embedding false ;; works without enabling embedding on the dashboard (#44962)
:embedding_params {"static_category" "enabled"
"static_category_label" "enabled"}})
(let [signed-token (dash-token dashboard)
url (format "preview_embed/dashboard/%s/params/%s/values" signed-token "_STATIC_CATEGORY_")]
(testing "Should work if the param we're fetching values for is enabled"
(testing "\nGET /api/preview_embed/dashboard/:token/params/:param-key/values"
(is (= {:values [["African"] ["American"] ["Asian"]]
:has_more_values false}
(mt/user-http-request :crowberto :get 200 url))))))))))
(deftest preview-locked-linked-chain-filter-values-test
(testing "GET /api/preview_embed/dashboard/:token/params/:key/values constrains a linked enabled param by a locked param (#41635)"
(with-embedding-enabled-and-new-secret-key!
(api.dashboard-test/with-chain-filter-fixtures [{:keys [dashboard]}]
(let [signed-token (dash-token dashboard {:_embedding_params {:category_id "enabled"
:category_name "enabled"
:price "locked"}
:params {:price 4}})
url (format "preview_embed/dashboard/%s/params/%s/values" signed-token "_CATEGORY_ID_")]
(is (= {:values [[40 "Japanese"] [67 "Steakhouse"]]
:has_more_values false}
(mt/user-http-request :crowberto :get 200 url))))))))
(deftest dashboard-params-search-test
(testing "GET /api/preview_embed/dashboard/:token/params/:param-key/search/:prefix"
(with-embedding-enabled-and-new-secret-key!
(api.dashboard-test/with-chain-filter-fixtures [{:keys [dashboard]}]
(t2/update! :model/Dashboard (u/the-id dashboard) {:enable_embedding false ;; works without enabling embedding on the dashboard (#44962)
:embedding_params {"static_category_label" "enabled"}})
(let [signed-token (dash-token dashboard)
search-url (format "preview_embed/dashboard/%s/params/%s/search/%s" signed-token "_STATIC_CATEGORY_LABEL_" "AF")]
(testing "Should work if the param we're fetching values for is enabled"
(is (= {:values [["African" "Af"]]
:has_more_values false}
(mt/user-http-request :crowberto :get 200 search-url)))))))))
(deftest boolean-parameter-values-test
(testing "embedding endpoint supports boolean parameter values (#27643)"
(embed-test/with-embedding-enabled-and-new-secret-key!
(mt/dataset places-cam-likes
(mt/with-temp [:model/Card {card-id :id :as card} {:dataset_query
{:database (mt/id)
:type :native
:native
{:template-tags
{"LIKED"
{:widget-type :string/=
:default [true]
:name "LIKED"
:type :dimension
:id "LIKED"
:dimension [:field (mt/id :places :liked) nil]
:display-name "Liked"
:options nil
:required true}}
:query "SELECT * FROM PLACES WHERE {{LIKED}}"}}}
:model/Dashboard {dashboard-id :id} {:parameters
[{:name "LIKED"
:slug "LIKED"
:id "ccb91bc"
:type :string/=
:sectionId "string"
:required true
:default [true]}]}
:model/DashboardCard dashcard {:dashboard_id dashboard-id
:parameter_mappings
[{:parameter_id "ccb91bc"
:card_id card-id
:target [:dimension [:template-tag "LIKED"]]}]
:card_id card-id}]
(testing "for card embeds"
(let [url (card-query-url card {:_embedding_params {:LIKED "enabled"}})]
(is (= [[3 "The Dentist" false]]
(-> (mt/user-http-request :crowberto :get 202 url
:parameters (json/encode {:LIKED false}))
:data
:rows)))
(is (= [[1 "Tempest" true]
[2 "Bullit" true]]
(-> (mt/user-http-request :crowberto :get 202 url
:parameters (json/encode {:LIKED true}))
:data
:rows)))))
(testing "for dashboard embeds"
(let [url (dashcard-url dashcard {:_embedding_params {:LIKED "enabled"}})]
(is (= [[3 "The Dentist" false]]
(-> (mt/user-http-request :crowberto :get 202 url
:parameters (json/encode {:LIKED false}))
:data
:rows)))
(is (= [[1 "Tempest" true]
[2 "Bullit" true]]
(-> (mt/user-http-request :crowberto :get 202 url
:parameters (json/encode {:LIKED true}))
:data
:rows))))))))))
(deftest string-parameter-values-test
(testing "embedding endpoint should not parse string values into numbers (#46240)"
(embed-test/with-embedding-enabled-and-new-secret-key!
(mt/dataset airports
(mt/with-temp [:model/Card {card-id :id} {:dataset_query
{:database (mt/id)
:type :query
:query
{:source-table (mt/id :airport)
:fields [[:field (mt/id :airport :name) nil]]}}}
:model/Dashboard {dashboard-id :id} {:parameters
[{:name "NAME"
:slug "NAME"
:id "ccb91bc"
:type :string/contains
:sectionId "number"}]}
:model/DashboardCard dashcard {:dashboard_id dashboard-id
:parameter_mappings
[{:parameter_id "ccb91bc"
:card_id card-id
:target [:dimension [:field (mt/id :airport :name) nil]]}]
:card_id card-id}]
(testing "for dashboard embeds"
(let [url (dashcard-url dashcard {:_embedding_params {:NAME "enabled"}})]
(is (= [["Cheongju International Airport/Cheongju Air Base (K-59/G-513)"]]
(-> (mt/user-http-request :crowberto :get 202 url
:parameters (json/encode {:NAME "513"}))
:data
:rows))))))))))
;;; ------------------------------------------ Tile endpoints ---------------------------------------------------------
(defn- png? [s]
(= [\P \N \G] (drop 1 (take 4 s))))
(defn- venues-query
[]
{:database (mt/id)
:type :query
:query {:source-table (mt/id :people)
:fields [[:field (mt/id :people :id) nil]
[:field (mt/id :people :state) nil]
[:field (mt/id :people :latitude) nil]
[:field (mt/id :people :longitude) nil]]}})
(deftest card-tile-query-test
(testing "GET api/preview_embed/tiles/card/:uuid/:zoom/:x/:y"
(embed-test/with-embedding-enabled-and-new-secret-key!
(mt/with-temp [:model/Card {card-id :id} {:dataset_query (venues-query)
:enable_embedding true}]
(let [token (embed-test/card-token card-id)]
(is (png? (mt/user-http-request
:crowberto :get 200 (format "preview_embed/tiles/card/%s/1/1/1"
token)
:latField (tiles.api-test/encoded-lat-field-ref)
:lonField (tiles.api-test/encoded-lon-field-ref)))))))))
(deftest dashcard-tile-query-test
(testing "GET api/preview_embed/tiles/dashboard/:uuid/dashcard/:dashcard-id/card/:card-id/:zoom/:x/:y"
(embed-test/with-embedding-enabled-and-new-secret-key!
(mt/with-temp [:model/Dashboard {dashboard-id :id} {:enable_embedding true}
:model/Card {card-id :id} {:dataset_query (venues-query)}
:model/DashboardCard {dashcard-id :id} {:card_id card-id
:dashboard_id dashboard-id}]
(let [token (embed-test/dash-token dashboard-id)]
(is (png? (mt/user-http-request
:crowberto :get 200 (format "preview_embed/tiles/dashboard/%s/dashcard/%d/card/%d/1/1/1"
token
dashcard-id
card-id)
:latField (tiles.api-test/encoded-lat-field-ref)
:lonField (tiles.api-test/encoded-lon-field-ref)))))))))
(deftest card-tile-query-implicit-join-ref-test
(testing "GET api/preview_embed/tiles/card/:uuid/:zoom/:x/:y returns a 400 when the lat/lon refs use an implicit join"
(embed-test/with-embedding-enabled-and-new-secret-key!
(mt/with-temp [:model/Card {card-id :id} {:dataset_query (tiles.api-test/implicit-join-query)
:enable_embedding true}]
(let [token (embed-test/card-token card-id)]
(is (= "Fields referenced via implicit joins are not supported."
(mt/user-http-request
:crowberto :get 400 (format "preview_embed/tiles/card/%s/1/1/1" token)
:latField (tiles.api-test/encoded-implicit-join-field-ref :latitude)
:lonField (tiles.api-test/encoded-implicit-join-field-ref :longitude)))))))))
(deftest dashcard-tile-query-implicit-join-ref-test
(testing "GET api/preview_embed/tiles/dashboard/:uuid/dashcard/:dashcard-id/card/:card-id/:zoom/:x/:y returns a 400 when the lat/lon refs use an implicit join"
(embed-test/with-embedding-enabled-and-new-secret-key!
(mt/with-temp [:model/Dashboard {dashboard-id :id} {:enable_embedding true}
:model/Card {card-id :id} {:dataset_query (tiles.api-test/implicit-join-query)}
:model/DashboardCard {dashcard-id :id} {:card_id card-id
:dashboard_id dashboard-id}]
(let [token (embed-test/dash-token dashboard-id)]
(is (= "Fields referenced via implicit joins are not supported."
(mt/user-http-request
:crowberto :get 400 (format "preview_embed/tiles/dashboard/%s/dashcard/%d/card/%d/1/1/1"
token
dashcard-id
card-id)
:latField (tiles.api-test/encoded-implicit-join-field-ref :latitude)
:lonField (tiles.api-test/encoded-implicit-join-field-ref :longitude)))))))))