diff --git a/Dockerfile b/Dockerfile index 854c3b0eda8a7ad060c42301d51b5711fddedbd8..1793f9eac95f897631a0416e346d5fa833eddaaf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,8 +16,9 @@ ENV LC_CTYPE en_US.UTF-8 # yarn: frontend building # make: backend building # gettext: translations +# java-cacerts: installs updated cacerts to /etc/ssl/certs/java/cacerts -RUN apk add --update coreutils bash yarn git wget make gettext +RUN apk add --update coreutils bash yarn git wget make gettext java-cacerts # lein: backend dependencies and building ADD https://raw.github.com/technomancy/leiningen/stable/bin/lein /usr/local/bin/lein @@ -40,9 +41,6 @@ ADD . . # build the app RUN bin/build -# install updated cacerts to /etc/ssl/certs/java/cacerts -RUN apk add --update java-cacerts - # import AWS RDS cert into /etc/ssl/certs/java/cacerts ADD https://s3.amazonaws.com/rds-downloads/rds-combined-ca-bundle.pem . RUN keytool -noprompt -import -trustcacerts -alias aws-rds \ diff --git a/bin/docker/Dockerfile b/bin/docker/Dockerfile index e212a778ec9a6024fb07797a192ed0e221b9e6ac..bdc1cb0a9fe1cabf327f0d0212f1b936e0191752 100644 --- a/bin/docker/Dockerfile +++ b/bin/docker/Dockerfile @@ -4,7 +4,7 @@ ENV FC_LANG en-US ENV LC_CTYPE en_US.UTF-8 # dependencies -RUN apk add --no-cache bash ttf-dejavu fontconfig +RUN apk add --update --no-cache bash ttf-dejavu fontconfig # add Metabase jar COPY ./metabase.jar /app/ diff --git a/src/metabase/api/pulse.clj b/src/metabase/api/pulse.clj index 26a4d0d1c82e549af7039d8ca69dec237d899b32..cb353128bd54fb006a3301564aac044903fe55c7 100644 --- a/src/metabase/api/pulse.clj +++ b/src/metabase/api/pulse.clj @@ -91,7 +91,6 @@ (let [pulse-before-update (api/write-check Pulse id)] (check-card-read-permissions cards) (collection/check-allowed-to-change-collection pulse-before-update pulse-updates) - (db/transaction ;; If the collection or position changed with this update, we might need to fixup the old and/or new collection, ;; depending on what changed. @@ -100,7 +99,7 @@ (pulse/update-pulse! (assoc (select-keys pulse-updates [:name :cards :channels :skip_if_empty :collection_id :collection_position :archived]) - :id id)))) + :id id)))) ;; return updated Pulse (pulse/retrieve-pulse id)) diff --git a/src/metabase/middleware/exceptions.clj b/src/metabase/middleware/exceptions.clj index 06be9b9031558760681501085b691b3afdb030b6..d36e8522ffb8a1e75b948b1342c4a84eb593940c 100644 --- a/src/metabase/middleware/exceptions.clj +++ b/src/metabase/middleware/exceptions.clj @@ -4,7 +4,6 @@ [clojure.string :as str] [clojure.tools.logging :as log] [metabase.middleware.security :as mw.security] - [metabase.util :as u] [metabase.util.i18n :as ui18n :refer [trs]]) (:import java.sql.SQLException org.eclipse.jetty.io.EofException)) @@ -56,13 +55,13 @@ status-code (str message) - ;; Otherwise it's a 500. Return a body that includes exception & filtered - ;; stacktrace for debugging purposes + ;; Otherwise it's a 500. Return the full Exception for debugging purposes :else - (assoc other-info - :message message - :type (class e) - :stacktrace (u/filtered-stacktrace e)))] + (merge + other-info + (Throwable->map e) + {:message message + :type (class e)}))] {:status (or status-code 500) :headers (mw.security/security-headers) diff --git a/src/metabase/pulse/render/png.clj b/src/metabase/pulse/render/png.clj index 1d1da38a5b07b0810ff9f35e309c725155dbb8c9..df904d3383105db294e5489e9dde81fe92b779c5 100644 --- a/src/metabase/pulse/render/png.clj +++ b/src/metabase/pulse/render/png.clj @@ -12,6 +12,7 @@ [metabase.pulse.render [common :as common] [style :as style]] + [metabase.util.i18n :refer [trs]] [schema.core :as s]) (:import cz.vutbr.web.css.MediaSpec java.awt.Dimension @@ -24,29 +25,33 @@ org.fit.cssbox.layout.BrowserCanvas org.w3c.dom.Document)) +(defn- register-font! [filename] + (with-open [is (io/input-stream (io/resource filename))] + (.registerFont (java.awt.GraphicsEnvironment/getLocalGraphicsEnvironment) + (java.awt.Font/createFont java.awt.Font/TRUETYPE_FONT is)))) -(defonce - ^{:doc "Makes custom fonts available to Java so that CSSBox can render them" - :private true} - register-fonts - (delay (doseq [weight ["regular" "700" "900"]] - (.registerFont (java.awt.GraphicsEnvironment/getLocalGraphicsEnvironment) - (java.awt.Font/createFont - java.awt.Font/TRUETYPE_FONT - (-> (format "frontend_client/app/fonts/lato-v16-latin/lato-v16-latin-%s.ttf" weight) - io/resource - io/input-stream)))))) +(defn- register-fonts! [] + (try + (doseq [weight ["regular" "700" "900"]] + (register-font! (format "frontend_client/app/fonts/lato-v16-latin/lato-v16-latin-%s.ttf" weight))) + (catch Throwable e + (let [message (str (trs "Error registering fonts: Metabase will not be able to send Pulses.") + " " + (trs "This is a known issue with certain JVMs. See {0} and for more details." + "https://github.com/metabase/metabase/issues/7986"))] + (log/error e message) + (throw (ex-info message {} e)))))) -(when-not *compile-files* - @register-fonts) +(defonce ^{:doc "Makes custom fonts available to Java so that CSSBox can render them." + :private true + :arglists '([])} register-fonts-if-needed! + (let [register!* (delay (register-fonts!))] + (fn [] + @register!*))) (defn- write-image! [^BufferedImage image, ^String format-name, ^ByteArrayOutputStream output-stream] - (try - (ImageIO/write image format-name output-stream) - (catch javax.imageio.IIOException iioex - (log/error iioex "Error writing image to output stream") - (throw iioex)))) + (ImageIO/write image format-name output-stream)) (defn- dom-analyzer ^DOMAnalyzer [^Document doc, ^StreamDocumentSource doc-source, ^Dimension window-size] @@ -76,6 +81,7 @@ (defn- render-to-png! [^String html, ^ByteArrayOutputStream os, width] + (register-fonts-if-needed!) (with-open [is (ByteArrayInputStream. (.getBytes html StandardCharsets/UTF_8))] (let [doc-source (StreamDocumentSource. is nil "text/html; charset=utf-8") doc (.parse (DefaultDOMSource. doc-source)) @@ -86,11 +92,15 @@ "Render the Hiccup HTML `content` of a Pulse to a PNG image, returning a byte array." [{:keys [content]} :- common/RenderedPulseCard width] - (let [html (html [:html [:body {:style (style/style - {:margin 0 - :padding 0 - :background-color :white})} - content]])] - (with-open [os (ByteArrayOutputStream.)] - (render-to-png! html os width) - (.toByteArray os)))) + (try + (let [html (html [:html [:body {:style (style/style + {:margin 0 + :padding 0 + :background-color :white})} + content]])] + (with-open [os (ByteArrayOutputStream.)] + (render-to-png! html os width) + (.toByteArray os))) + (catch Throwable e + (log/error e (trs "Error rendering Pulse")) + (throw e)))) diff --git a/test/metabase/api/card_test.clj b/test/metabase/api/card_test.clj index 07632c77311fc42d6428946b48549a9777fea47f..eb10cd34e9de93328d23a98d45472acf47303c98 100644 --- a/test/metabase/api/card_test.clj +++ b/test/metabase/api/card_test.clj @@ -25,7 +25,6 @@ [metabase.test [data :as data] [util :as tu]] - [metabase.test.data.users :as test-users] [toucan.db :as db] [toucan.util.test :as tt]) (:import java.io.ByteArrayInputStream @@ -143,7 +142,7 @@ ;;; +----------------------------------------------------------------------------------------------------------------+ (defn- card-returned? [model object-or-id card-or-id] - (contains? (set (for [card ((test-users/user->client :rasta) :get 200 "card", :f model, :model_id (u/get-id object-or-id))] + (contains? (set (for [card ((mt/user->client :rasta) :get 200 "card", :f model, :model_id (u/get-id object-or-id))] (u/get-id card))) (u/get-id card-or-id))) @@ -166,7 +165,7 @@ (deftest model-id-requied-when-f-is-database-test (is (= {:errors {:model_id "model_id is a required parameter when filter mode is 'database'"}} - ((test-users/user->client :crowberto) :get 400 "card" :f :database)))) + ((mt/user->client :crowberto) :get 400 "card" :f :database)))) (deftest filter-cards-by-table-test (testing "Filter cards by table" @@ -186,7 +185,7 @@ ;; Make sure `model_id` is required when `f` is :table (deftest model_id-requied-when-f-is-table (is (= {:errors {:model_id "model_id is a required parameter when filter mode is 'table'"}} - ((test-users/user->client :crowberto) :get 400 "card", :f :table)))) + ((mt/user->client :crowberto) :get 400 "card", :f :table)))) (deftest filter-by-recent-test (testing "Filter by `recent`" @@ -196,21 +195,21 @@ Card [card-4 {:name "Card 4"}] ;; 3 was viewed most recently, followed by 4, then 1. Card 2 was viewed by a different user so ;; shouldn't be returned - ViewLog [_ {:model "card", :model_id (u/get-id card-1), :user_id (test-users/user->id :rasta) + ViewLog [_ {:model "card", :model_id (u/get-id card-1), :user_id (mt/user->id :rasta) :timestamp #t "2015-12-01"}] - ViewLog [_ {:model "card", :model_id (u/get-id card-2), :user_id (test-users/user->id :trashbird) + ViewLog [_ {:model "card", :model_id (u/get-id card-2), :user_id (mt/user->id :trashbird) :timestamp #t "2016-01-01"}] - ViewLog [_ {:model "card", :model_id (u/get-id card-3), :user_id (test-users/user->id :rasta) + ViewLog [_ {:model "card", :model_id (u/get-id card-3), :user_id (mt/user->id :rasta) :timestamp #t "2016-02-01"}] - ViewLog [_ {:model "card", :model_id (u/get-id card-4), :user_id (test-users/user->id :rasta) + ViewLog [_ {:model "card", :model_id (u/get-id card-4), :user_id (mt/user->id :rasta) :timestamp #t "2016-03-01"}] - ViewLog [_ {:model "card", :model_id (u/get-id card-3), :user_id (test-users/user->id :rasta) + ViewLog [_ {:model "card", :model_id (u/get-id card-3), :user_id (mt/user->id :rasta) :timestamp #t "2016-04-01"}]] (with-cards-in-readable-collection [card-1 card-2 card-3 card-4] (is (= ["Card 3" "Card 4" "Card 1"] - (map :name ((test-users/user->client :rasta) :get 200 "card", :f :recent))) + (map :name ((mt/user->client :rasta) :get 200 "card", :f :recent))) "Should return cards that were recently viewed by current user only"))))) (deftest filter-by-popular-test @@ -219,15 +218,15 @@ Card [card-2 {:name "Card 2"}] Card [card-3 {:name "Card 3"}] ;; 3 entries for card 3, 2 for card 2, none for card 1, - ViewLog [_ {:model "card", :model_id (u/get-id card-3), :user_id (test-users/user->id :rasta)}] - ViewLog [_ {:model "card", :model_id (u/get-id card-2), :user_id (test-users/user->id :trashbird)}] - ViewLog [_ {:model "card", :model_id (u/get-id card-2), :user_id (test-users/user->id :rasta)}] - ViewLog [_ {:model "card", :model_id (u/get-id card-3), :user_id (test-users/user->id :crowberto)}] - ViewLog [_ {:model "card", :model_id (u/get-id card-3), :user_id (test-users/user->id :rasta)}]] + ViewLog [_ {:model "card", :model_id (u/get-id card-3), :user_id (mt/user->id :rasta)}] + ViewLog [_ {:model "card", :model_id (u/get-id card-2), :user_id (mt/user->id :trashbird)}] + ViewLog [_ {:model "card", :model_id (u/get-id card-2), :user_id (mt/user->id :rasta)}] + ViewLog [_ {:model "card", :model_id (u/get-id card-3), :user_id (mt/user->id :crowberto)}] + ViewLog [_ {:model "card", :model_id (u/get-id card-3), :user_id (mt/user->id :rasta)}]] (with-cards-in-readable-collection [card-1 card-2 card-3] (is (= ["Card 3" "Card 2"] - (map :name ((test-users/user->client :rasta) :get 200 "card", :f :popular))) + (map :name ((mt/user->client :rasta) :get 200 "card", :f :popular))) (str "`f=popular` should return cards sorted by number of ViewLog entries for all users; cards with no " "entries should be excluded")))))) @@ -238,7 +237,7 @@ Card [card-3 {:name "Card 3", :archived true}]] (with-cards-in-readable-collection [card-1 card-2 card-3] (is (= #{"Card 2" "Card 3"} - (set (map :name ((test-users/user->client :rasta) :get 200 "card", :f :archived)))) + (set (map :name ((mt/user->client :rasta) :get 200 "card", :f :archived)))) "The set of Card returned with f=archived should be equal to the set of archived cards"))))) (deftest filter-by-fav-test @@ -246,11 +245,11 @@ (tt/with-temp* [Card [card-1 {:name "Card 1"}] Card [card-2 {:name "Card 2"}] Card [card-3 {:name "Card 3"}] - CardFavorite [_ {:card_id (u/get-id card-1), :owner_id (test-users/user->id :rasta)}] - CardFavorite [_ {:card_id (u/get-id card-2), :owner_id (test-users/user->id :crowberto)}]] + CardFavorite [_ {:card_id (u/get-id card-1), :owner_id (mt/user->id :rasta)}] + CardFavorite [_ {:card_id (u/get-id card-2), :owner_id (mt/user->id :crowberto)}]] (with-cards-in-readable-collection [card-1 card-2 card-3] (is (= [{:name "Card 1", :favorite true}] - (for [card ((test-users/user->client :rasta) :get 200 "card", :f :fav)] + (for [card ((mt/user->client :rasta) :get 200 "card", :f :fav)] (select-keys card [:name :favorite])))))))) @@ -261,34 +260,34 @@ ;; Test that we can make a card (deftest create-a-card (let [card-name (tu/random-name)] - (is (= (merge - card-defaults - {:name card-name - :collection_id true - :collection true - :creator_id (test-users/user->id :rasta) - :dataset_query true - :query_type "query" - :visualization_settings {:global {:title nil}} - :database_id true - :table_id true - :can_write true - :dashboard_count 0 - :read_permissions nil - :result_metadata true - :creator (merge - (select-keys (test-users/fetch-user :rasta) [:id :date_joined :last_login]) - {:common_name "Rasta Toucan" - :is_superuser false - :is_qbnewb true - :last_name "Toucan" - :first_name "Rasta" - :email "rasta@metabase.com"})}) - (tu/with-non-admin-groups-no-root-collection-perms - (tt/with-temp* [Collection [collection]] - (tu/with-model-cleanup [Card] - (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - (-> ((test-users/user->client :rasta) :post 202 "card" + (tu/with-non-admin-groups-no-root-collection-perms + (tt/with-temp* [Collection [collection]] + (tu/with-model-cleanup [Card] + (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) + (is (= (merge + card-defaults + {:name card-name + :collection_id true + :collection true + :creator_id (mt/user->id :rasta) + :dataset_query true + :query_type "query" + :visualization_settings {:global {:title nil}} + :database_id true + :table_id true + :can_write true + :dashboard_count 0 + :read_permissions nil + :result_metadata true + :creator (merge + (select-keys (mt/fetch-user :rasta) [:id :date_joined :last_login]) + {:common_name "Rasta Toucan" + :is_superuser false + :is_qbnewb true + :last_name "Toucan" + :first_name "Rasta" + :email "rasta@metabase.com"})}) + (-> ((mt/user->client :rasta) :post 202 "card" (assoc (card-with-name-and-query card-name (mbql-count-query (data/id) (data/id :venues))) :collection_id (u/get-id collection))) (dissoc :created_at :updated_at :id) @@ -301,33 +300,33 @@ ;; Make sure when saving a Card the query metadata is saved (if correct) (deftest saving-card-saves-query-metadata - (is (= [{:base_type "type/Integer" - :display_name "Count Chocula" - :name "count_chocula" - :special_type "type/Number"}] - (tu/with-non-admin-groups-no-root-collection-perms - (let [metadata [{:base_type :type/Integer - :display_name "Count Chocula" - :name "count_chocula" - :special_type :type/Number}] - card-name (tu/random-name)] - (tt/with-temp Collection [collection] - (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - (tu/with-model-cleanup [Card] - ;; create a card with the metadata - ((test-users/user->client :rasta) :post 202 "card" - (assoc (card-with-name-and-query card-name) - :collection_id (u/get-id collection) - :result_metadata metadata - :metadata_checksum (#'results-metadata/metadata-checksum metadata))) - ;; now check the metadata that was saved in the DB + (tu/with-non-admin-groups-no-root-collection-perms + (let [metadata [{:base_type :type/Integer + :display_name "Count Chocula" + :name "count_chocula" + :special_type :type/Number}] + card-name (tu/random-name)] + (tt/with-temp Collection [collection] + (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) + (tu/with-model-cleanup [Card] + ;; create a card with the metadata + ((mt/user->client :rasta) :post 202 "card" + (assoc (card-with-name-and-query card-name) + :collection_id (u/get-id collection) + :result_metadata metadata + :metadata_checksum (#'results-metadata/metadata-checksum metadata))) + ;; now check the metadata that was saved in the DB + (is (= [{:base_type "type/Integer" + :display_name "Count Chocula" + :name "count_chocula" + :special_type "type/Number"}] (db/select-one-field :result_metadata Card :name card-name)))))))) ;; we should be able to save a Card if the `result_metadata` is *empty* (but not nil) (#9286) (deftest save-card-with-empty-result-metadata (is (tu/with-model-cleanup [Card] ;; create a card with the metadata - ((test-users/user->client :rasta) :post 202 "card" + ((mt/user->client :rasta) :post 202 "card" (assoc (card-with-name-and-query) :result_metadata [] :metadata_checksum (#'results-metadata/metadata-checksum [])))))) @@ -363,7 +362,7 @@ (tu/throw-if-called qp.async/result-metadata-for-query-async (tu/with-model-cleanup [Card] ;; create a card with the metadata - ((test-users/user->client :rasta) :post 202 "card" + ((mt/user->client :rasta) :post 202 "card" (assoc (card-with-name-and-query card-name) :collection_id (u/get-id collection) :result_metadata (map fingerprint-integers->doubles metadata) @@ -390,7 +389,7 @@ (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) (tu/with-model-cleanup [Card] ;; create a card with the metadata - ((test-users/user->client :rasta) :post 202 "card" + ((mt/user->client :rasta) :post 202 "card" (assoc (card-with-name-and-query card-name) :collection_id (u/get-id collection) :result_metadata metadata @@ -418,7 +417,7 @@ (reset! sql-result sql) (orig driver conn sql params))] ;; create a card with the metadata - ((test-users/user->client :rasta) :post 202 "card" + ((mt/user->client :rasta) :post 202 "card" (assoc (card-with-name-and-query card-name) :collection_id (u/get-id collection) :result_metadata metadata @@ -436,7 +435,7 @@ (is (= true (boolean (when-let [s @sql-result] - (re-find (re-pattern (str "userID: " (test-users/user->id :rasta))) + (re-find (re-pattern (str "userID: " (mt/user->id :rasta))) s))))))))))))) ;; Make sure we can create a Card with a Collection position @@ -447,7 +446,7 @@ (let [card-name (tu/random-name)] (tt/with-temp Collection [collection] (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - ((test-users/user->client :rasta) :post 202 "card" (assoc (card-with-name-and-query card-name) + ((mt/user->client :rasta) :post 202 "card" (assoc (card-with-name-and-query card-name) :collection_id (u/get-id collection), :collection_position 1)) (some-> (db/select-one [Card :collection_id :collection_position] :name card-name) (update :collection_id (partial = (u/get-id collection))))))))))) @@ -458,7 +457,7 @@ (tu/with-model-cleanup [Card] (let [card-name (tu/random-name)] (tt/with-temp Collection [collection] - ((test-users/user->client :rasta) :post 403 "card" (assoc (card-with-name-and-query card-name) + ((mt/user->client :rasta) :post 403 "card" (assoc (card-with-name-and-query card-name) :collection_id (u/get-id collection) :collection_position 1)) (some-> (db/select-one [Card :collection_id :collection_position] :name card-name) @@ -483,9 +482,9 @@ card-defaults (select-keys card [:id :name :created_at :updated_at]) {:dashboard_count 0 - :creator_id (test-users/user->id :rasta) + :creator_id (mt/user->id :rasta) :creator (merge - (select-keys (test-users/fetch-user :rasta) [:id :date_joined :last_login]) + (select-keys (mt/fetch-user :rasta) [:id :date_joined :last_login]) {:common_name "Rasta Toucan" :is_superuser false :is_qbnewb true @@ -502,7 +501,7 @@ :table_id (u/get-id table) :collection_id (u/get-id collection) :collection (into {} collection)}) - ((test-users/user->client :rasta) :get 200 (str "card/" (u/get-id card)))))))) + ((mt/user->client :rasta) :get 200 (str "card/" (u/get-id card)))))))) (deftest check-that-a-user-without-permissions-isn-t-allowed-to-fetch-the-card (is (= "You don't have permissions to do that." @@ -513,7 +512,7 @@ ;; revoke permissions for default group to this database (perms/revoke-permissions! (perms-group/all-users) (u/get-id db)) ;; now a non-admin user shouldn't be able to fetch this card - ((test-users/user->client :rasta) :get 403 (str "card/" (u/get-id card)))))))) + ((mt/user->client :rasta) :get 403 (str "card/" (u/get-id card)))))))) ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | UPDATING A CARD | @@ -522,7 +521,7 @@ (deftest updating-a-card-that-doesnt-exist-should-give-a-404 (is (= "Not found." - ((test-users/user->client :crowberto) :put 404 "card/12345")))) + ((mt/user->client :crowberto) :put 404 "card/12345")))) (deftest test-that-we-can-edit-a-card @@ -530,7 +529,7 @@ (with-cards-in-writeable-collection card (is (= "Original Name" (db/select-one-field :name Card, :id (u/get-id card)))) - ((test-users/user->client :rasta) :put 202 (str "card/" (u/get-id card)) {:name "Updated Name"}) + ((mt/user->client :rasta) :put 202 (str "card/" (u/get-id card)) {:name "Updated Name"}) (is (= "Updated Name" (db/select-one-field :name Card, :id (u/get-id card))))))) @@ -539,7 +538,7 @@ (with-cards-in-writeable-collection card (let [archived? (fn [] (:archived (Card (u/get-id card)))) set-archived! (fn [archived] - ((test-users/user->client :rasta) :put 202 (str "card/" (u/get-id card)) {:archived archived}) + ((mt/user->client :rasta) :put 202 (str "card/" (u/get-id card)) {:archived archived}) (archived?))] (is (= false (archived?))) @@ -554,119 +553,119 @@ (tt/with-temp* [Collection [collection] Card [card {:collection_id (u/get-id collection)}]] (perms/grant-collection-read-permissions! (perms-group/all-users) collection) - ((test-users/user->client :rasta) :put 403 (str "card/" (u/get-id card)) {:archived true})))))) + ((mt/user->client :rasta) :put 403 (str "card/" (u/get-id card)) {:archived true})))))) ;; Can we clear the description of a Card? (#4738) (deftest can-we-clear-the-description-of-a-card----4738- (is (nil? (tt/with-temp Card [card {:description "What a nice Card"}] (with-cards-in-writeable-collection card - ((test-users/user->client :rasta) :put 202 (str "card/" (u/get-id card)) {:description nil}) + ((mt/user->client :rasta) :put 202 (str "card/" (u/get-id card)) {:description nil}) (db/select-one-field :description Card :id (u/get-id card))))))) (deftest description-should-be-blankable-as-well (is (= "" (tt/with-temp Card [card {:description "What a nice Card"}] (with-cards-in-writeable-collection card - ((test-users/user->client :rasta) :put 202 (str "card/" (u/get-id card)) {:description ""}) + ((mt/user->client :rasta) :put 202 (str "card/" (u/get-id card)) {:description ""}) (db/select-one-field :description Card :id (u/get-id card))))))) (deftest can-we-update-a-card-s-embedding-params- (is (= {:abc "enabled"} (tt/with-temp Card [card] (tu/with-temporary-setting-values [enable-embedding true] - ((test-users/user->client :crowberto) :put 202 (str "card/" (u/get-id card)) {:embedding_params {:abc "enabled"}})) + ((mt/user->client :crowberto) :put 202 (str "card/" (u/get-id card)) {:embedding_params {:abc "enabled"}})) (db/select-one-field :embedding_params Card :id (u/get-id card)))))) (deftest we-shouldn-t-be-able-to-update-them-if-we-re-not-an-admin--- (is (= "You don't have permissions to do that." (tt/with-temp Card [card] (tu/with-temporary-setting-values [enable-embedding true] - ((test-users/user->client :rasta) :put 403 (str "card/" (u/get-id card)) {:embedding_params {:abc "enabled"}})))))) + ((mt/user->client :rasta) :put 403 (str "card/" (u/get-id card)) {:embedding_params {:abc "enabled"}})))))) (deftest ---or-if-embedding-isn-t-enabled (is (= "Embedding is not enabled." (tt/with-temp Card [card] (tu/with-temporary-setting-values [enable-embedding false] - ((test-users/user->client :crowberto) :put 400 (str "card/" (u/get-id card)) {:embedding_params {:abc "enabled"}})))))) + ((mt/user->client :crowberto) :put 400 (str "card/" (u/get-id card)) {:embedding_params {:abc "enabled"}})))))) (deftest make-sure-when-updating-a-card-the-query-metadata-is-saved--if-correct- - (is (= [{:base_type "type/Integer" - :display_name "Count Chocula" - :name "count_chocula" - :special_type "type/Number"}] - (let [metadata [{:base_type :type/Integer - :display_name "Count Chocula" - :name "count_chocula" - :special_type :type/Number}]] - (tt/with-temp Card [card] - (with-cards-in-writeable-collection card - ;; update the Card's query - ((test-users/user->client :rasta) :put 202 (str "card/" (u/get-id card)) - {:dataset_query (mbql-count-query) - :result_metadata metadata - :metadata_checksum (#'results-metadata/metadata-checksum metadata)}) - ;; now check the metadata that was saved in the DB + (let [metadata [{:base_type :type/Integer + :display_name "Count Chocula" + :name "count_chocula" + :special_type :type/Number}]] + (tt/with-temp Card [card] + (with-cards-in-writeable-collection card + ;; update the Card's query + ((mt/user->client :rasta) :put 202 (str "card/" (u/get-id card)) + {:dataset_query (mbql-count-query) + :result_metadata metadata + :metadata_checksum (#'results-metadata/metadata-checksum metadata)}) + ;; now check the metadata that was saved in the DB + (is (= [{:base_type "type/Integer" + :display_name "Count Chocula" + :name "count_chocula" + :special_type "type/Number"}] (db/select-one-field :result_metadata Card :id (u/get-id card)))))))) (deftest make-sure-when-updating-a-card-the-correct-query-metadata-is-fetched--if-incorrect- - (is (= [{:base_type "type/BigInteger" - :display_name "Count" - :name "count" - :special_type "type/Quantity" - :fingerprint {:global {:distinct-count 1 - :nil% 0.0}, - :type {:type/Number {:min 100.0, :max 100.0, :avg 100.0, :q1 100.0, :q3 100.0 :sd nil}}}}] - (let [metadata [{:base_type :type/BigInteger - :display_name "Count Chocula" - :name "count_chocula" - :special_type :type/Quantity}]] - (tt/with-temp Card [card] - (with-cards-in-writeable-collection card - ;; update the Card's query - ((test-users/user->client :rasta) :put 202 (str "card/" (u/get-id card)) - {:dataset_query (mbql-count-query) - :result_metadata metadata - :metadata_checksum "ABC123"}) ; invalid checksum - ;; now check the metadata that was saved in the DB + (let [metadata [{:base_type :type/BigInteger + :display_name "Count Chocula" + :name "count_chocula" + :special_type :type/Quantity}]] + (tt/with-temp Card [card] + (with-cards-in-writeable-collection card + ;; update the Card's query + ((mt/user->client :rasta) :put 202 (str "card/" (u/get-id card)) + {:dataset_query (mbql-count-query) + :result_metadata metadata + :metadata_checksum "ABC123"}) ; invalid checksum + ;; now check the metadata that was saved in the DB + (is (= [{:base_type "type/BigInteger" + :display_name "Count" + :name "count" + :special_type "type/Quantity" + :fingerprint {:global {:distinct-count 1 + :nil% 0.0}, + :type {:type/Number {:min 100.0, :max 100.0, :avg 100.0, :q1 100.0, :q3 100.0 :sd nil}}}}] (db/select-one-field :result_metadata Card :id (u/get-id card)))))))) (deftest can-we-change-the-collection-position-of-a-card- - (is (= 1 - (tt/with-temp Card [card] - (with-cards-in-writeable-collection card - ((test-users/user->client :rasta) :put 202 (str "card/" (u/get-id card)) - {:collection_position 1}) + (tt/with-temp Card [card] + (with-cards-in-writeable-collection card + ((mt/user->client :rasta) :put 202 (str "card/" (u/get-id card)) + {:collection_position 1}) + (is (= 1 (db/select-one-field :collection_position Card :id (u/get-id card))))))) (deftest ---and-unset--unpin--it-as-well- - (is (nil? (tt/with-temp Card [card {:collection_position 1}] - (with-cards-in-writeable-collection card - ((test-users/user->client :rasta) :put 202 (str "card/" (u/get-id card)) - {:collection_position nil}) - (db/select-one-field :collection_position Card :id (u/get-id card))))))) - - + (tt/with-temp Card [card {:collection_position 1}] + (with-cards-in-writeable-collection card + ((mt/user->client :rasta) :put 202 (str "card/" (u/get-id card)) + {:collection_position nil}) + (is (= nil + (db/select-one-field :collection_position Card :id (u/get-id card))))))) (deftest ---we-shouldn-t-be-able-to-if-we-don-t-have-permissions-for-the-collection - (is (nil? (tu/with-non-admin-groups-no-root-collection-perms - (tt/with-temp* [Collection [collection] - Card [card {:collection_id (u/get-id collection)}]] - ((test-users/user->client :rasta) :put 403 (str "card/" (u/get-id card)) - {:collection_position 1}) - (db/select-one-field :collection_position Card :id (u/get-id card))))))) + (tu/with-non-admin-groups-no-root-collection-perms + (tt/with-temp* [Collection [collection] + Card [card {:collection_id (u/get-id collection)}]] + ((mt/user->client :rasta) :put 403 (str "card/" (u/get-id card)) + {:collection_position 1}) + (is (= nil + (db/select-one-field :collection_position Card :id (u/get-id card))))))) (deftest gets-a-card - (is (= 1 - (tu/with-non-admin-groups-no-root-collection-perms - (tt/with-temp* [Collection [collection] - Card [card {:collection_id (u/get-id collection), :collection_position 1}]] - ((test-users/user->client :rasta) :put 403 (str "card/" (u/get-id card)) - {:collection_position nil}) + (tu/with-non-admin-groups-no-root-collection-perms + (tt/with-temp* [Collection [collection] + Card [card {:collection_id (u/get-id collection), :collection_position 1}]] + ((mt/user->client :rasta) :put 403 (str "card/" (u/get-id card)) + {:collection_position nil}) + (is (= 1 (db/select-one-field :collection_position Card :id (u/get-id card))))))) ;;; +----------------------------------------------------------------------------------------------------------------+ -;;; | UPDATING THE POSITION OF A CARDS | +;;; | Updating the positions of stuff | ;;; +----------------------------------------------------------------------------------------------------------------+ (defn- name->position [results] @@ -677,65 +676,69 @@ "Call the collection endpoint for `collection-id` as `user-kwd`. Will return a map with the names of the items as keys and their position as the value" [user-kwd collection-or-collection-id] - (name->position ((test-users/user->client user-kwd) :get 200 (format "collection/%s/items" (u/get-id collection-or-collection-id))))) + (name->position ((mt/user->client user-kwd) :get 200 (format "collection/%s/items" (u/get-id collection-or-collection-id))))) (defmacro with-ordered-items "Macro for creating many sequetial collection_position model instances, putting each in `collection`" [collection model-and-name-syms & body] `(tt/with-temp* ~(vec (mapcat (fn [idx [model-instance name-sym]] - [model-instance [name-sym {:name (name name-sym) - :collection_id `(u/get-id ~collection) + [model-instance [name-sym {:name (name name-sym) + :collection_id `(u/get-id ~collection) :collection_position idx}]]) (iterate inc 1) (partition-all 2 model-and-name-syms))) - ~@body)) + (testing (format "\nWith ordered items in Collection %d: %s" + (u/get-id ~collection) + ~(str/join ", " (for [[model symb] (partition-all 2 model-and-name-syms)] + (format "%s %s" (name model) (name symb))))) + ~@body))) (deftest check-to-make-sure-we-can-move-a-card-in-a-collection-of-just-cards - (is (= {"c" 1 - "a" 2 - "b" 3 - "d" 4} - (tu/with-non-admin-groups-no-root-collection-perms - (tt/with-temp Collection [collection] - (with-ordered-items collection [Card a - Card b - Card c - Card d] - (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - ((test-users/user->client :rasta) :put 202 (str "card/" (u/get-id c)) - {:collection_position 1}) + (tu/with-non-admin-groups-no-root-collection-perms + (tt/with-temp Collection [collection] + (with-ordered-items collection [Card a + Card b + Card c + Card d] + (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) + ((mt/user->client :rasta) :put 202 (str "card/" (u/get-id c)) + {:collection_position 1}) + (is (= {"c" 1 + "a" 2 + "b" 3 + "d" 4} (get-name->collection-position :rasta collection))))))) (deftest change-the-position-of-the-4th-card-to-1st--all-other-cards-should-inc-their-position - (is (= {"d" 1 - "a" 2 - "b" 3 - "c" 4} - (tu/with-non-admin-groups-no-root-collection-perms - (tt/with-temp Collection [collection] - (with-ordered-items collection [Dashboard a - Dashboard b - Pulse c - Card d] - (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - ((test-users/user->client :rasta) :put 202 (str "card/" (u/get-id d)) - {:collection_position 1}) + (tu/with-non-admin-groups-no-root-collection-perms + (tt/with-temp Collection [collection] + (with-ordered-items collection [Dashboard a + Dashboard b + Pulse c + Card d] + (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) + ((mt/user->client :rasta) :put 202 (str "card/" (u/get-id d)) + {:collection_position 1}) + (is (= {"d" 1 + "a" 2 + "b" 3 + "c" 4} (get-name->collection-position :rasta collection))))))) (deftest change-the-position-of-the-1st-card-to-the-4th--all-of-the-other-items-dec - (is (= {"b" 1 - "c" 2 - "d" 3 - "a" 4} - (tu/with-non-admin-groups-no-root-collection-perms - (tt/with-temp Collection [collection] - (with-ordered-items collection [Card a - Dashboard b - Pulse c - Dashboard d] - (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - ((test-users/user->client :rasta) :put 202 (str "card/" (u/get-id a)) - {:collection_position 4}) + (tu/with-non-admin-groups-no-root-collection-perms + (tt/with-temp Collection [collection] + (with-ordered-items collection [Card a + Dashboard b + Pulse c + Dashboard d] + (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) + ((mt/user->client :rasta) :put 202 (str "card/" (u/get-id a)) + {:collection_position 4}) + (is (= {"b" 1 + "c" 2 + "d" 3 + "a" 4} (get-name->collection-position :rasta collection))))))) (deftest change-the-position-of-a-card-from-nil-to-2nd--should-adjust-the-existing-items @@ -751,7 +754,7 @@ Dashboard [_ {:name "c", :collection_id coll-id, :collection_position 2}] Card [_ {:name "d", :collection_id coll-id, :collection_position 3}]] (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - ((test-users/user->client :rasta) :put 202 (str "card/" (u/get-id b)) + ((mt/user->client :rasta) :put 202 (str "card/" (u/get-id b)) {:collection_position 2}) (get-name->collection-position :rasta coll-id)))))) @@ -767,7 +770,7 @@ Dashboard c Pulse d] (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - ((test-users/user->client :rasta) :put 202 (str "card/" (u/get-id b)) + ((mt/user->client :rasta) :put 202 (str "card/" (u/get-id b)) {:collection_position nil}) (get-name->collection-position :rasta collection))))))) @@ -795,7 +798,7 @@ Dashboard h] (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection-1) (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection-2) - ((test-users/user->client :rasta) :put 202 (str "card/" (u/get-id f)) + ((mt/user->client :rasta) :put 202 (str "card/" (u/get-id f)) {:collection_id (u/get-id collection-1)}) [(get-name->collection-position :rasta collection-1) (get-name->collection-position :rasta collection-2)]))))))) @@ -822,7 +825,7 @@ Card h] (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection-1) (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection-2) - ((test-users/user->client :rasta) :put 202 (str "card/" (u/get-id h)) + ((mt/user->client :rasta) :put 202 (str "card/" (u/get-id h)) {:collection_position 1, :collection_id (u/get-id collection-1)}) [(get-name->collection-position :rasta collection-1) (get-name->collection-position :rasta collection-2)]))))))) @@ -849,7 +852,7 @@ (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) [(get-name->collection-position :rasta collection) (do - ((test-users/user->client :rasta) :post 202 "card" + ((mt/user->client :rasta) :post 202 "card" (merge (card-with-name-and-query "a") {:collection_id (u/get-id collection) :collection_position 1})) @@ -876,7 +879,7 @@ (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) [(get-name->collection-position :rasta collection) (do - ((test-users/user->client :rasta) :post 202 "card" + ((mt/user->client :rasta) :post 202 "card" (merge (card-with-name-and-query "d") {:collection_id (u/get-id collection) :collection_position 4})) @@ -903,7 +906,7 @@ (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) [(get-name->collection-position :rasta collection) (do - ((test-users/user->client :rasta) :post 202 "card" + ((mt/user->client :rasta) :post 202 "card" (merge (card-with-name-and-query "d") {:collection_id (u/get-id collection) :collection_position nil})) @@ -924,9 +927,9 @@ Pulse e Pulse f] (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - ((test-users/user->client :rasta) :put 202 (str "card/" (u/get-id d)) + ((mt/user->client :rasta) :put 202 (str "card/" (u/get-id d)) {:collection_position 1, :collection_id (u/get-id collection)}) - (name->position ((test-users/user->client :rasta) :get 200 (format "collection/%s/items" (u/get-id collection)))))))))) + (name->position ((mt/user->client :rasta) :get 200 (format "collection/%s/items" (u/get-id collection)))))))))) ;;; +----------------------------------------------------------------------------------------------------------------+ @@ -946,23 +949,23 @@ [{:message "Archiving a Card should trigger Alert deletion" :expected-email "the question was archived by Rasta Toucan" :f (fn [{:keys [card]}] - ((test-users/user->client :rasta) :put 202 (str "card/" (u/get-id card)) {:archived true}))} + ((mt/user->client :rasta) :put 202 (str "card/" (u/get-id card)) {:archived true}))} {:message "Validate changing a display type triggers alert deletion" :card {:display :table} :expected-email "the question was edited by Rasta Toucan" :f (fn [{:keys [card]}] - ((test-users/user->client :rasta) :put 202 (str "card/" (u/get-id card)) {:display :line}))} + ((mt/user->client :rasta) :put 202 (str "card/" (u/get-id card)) {:display :line}))} {:message "Changing the display type from line to table should force a delete" :card {:display :line} :expected-email "the question was edited by Rasta Toucan" :f (fn [{:keys [card]}] - ((test-users/user->client :rasta) :put 202 (str "card/" (u/get-id card)) {:display :table}))} + ((mt/user->client :rasta) :put 202 (str "card/" (u/get-id card)) {:display :table}))} {:message "Removing the goal value will trigger the alert to be deleted" :card {:display :line :visualization_settings {:graph.goal_value 10}} :expected-email "the question was edited by Rasta Toucan" :f (fn [{:keys [card]}] - ((test-users/user->client :rasta) :put 202 (str "card/" (u/get-id card)) {:visualization_settings {:something "else"}}))} + ((mt/user->client :rasta) :put 202 (str "card/" (u/get-id card)) {:visualization_settings {:something "else"}}))} {:message "Adding an additional breakout will cause the alert to be removed" :card {:display :line :visualization_settings {:graph.goal_value 10} @@ -974,7 +977,7 @@ "hour"]])} :expected-email "the question was edited by Crowberto Corv" :f (fn [{:keys [card]}] - ((test-users/user->client :crowberto) :put 202 (str "card/" (u/get-id card)) + ((mt/user->client :crowberto) :put 202 (str "card/" (u/get-id card)) {:dataset_query (assoc-in (mbql-count-query (data/id) (data/id :checkins)) [:query :breakout] [[:datetime-field (data/id :checkins :date) "hour"] [:datetime-field (data/id :checkins :date) "minute"]])}))}]] @@ -982,16 +985,16 @@ (tt/with-temp* [Card [card card] Pulse [pulse {:alert_condition "rows" :alert_first_only false - :creator_id (test-users/user->id :rasta) + :creator_id (mt/user->id :rasta) :name "Original Alert Name"}] PulseCard [_ {:pulse_id (u/get-id pulse) :card_id (u/get-id card) :position 0}] PulseChannel [pc {:pulse_id (u/get-id pulse)}] - PulseChannelRecipient [_ {:user_id (test-users/user->id :crowberto) + PulseChannelRecipient [_ {:user_id (mt/user->id :crowberto) :pulse_channel_id (u/get-id pc)}] - PulseChannelRecipient [_ {:user_id (test-users/user->id :rasta) + PulseChannelRecipient [_ {:user_id (mt/user->id :rasta) :pulse_channel_id (u/get-id pc)}]] (with-cards-in-writeable-collection card (et/with-fake-inbox @@ -1015,23 +1018,23 @@ :visualization_settings {:graph.goal_value 10}}] Pulse [pulse {:alert_condition "goal" :alert_first_only false - :creator_id (test-users/user->id :rasta) + :creator_id (mt/user->id :rasta) :name "Original Alert Name"}] PulseCard [_ {:pulse_id (u/get-id pulse) :card_id (u/get-id card) :position 0}] PulseChannel [pc {:pulse_id (u/get-id pulse)}] - PulseChannelRecipient [_ {:user_id (test-users/user->id :rasta) + PulseChannelRecipient [_ {:user_id (mt/user->id :rasta) :pulse_channel_id (u/get-id pc)}]] (with-cards-in-writeable-collection card (et/with-fake-inbox (array-map :emails-1 (do - ((test-users/user->client :rasta) :put 202 (str "card/" (u/get-id card)) {:display :area}) + ((mt/user->client :rasta) :put 202 (str "card/" (u/get-id card)) {:display :area}) (et/regex-email-bodies #"the question was edited by Rasta Toucan")) :pulse-1 (boolean (Pulse (u/get-id pulse))) :emails-2 (do - ((test-users/user->client :rasta) :put 202 (str "card/" (u/get-id card)) {:display :bar}) + ((mt/user->client :rasta) :put 202 (str "card/" (u/get-id card)) {:display :bar}) (et/regex-email-bodies #"the question was edited by Rasta Toucan")) :pulse-2 (boolean (Pulse (u/get-id pulse)))))))))) @@ -1043,13 +1046,13 @@ (deftest check-that-we-can-delete-a-card (is (nil? (tt/with-temp Card [card] (with-cards-in-writeable-collection card - ((test-users/user->client :rasta) :delete 204 (str "card/" (u/get-id card))) + ((mt/user->client :rasta) :delete 204 (str "card/" (u/get-id card))) (Card (u/get-id card))))))) ;; deleting a card that doesn't exist should return a 404 (#1957) (deftest deleting-a-card-that-doesnt-exist-should-return-a-404---1957- (is (= "Not found." - ((test-users/user->client :crowberto) :delete 404 "card/12345")))) + ((mt/user->client :crowberto) :delete 404 "card/12345")))) ;;; +----------------------------------------------------------------------------------------------------------------+ @@ -1058,13 +1061,13 @@ ;; Helper Functions (defn- fave? [card] - (db/exists? CardFavorite, :card_id (u/get-id card), :owner_id (test-users/user->id :rasta))) + (db/exists? CardFavorite, :card_id (u/get-id card), :owner_id (mt/user->id :rasta))) (defn- fave! [card] - ((test-users/user->client :rasta) :post 200 (format "card/%d/favorite" (u/get-id card)))) + ((mt/user->client :rasta) :post 200 (format "card/%d/favorite" (u/get-id card)))) (defn- unfave! [card] - ((test-users/user->client :rasta) :delete 204 (format "card/%d/favorite" (u/get-id card)))) + ((mt/user->client :rasta) :delete 204 (format "card/%d/favorite" (u/get-id card)))) ;; ## GET /api/card/:id/favorite (deftest can-we-see-if-a-card-is-a-favorite-- @@ -1115,7 +1118,7 @@ (is (= ["COUNT(*)" "75"] (str/split-lines - ((test-users/user->client :rasta) :post 202 (format "card/%d/query/csv" + ((mt/user->client :rasta) :post 202 (format "card/%d/query/csv" (u/get-id card))))))))) (testing "with-paramters" (with-temp-native-card-with-params [_ card] @@ -1123,7 +1126,7 @@ (is (= ["COUNT(*)" "8"] (str/split-lines - ((test-users/user->client :rasta) :post 202 (format "card/%d/query/csv?parameters=%s" + ((mt/user->client :rasta) :post 202 (format "card/%d/query/csv?parameters=%s" (u/get-id card) encoded-params))))))))) (deftest json-download-test @@ -1131,12 +1134,12 @@ (with-temp-native-card [_ card] (with-cards-in-readable-collection card (is (= [{(keyword "COUNT(*)") 75}] - ((test-users/user->client :rasta) :post 202 (format "card/%d/query/json" (u/get-id card)))))))) + ((mt/user->client :rasta) :post 202 (format "card/%d/query/json" (u/get-id card)))))))) (testing "with parameters" (with-temp-native-card-with-params [_ card] (with-cards-in-readable-collection card (is (= [{(keyword "COUNT(*)") 8}] - ((test-users/user->client :rasta) :post 202 (format "card/%d/query/json?parameters=%s" + ((mt/user->client :rasta) :post 202 (format "card/%d/query/json?parameters=%s" (u/get-id card) encoded-params)))))))) (defn- parse-xlsx-results [results] @@ -1152,14 +1155,14 @@ (with-cards-in-readable-collection card (is (= [{:col "COUNT(*)"} {:col 75.0}] (parse-xlsx-results - ((test-users/user->client :rasta) :post 202 (format "card/%d/query/xlsx" (u/get-id card)) + ((mt/user->client :rasta) :post 202 (format "card/%d/query/xlsx" (u/get-id card)) {:request-options {:as :byte-array}}))))))) (testing "with parameters" (with-temp-native-card-with-params [_ card] (with-cards-in-readable-collection card (is (= [{:col "COUNT(*)"} {:col 8.0}] (parse-xlsx-results - ((test-users/user->client :rasta) :post 202 (format "card/%d/query/xlsx?parameters=%s" + ((mt/user->client :rasta) :post 202 (format "card/%d/query/xlsx?parameters=%s" (u/get-id card) encoded-params) {:request-options {:as :byte-array}})))))))) @@ -1178,18 +1181,18 @@ options))] (testing "Sanity check: this CSV download should not be subject to C O N S T R A I N T S" (is (= {:constraints nil} - ((test-users/user->client :rasta) :post 200 (format "card/%d/query/csv" (u/get-id card)))))) + ((mt/user->client :rasta) :post 200 (format "card/%d/query/csv" (u/get-id card)))))) (with-redefs [constraints/default-query-constraints {:max-results 10, :max-results-bare-rows 10}] (testing (str "Downloading CSV/JSON/XLSX results shouldn't be subject to the default query constraints -- even " "if the query comes in with `add-default-userland-constraints` (as will be the case if the query " "gets saved from one that had it -- see #9831)") (is (= {:constraints nil} - ((test-users/user->client :rasta) :post 200 (format "card/%d/query/csv" (u/get-id card)))))) + ((mt/user->client :rasta) :post 200 (format "card/%d/query/csv" (u/get-id card)))))) (testing (str "non-\"download\" queries should still get the default constraints (this also is a sanitiy " "check to make sure the `with-redefs` in the test above actually works)") (is (= {:constraints {:max-results 10, :max-results-bare-rows 10}} - ((test-users/user->client :rasta) :post 200 (format "card/%d/query" (u/get-id card)))))))))))) + ((mt/user->client :rasta) :post 200 (format "card/%d/query" (u/get-id card)))))))))))) ;;; +----------------------------------------------------------------------------------------------------------------+ @@ -1201,7 +1204,7 @@ (tt/with-temp Collection [collection] (tu/with-model-cleanup [Card] (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - (let [card ((test-users/user->client :rasta) :post 202 "card" + (let [card ((mt/user->client :rasta) :post 202 "card" (assoc (card-with-name-and-query) :collection_id (u/get-id collection)))] (= (db/select-one-field :collection_id Card :id (u/get-id card)) @@ -1212,14 +1215,14 @@ (tu/with-non-admin-groups-no-root-collection-perms (tu/with-model-cleanup [Card] (tt/with-temp Collection [collection] - ((test-users/user->client :rasta) :post 403 "card" + ((mt/user->client :rasta) :post 403 "card" (assoc (card-with-name-and-query) :collection_id (u/get-id collection))))))))) (deftest make-sure-we-can-change-the--collection-id--of-a-card-if-it-s-not-in-any-collection (tt/with-temp* [Card [card] Collection [collection]] - ((test-users/user->client :crowberto) :put 202 (str "card/" (u/get-id card)) {:collection_id (u/get-id collection)}) + ((mt/user->client :crowberto) :put 202 (str "card/" (u/get-id card)) {:collection_id (u/get-id collection)}) (= (db/select-one-field :collection_id Card :id (u/get-id card)) (u/get-id collection)))) @@ -1228,7 +1231,7 @@ (tu/with-non-admin-groups-no-root-collection-perms (tt/with-temp* [Collection [collection] Card [card {:collection_id (u/get-id collection)}]] - ((test-users/user->client :rasta) :put 403 (str "card/" (u/get-id card)) + ((mt/user->client :rasta) :put 403 (str "card/" (u/get-id card)) {:name "Number of Blueberries Consumed Per Month"})))))) @@ -1241,7 +1244,7 @@ Collection [new-collection] Card [card {:collection_id (u/get-id original-collection)}]] (perms/grant-collection-readwrite-permissions! (perms-group/all-users) original-collection) - ((test-users/user->client :rasta) :put 403 (str "card/" (u/get-id card)) + ((mt/user->client :rasta) :put 403 (str "card/" (u/get-id card)) {:collection_id (u/get-id new-collection)})))))) @@ -1254,7 +1257,7 @@ Collection [new-collection] Card [card {:collection_id (u/get-id original-collection)}]] (perms/grant-collection-readwrite-permissions! (perms-group/all-users) new-collection) - ((test-users/user->client :rasta) :put 403 (str "card/" (u/get-id card)) + ((mt/user->client :rasta) :put 403 (str "card/" (u/get-id card)) {:collection_id (u/get-id new-collection)})))))) @@ -1266,7 +1269,7 @@ Card [card {:collection_id (u/get-id original-collection)}]] (perms/grant-collection-readwrite-permissions! (perms-group/all-users) original-collection) (perms/grant-collection-readwrite-permissions! (perms-group/all-users) new-collection) - ((test-users/user->client :rasta) :put 202 (str "card/" (u/get-id card)) {:collection_id (u/get-id new-collection)}) + ((mt/user->client :rasta) :put 202 (str "card/" (u/get-id card)) {:collection_id (u/get-id new-collection)}) (= (db/select-one-field :collection_id Card :id (u/get-id card)) (u/get-id new-collection))))) @@ -1290,7 +1293,7 @@ [username expected-status-code collection-or-collection-id-or-nil cards-or-card-ids] (array-map :response - ((test-users/user->client username) :post expected-status-code "card/collections" + ((mt/user->client username) :post expected-status-code "card/collections" {:collection_id (when collection-or-collection-id-or-nil (u/get-id collection-or-collection-id-or-nil)) :card_ids (map u/get-id cards-or-card-ids)}) @@ -1376,8 +1379,8 @@ Card [card-e {:name "e", :collection_id coll-id-2, :collection_position 2}] Card [card-f {:name "f", :collection_id coll-id-2, :collection_position 3}]] [(POST-card-collections! :crowberto 200 new-collection [card-a card-b]) - (merge (name->position ((test-users/user->client :crowberto) :get 200 (format "collection/%s/items" coll-id-1) :model "card" :archived "false")) - (name->position ((test-users/user->client :crowberto) :get 200 (format "collection/%s/items" coll-id-2) :model "card" :archived "false")))])))) + (merge (name->position ((mt/user->client :crowberto) :get 200 (format "collection/%s/items" coll-id-1) :model "card" :archived "false")) + (name->position ((mt/user->client :crowberto) :get 200 (format "collection/%s/items" coll-id-2) :model "card" :archived "false")))])))) (deftest moving-a-card-without-a-collection-position-keeps-the-collection-position-nil (is (= [{:response {:status "ok"} @@ -1392,8 +1395,8 @@ Card [card-b {:name "b", :collection_id coll-id-2, :collection_position 1}] Card [card-c {:name "c", :collection_id coll-id-2, :collection_position 2}]] [(POST-card-collections! :crowberto 200 new-collection [card-a card-b]) - (merge (name->position ((test-users/user->client :crowberto) :get 200 (format "collection/%s/items" coll-id-1) :model "card" :archived "false")) - (name->position ((test-users/user->client :crowberto) :get 200 (format "collection/%s/items" coll-id-2) :model "card" :archived "false")))])))) + (merge (name->position ((mt/user->client :crowberto) :get 200 (format "collection/%s/items" coll-id-1) :model "card" :archived "false")) + (name->position ((mt/user->client :crowberto) :get 200 (format "collection/%s/items" coll-id-2) :model "card" :archived "false")))])))) ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | PUBLIC SHARING ENDPOINTS | @@ -1401,7 +1404,7 @@ (defn- shared-card [] {:public_uuid (str (UUID/randomUUID)) - :made_public_by_id (test-users/user->id :crowberto)}) + :made_public_by_id (mt/user->id :crowberto)}) ;;; ----------------------------------------- POST /api/card/:id/public_link ----------------------------------------- @@ -1409,7 +1412,7 @@ (deftest test-that-we-can-share-a-card (tu/with-temporary-setting-values [enable-public-sharing true] (tt/with-temp Card [card] - (let [{uuid :uuid} ((test-users/user->client :crowberto) :post 200 (format "card/%d/public_link" (u/get-id card)))] + (let [{uuid :uuid} ((mt/user->client :crowberto) :post 200 (format "card/%d/public_link" (u/get-id card)))] (is (= true (boolean (db/exists? Card :id (u/get-id card), :public_uuid uuid)))))))) @@ -1417,37 +1420,37 @@ (is (= "You don't have permissions to do that." (tu/with-temporary-setting-values [enable-public-sharing true] (tt/with-temp Card [card] - ((test-users/user->client :rasta) :post 403 (format "card/%d/public_link" (u/get-id card)))))))) + ((mt/user->client :rasta) :post 403 (format "card/%d/public_link" (u/get-id card)))))))) (deftest test-that-we--cannot--share-a-card-if-the-setting-is-disabled (is (= "Public sharing is not enabled." (tu/with-temporary-setting-values [enable-public-sharing false] (tt/with-temp Card [card] - ((test-users/user->client :crowberto) :post 400 (format "card/%d/public_link" (u/get-id card)))))))) + ((mt/user->client :crowberto) :post 400 (format "card/%d/public_link" (u/get-id card)))))))) (deftest test-that-we--cannot--share-a-card-if-the-card-has-been-archived (is (= {:message "The object has been archived.", :error_code "archived"} (tu/with-temporary-setting-values [enable-public-sharing true] (tt/with-temp Card [card {:archived true}] - ((test-users/user->client :crowberto) :post 404 (format "card/%d/public_link" (u/get-id card)))))))) + ((mt/user->client :crowberto) :post 404 (format "card/%d/public_link" (u/get-id card)))))))) (deftest test-that-we-get-a-404-if-the-card-doesnt-exist (is (= "Not found." (tu/with-temporary-setting-values [enable-public-sharing true] - ((test-users/user->client :crowberto) :post 404 (format "card/%d/public_link" Integer/MAX_VALUE)))))) + ((mt/user->client :crowberto) :post 404 (format "card/%d/public_link" Integer/MAX_VALUE)))))) (deftest test-that-if-a-card-has-already-been-shared-we-re-se-the-existing-uuid (tu/with-temporary-setting-values [enable-public-sharing true] (tt/with-temp Card [card (shared-card)] (= (:public_uuid card) - (:uuid ((test-users/user->client :crowberto) :post 200 (format "card/%d/public_link" (u/get-id card)))))))) + (:uuid ((mt/user->client :crowberto) :post 200 (format "card/%d/public_link" (u/get-id card)))))))) ;;; ---------------------------------------- DELETE /api/card/:id/public_link ---------------------------------------- (deftest test-that-we-can-unshare-a-card (tu/with-temporary-setting-values [enable-public-sharing true] (tt/with-temp Card [card (shared-card)] - ((test-users/user->client :crowberto) :delete 204 (format "card/%d/public_link" (u/get-id card))) + ((mt/user->client :crowberto) :delete 204 (format "card/%d/public_link" (u/get-id card))) (is (= false (db/exists? Card :id (u/get-id card), :public_uuid (:public_uuid card))))))) @@ -1455,34 +1458,34 @@ (is (= "You don't have permissions to do that." (tu/with-temporary-setting-values [enable-public-sharing true] (tt/with-temp Card [card (shared-card)] - ((test-users/user->client :rasta) :delete 403 (format "card/%d/public_link" (u/get-id card)))))))) + ((mt/user->client :rasta) :delete 403 (format "card/%d/public_link" (u/get-id card)))))))) (deftest test-that-we-get-a-404-if-card-isn-t-shared (is (= "Not found." (tu/with-temporary-setting-values [enable-public-sharing true] (tt/with-temp Card [card] - ((test-users/user->client :crowberto) :delete 404 (format "card/%d/public_link" (u/get-id card)))))))) + ((mt/user->client :crowberto) :delete 404 (format "card/%d/public_link" (u/get-id card)))))))) (deftest test-that-we-get-a-404-if-card-doesnt-exist (is (= "Not found." (tu/with-temporary-setting-values [enable-public-sharing true] - ((test-users/user->client :crowberto) :delete 404 (format "card/%d/public_link" Integer/MAX_VALUE)))))) + ((mt/user->client :crowberto) :delete 404 (format "card/%d/public_link" Integer/MAX_VALUE)))))) (deftest test-that-we-can-fetch-a-list-of-publicly-accessible-cards - (is (= [{:name true, :id true, :public_uuid true}] - (tu/with-temporary-setting-values [enable-public-sharing true] - (tt/with-temp Card [card (shared-card)] - (for [card ((test-users/user->client :crowberto) :get 200 "card/public")] + (tu/with-temporary-setting-values [enable-public-sharing true] + (tt/with-temp Card [card (shared-card)] + (is (= [{:name true, :id true, :public_uuid true}] + (for [card ((mt/user->client :crowberto) :get 200 "card/public")] (m/map-vals boolean (select-keys card [:name :id :public_uuid])))))))) (deftest test-that-we-can-fetch-a-list-of-embeddable-cards - (is (= [{:name true, :id true}] - (tu/with-temporary-setting-values [enable-embedding true] - (tt/with-temp Card [card {:enable_embedding true}] - (for [card ((test-users/user->client :crowberto) :get 200 "card/embeddable")] + (tu/with-temporary-setting-values [enable-embedding true] + (tt/with-temp Card [card {:enable_embedding true}] + (is (= [{:name true, :id true}] + (for [card ((mt/user->client :crowberto) :get 200 "card/embeddable")] (m/map-vals boolean (select-keys card [:name :id])))))))) (deftest test-related-recommended-entities - (is (= #{:table :metrics :segments :dashboard-mates :similar-questions :canonical-metric :dashboards :collections} - (tt/with-temp Card [card] - (-> ((test-users/user->client :crowberto) :get 200 (format "card/%s/related" (u/get-id card))) keys set))))) + (tt/with-temp Card [card] + (is (= #{:table :metrics :segments :dashboard-mates :similar-questions :canonical-metric :dashboards :collections} + (-> ((mt/user->client :crowberto) :get 200 (format "card/%s/related" (u/get-id card))) keys set))))) diff --git a/test/metabase/api/pulse_test.clj b/test/metabase/api/pulse_test.clj index 8a81833c5ca807378980f839a20c59ad4485298c..c3293fd05e5949e33e218bfb77f821ec39a78d7a 100644 --- a/test/metabase/api/pulse_test.clj +++ b/test/metabase/api/pulse_test.clj @@ -1,10 +1,10 @@ (ns metabase.api.pulse-test "Tests for /api/pulse endpoints." (:require [clojure.test :refer :all] - [expectations :refer [expect]] [metabase - [email-test :as et] [http-client :as http] + [models :refer [Card Collection Dashboard Pulse PulseCard PulseChannel PulseChannelRecipient]] + [test :as mt] [util :as u]] [metabase.api [card-test :as card-api-test] @@ -12,25 +12,14 @@ [metabase.integrations.slack :as slack] [metabase.middleware.util :as middleware.u] [metabase.models - [card :refer [Card]] - [collection :refer [Collection]] - [dashboard :refer [Dashboard]] - [database :refer [Database]] [permissions :as perms] [permissions-group :as perms-group] - [pulse :as pulse :refer [Pulse]] - [pulse-card :refer [PulseCard]] - [pulse-channel :refer [PulseChannel]] - [pulse-channel-recipient :refer [PulseChannelRecipient]] - [pulse-test :as pulse-test] - [table :refer [Table]]] - [metabase.test - [data :as data] - [util :as tu]] - [metabase.test.data.users :refer :all] + [pulse :as pulse] + [pulse-test :as pulse-test]] + [metabase.pulse.render.png :as png] [metabase.test.mock.util :refer [pulse-channel-defaults]] - [toucan.db :as db] - [toucan.util.test :as tt])) + [schema.core :as s] + [toucan.db :as db])) ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | Helper Fns & Macros | @@ -68,8 +57,8 @@ (update card :collection_id boolean))))) (defn- do-with-pulses-in-a-collection [grant-collection-perms-fn! pulses-or-ids f] - (tu/with-non-admin-groups-no-root-collection-perms - (tt/with-temp Collection [collection] + (mt/with-non-admin-groups-no-root-collection-perms + (mt/with-temp Collection [collection] (grant-collection-perms-fn! (perms-group/all-users) collection) ;; use db/execute! instead of db/update! so the updated_at field doesn't get automatically updated! (when (seq pulses-or-ids) @@ -92,8 +81,9 @@ ;; We assume that all endpoints for a given context are enforced by the same middleware, so we don't run the same ;; authentication test on every single individual endpoint -(expect (:body middleware.u/response-unauthentic) (http/client :get 401 "pulse")) -(expect (:body middleware.u/response-unauthentic) (http/client :put 401 "pulse/13")) +(deftest authentication-test + (is (= (:body middleware.u/response-unauthentic) (http/client :get 401 "pulse"))) + (is (= (:body middleware.u/response-unauthentic) (http/client :put 401 "pulse/13")))) ;;; +----------------------------------------------------------------------------------------------------------------+ @@ -107,43 +97,41 @@ "`(collection_id, description, display, id, include_csv, include_xls, name)` " "2) value must be a map with the keys `id`, `include_csv`, and `include_xls`. The array cannot be empty.")}}) -(expect - {:errors {:name "value must be a non-blank string."}} - ((user->client :rasta) :post 400 "pulse" {})) - -(expect - default-post-card-ref-validation-error - ((user->client :rasta) :post 400 "pulse" {:name "abc"})) - -(expect - default-post-card-ref-validation-error - ((user->client :rasta) :post 400 "pulse" {:name "abc" - :cards "foobar"})) - -(expect - default-post-card-ref-validation-error - ((user->client :rasta) :post 400 "pulse" {:name "abc" - :cards ["abc"]})) - -(expect - {:errors {:channels "value must be an array. Each value must be a map. The array cannot be empty."}} - ((user->client :rasta) :post 400 "pulse" {:name "abc" - :cards [{:id 100, :include_csv false, :include_xls false} - {:id 200, :include_csv false, :include_xls false}]})) - -(expect - {:errors {:channels "value must be an array. Each value must be a map. The array cannot be empty."}} - ((user->client :rasta) :post 400 "pulse" {:name "abc" - :cards [{:id 100, :include_csv false, :include_xls false} - {:id 200, :include_csv false, :include_xls false}] - :channels "foobar"})) - -(expect - {:errors {:channels "value must be an array. Each value must be a map. The array cannot be empty."}} - ((user->client :rasta) :post 400 "pulse" {:name "abc" - :cards [{:id 100, :include_csv false, :include_xls false} - {:id 200, :include_csv false, :include_xls false}] - :channels ["abc"]})) +(deftest create-pulse-validation-test + (doseq [[input expected-error] + {{} + {:errors {:name "value must be a non-blank string."}} + + {:name "abc"} + default-post-card-ref-validation-error + + {:name "abc" + :cards "foobar"} + default-post-card-ref-validation-error + + {:name "abc" + :cards ["abc"]} + default-post-card-ref-validation-error + + {:name "abc" + :cards [{:id 100, :include_csv false, :include_xls false} + {:id 200, :include_csv false, :include_xls false}]} + {:errors {:channels "value must be an array. Each value must be a map. The array cannot be empty."}} + + {:name "abc" + :cards [{:id 100, :include_csv false, :include_xls false} + {:id 200, :include_csv false, :include_xls false}] + :channels "foobar"} + {:errors {:channels "value must be an array. Each value must be a map. The array cannot be empty."}} + + {:name "abc" + :cards [{:id 100, :include_csv false, :include_xls false} + {:id 200, :include_csv false, :include_xls false}] + :channels ["abc"]} + {:errors {:channels "value must be an array. Each value must be a map. The array cannot be empty."}}}] + (testing (pr-str input) + (is (= expected-error + ((mt/user->client :rasta) :post 400 "pulse" input)))))) (defn- remove-extra-channels-fields [channels] (for [channel channels] @@ -165,49 +153,57 @@ :schedule_day nil :recipients []}) -(tt/expect-with-temp [Card [card-1] - Card [card-2]] - (merge - pulse-defaults - {:name "A Pulse" - :creator_id (user->id :rasta) - :creator (user-details (fetch-user :rasta)) - :cards (for [card [card-1 card-2]] - (assoc (pulse-card-details card) - :collection_id true)) - :channels [(merge pulse-channel-defaults - {:channel_type "email" - :schedule_type "daily" - :schedule_hour 12 - :recipients []})] - :collection_id true}) - (card-api-test/with-cards-in-readable-collection [card-1 card-2] - (tt/with-temp Collection [collection] - (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - (tu/with-model-cleanup [Pulse] - (-> ((user->client :rasta) :post 200 "pulse" {:name "A Pulse" - :collection_id (u/get-id collection) - :cards [{:id (u/get-id card-1) - :include_csv false - :include_xls false} - {:id (u/get-id card-2) - :include_csv false - :include_xls false}] - :channels [daily-email-channel] - :skip_if_empty false}) - pulse-response - (update :channels remove-extra-channels-fields)))))) - -;; Create a pulse with a HybridPulseCard and a CardRef, PUT accepts this format, we should make sure POST does as well -(tt/expect-with-temp [Card [card-1] +(deftest create-test + (testing "POST /api/pulse" + (mt/with-temp* [Card [card-1] + Card [card-2]] + (card-api-test/with-cards-in-readable-collection [card-1 card-2] + (mt/with-temp Collection [collection] + (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) + (mt/with-model-cleanup [Pulse] + (is (= (merge + pulse-defaults + {:name "A Pulse" + :creator_id (mt/user->id :rasta) + :creator (user-details (mt/fetch-user :rasta)) + :cards (for [card [card-1 card-2]] + (assoc (pulse-card-details card) + :collection_id true)) + :channels [(merge pulse-channel-defaults + {:channel_type "email" + :schedule_type "daily" + :schedule_hour 12 + :recipients []})] + :collection_id true}) + (-> ((mt/user->client :rasta) :post 200 "pulse" {:name "A Pulse" + :collection_id (u/get-id collection) + :cards [{:id (u/get-id card-1) + :include_csv false + :include_xls false} + {:id (u/get-id card-2) + :include_csv false + :include_xls false}] + :channels [daily-email-channel] + :skip_if_empty false}) + pulse-response + (update :channels remove-extra-channels-fields)))))))))) + +(deftest create-with-hybrid-pulse-card-test + (testing "POST /api/pulse" + (testing "Create a pulse with a HybridPulseCard and a CardRef, PUT accepts this format, we should make sure POST does as well" + (mt/with-temp* [Card [card-1] Card [card-2 {:name "The card" :description "Info" :display :table}]] - (merge + (card-api-test/with-cards-in-readable-collection [card-1 card-2] + (mt/with-temp Collection [collection] + (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) + (mt/with-model-cleanup [Pulse] + (is (= (merge pulse-defaults {:name "A Pulse" - :creator_id (user->id :rasta) - :creator (user-details (fetch-user :rasta)) + :creator_id (mt/user->id :rasta) + :creator (user-details (mt/fetch-user :rasta)) :cards (for [card [card-1 card-2]] (assoc (pulse-card-details card) :collection_id true)) @@ -217,96 +213,88 @@ :schedule_hour 12 :recipients []})] :collection_id true}) - (card-api-test/with-cards-in-readable-collection [card-1 card-2] - (tt/with-temp Collection [collection] - (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - (tu/with-model-cleanup [Pulse] - (-> ((user->client :rasta) :post 200 "pulse" {:name "A Pulse" - :collection_id (u/get-id collection) - :cards [{:id (u/get-id card-1) - :include_csv false - :include_xls false} - (-> card-2 - (select-keys [:id :name :description :display :collection_id]) - (assoc :include_csv false, :include_xls false))] - :channels [daily-email-channel] - :skip_if_empty false}) - pulse-response - (update :channels remove-extra-channels-fields)))))) - -;; Create a pulse with a csv and xls -(tt/expect-with-temp [Card [card-1] + (-> ((mt/user->client :rasta) :post 200 "pulse" {:name "A Pulse" + :collection_id (u/get-id collection) + :cards [{:id (u/get-id card-1) + :include_csv false + :include_xls false} + (-> card-2 + (select-keys [:id :name :description :display :collection_id]) + (assoc :include_csv false, :include_xls false))] + :channels [daily-email-channel] + :skip_if_empty false}) + pulse-response + (update :channels remove-extra-channels-fields))))))))))) + +(deftest create-csv-xls-test + (testing "POST /api/pulse" + (testing "Create a pulse with a csv and xls" + (mt/with-temp* [Card [card-1] Card [card-2]] - (merge - pulse-defaults - {:name "A Pulse" - :creator_id (user->id :rasta) - :creator (user-details (fetch-user :rasta)) - :cards [(assoc (pulse-card-details card-1) :include_csv true, :include_xls true, :collection_id true) - (assoc (pulse-card-details card-2) :collection_id true)] - :channels [(merge pulse-channel-defaults - {:channel_type "email" - :schedule_type "daily" - :schedule_hour 12 - :recipients []})] - :collection_id true}) - (tu/with-non-admin-groups-no-root-collection-perms - (tt/with-temp Collection [collection] - (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - (tu/with-model-cleanup [Pulse] - (card-api-test/with-cards-in-readable-collection [card-1 card-2] - (-> ((user->client :rasta) :post 200 "pulse" {:name "A Pulse" - :collection_id (u/get-id collection) - :cards [{:id (u/get-id card-1) - :include_csv true - :include_xls true} - {:id (u/get-id card-2) - :include_csv false - :include_xls false}] - :channels [daily-email-channel] - :skip_if_empty false}) - pulse-response - (update :channels remove-extra-channels-fields))))))) - -;; Make sure we can create a Pulse with a Collection position -(expect - #metabase.models.pulse.PulseInstance{:collection_id true, :collection_position 1} - (tu/with-non-admin-groups-no-root-collection-perms - (tu/with-model-cleanup [Pulse] - (let [pulse-name (tu/random-name)] - (tt/with-temp* [Card [card] - Collection [collection]] - (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - (card-api-test/with-cards-in-readable-collection [card] - ((user->client :rasta) :post 200 "pulse" {:name pulse-name - :cards [{:id (u/get-id card) - :include_csv false - :include_xls false}] - :channels [daily-email-channel] - :skip_if_empty false - :collection_id (u/get-id collection) - :collection_position 1}) - (some-> (db/select-one [Pulse :collection_id :collection_position] :name pulse-name) - (update :collection_id (partial = (u/get-id collection)))))))))) - -;; ...but not if we don't have permissions for the Collection -(expect - nil - (tu/with-non-admin-groups-no-root-collection-perms - (tu/with-model-cleanup [Pulse] - (let [pulse-name (tu/random-name)] - (tt/with-temp* [Card [card] - Collection [collection]] - ((user->client :rasta) :post 403 "pulse" {:name pulse-name - :cards [{:id (u/get-id card) - :include_csv false - :include_xls false}] - :channels [daily-email-channel] - :skip_if_empty false - :collection_id (u/get-id collection) - :collection_position 1}) - (some-> (db/select-one [Pulse :collection_id :collection_position] :name pulse-name) - (update :collection_id (partial = (u/get-id collection))))))))) + (mt/with-non-admin-groups-no-root-collection-perms + (mt/with-temp Collection [collection] + (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) + (mt/with-model-cleanup [Pulse] + (card-api-test/with-cards-in-readable-collection [card-1 card-2] + (is (= (merge + pulse-defaults + {:name "A Pulse" + :creator_id (mt/user->id :rasta) + :creator (user-details (mt/fetch-user :rasta)) + :cards [(assoc (pulse-card-details card-1) :include_csv true, :include_xls true, :collection_id true) + (assoc (pulse-card-details card-2) :collection_id true)] + :channels [(merge pulse-channel-defaults + {:channel_type "email" + :schedule_type "daily" + :schedule_hour 12 + :recipients []})] + :collection_id true}) + (-> ((mt/user->client :rasta) :post 200 "pulse" {:name "A Pulse" + :collection_id (u/get-id collection) + :cards [{:id (u/get-id card-1) + :include_csv true + :include_xls true} + {:id (u/get-id card-2) + :include_csv false + :include_xls false}] + :channels [daily-email-channel] + :skip_if_empty false}) + pulse-response + (update :channels remove-extra-channels-fields)))))))))))) + +(deftest create-with-collection-position-test + (testing "POST /api/pulse" + (testing "Make sure we can create a Pulse with a Collection position" + (mt/with-model-cleanup [Pulse] + (letfn [(create-pulse! [expected-status-code pulse-name card collection] + (let [response ((mt/user->client :rasta) :post expected-status-code "pulse" + {:name pulse-name + :cards [{:id (u/get-id card) + :include_csv false + :include_xls false}] + :channels [daily-email-channel] + :skip_if_empty false + :collection_id (u/get-id collection) + :collection_position 1})] + (testing "response" + (is (= nil + (:errors response))))))] + (let [pulse-name (mt/random-name)] + (mt/with-temp* [Card [card] + Collection [collection]] + (card-api-test/with-cards-in-readable-collection [card] + (create-pulse! 200 pulse-name card collection) + (is (= {:collection_id (u/get-id collection), :collection_position 1} + (mt/derecordize (db/select-one [Pulse :collection_id :collection_position] :name pulse-name))))))) + + (testing "...but not if we don't have permissions for the Collection" + (mt/with-non-admin-groups-no-root-collection-perms + (let [pulse-name (mt/random-name)] + (mt/with-temp* [Card [card] + Collection [collection]] + (create-pulse! 403 pulse-name card collection) + (is (= nil + (db/select-one [Pulse :collection_id :collection_position] :name pulse-name)))))))))))) ;;; +----------------------------------------------------------------------------------------------------------------+ @@ -321,652 +309,607 @@ "`(collection_id, description, display, id, include_csv, include_xls, name)` " "2) value must be a map with the keys `id`, `include_csv`, and `include_xls`. The array cannot be empty.")}}) -(expect - {:errors {:name "value may be nil, or if non-nil, value must be a non-blank string."}} - ((user->client :rasta) :put 400 "pulse/1" {:name 123})) +(deftest update-pulse-validation-test + (testing "PUT /api/pulse/:id" + (doseq [[input expected-error] + {{:name 123} + {:errors {:name "value may be nil, or if non-nil, value must be a non-blank string."}} -(expect - default-put-card-ref-validation-error - ((user->client :rasta) :put 400 "pulse/1" {:cards 123})) + {:cards 123} + default-put-card-ref-validation-error -(expect - default-put-card-ref-validation-error - ((user->client :rasta) :put 400 "pulse/1" {:cards "foobar"})) + {:cards "foobar"} + default-put-card-ref-validation-error -(expect - default-put-card-ref-validation-error - ((user->client :rasta) :put 400 "pulse/1" {:cards ["abc"]})) + {:cards ["abc"]} + default-put-card-ref-validation-error -(expect - {:errors {:channels (str "value may be nil, or if non-nil, value must be an array. Each value must be a map. " - "The array cannot be empty.")}} - ((user->client :rasta) :put 400 "pulse/1" {:channels 123})) + {:channels 123} + {:errors {:channels (str "value may be nil, or if non-nil, value must be an array. Each value must be a map. " + "The array cannot be empty.")}} -(expect - {:errors {:channels (str "value may be nil, or if non-nil, value must be an array. Each value must be a map. " - "The array cannot be empty.")}} - ((user->client :rasta) :put 400 "pulse/1" {:channels "foobar"})) + {:channels "foobar"} + {:errors {:channels (str "value may be nil, or if non-nil, value must be an array. Each value must be a map. " + "The array cannot be empty.")}} -(expect - {:errors {:channels (str "value may be nil, or if non-nil, value must be an array. Each value must be a map. " - "The array cannot be empty.")}} - ((user->client :rasta) :put 400 "pulse/1" {:channels ["abc"]})) + {:channels ["abc"]} + {:errors {:channels (str "value may be nil, or if non-nil, value must be an array. Each value must be a map. " + "The array cannot be empty.")}}}] + (testing (pr-str input) + (is (= expected-error + ((mt/user->client :rasta) :put 400 "pulse/1" input))))))) -(tt/expect-with-temp [Pulse [pulse] - PulseChannel [pc {:pulse_id (u/get-id pulse)}] - PulseChannelRecipient [_ {:pulse_channel_id (u/get-id pc), :user_id (user->id :rasta)}] - Card [card]] - (merge - pulse-defaults - {:name "Updated Pulse" - :creator_id (user->id :rasta) - :creator (user-details (fetch-user :rasta)) - :cards [(assoc (pulse-card-details card) - :collection_id true)] - :channels [(merge pulse-channel-defaults - {:channel_type "slack" - :schedule_type "hourly" - :details {:channels "#general"} - :recipients []})] - :collection_id true}) - (with-pulses-in-writeable-collection [pulse] - (card-api-test/with-cards-in-readable-collection [card] - (-> ((user->client :rasta) :put 200 (format "pulse/%d" (u/get-id pulse)) - {:name "Updated Pulse" - :cards [{:id (u/get-id card) - :include_csv false - :include_xls false}] - :channels [{:enabled true - :channel_type "slack" - :schedule_type "hourly" - :schedule_hour 12 - :schedule_day "mon" - :recipients [] - :details {:channels "#general"}}] - :skip_if_empty false}) - pulse-response - (update :channels remove-extra-channels-fields))))) - -;; Can we add a card to an existing pulse that has a card? Specifically this will include a HybridPulseCard (the -;; original card associated with the pulse) and a CardRef (the new card) -(tt/expect-with-temp [Pulse [pulse {:name "Original Pulse Name"}] +(deftest update-test + (testing "PUT /api/pulse/:id" + (mt/with-temp* [Pulse [pulse] + PulseChannel [pc {:pulse_id (u/get-id pulse)}] + PulseChannelRecipient [_ {:pulse_channel_id (u/get-id pc), :user_id (mt/user->id :rasta)}] + Card [card]] + (with-pulses-in-writeable-collection [pulse] + (card-api-test/with-cards-in-readable-collection [card] + (is (= (merge + pulse-defaults + {:name "Updated Pulse" + :creator_id (mt/user->id :rasta) + :creator (user-details (mt/fetch-user :rasta)) + :cards [(assoc (pulse-card-details card) + :collection_id true)] + :channels [(merge pulse-channel-defaults + {:channel_type "slack" + :schedule_type "hourly" + :details {:channels "#general"} + :recipients []})] + :collection_id true}) + (-> ((mt/user->client :rasta) :put 200 (format "pulse/%d" (u/get-id pulse)) + {:name "Updated Pulse" + :cards [{:id (u/get-id card) + :include_csv false + :include_xls false}] + :channels [{:enabled true + :channel_type "slack" + :schedule_type "hourly" + :schedule_hour 12 + :schedule_day "mon" + :recipients [] + :details {:channels "#general"}}] + :skip_if_empty false}) + pulse-response + (update :channels remove-extra-channels-fields))))))))) + +(deftest add-card-to-existing-test + (testing "PUT /api/pulse/:id" + (testing "Can we add a card to an existing pulse that has a card?" + ;; Specifically this will include a HybridPulseCard (the original card associated with the pulse) and a CardRef + ;; (the new card) + (mt/with-temp* [Pulse [pulse {:name "Original Pulse Name"}] Card [card-1 {:name "Test" :description "Just Testing"}] PulseCard [_ {:card_id (u/get-id card-1) :pulse_id (u/get-id pulse)}] Card [card-2 {:name "Test2" :description "Just Testing2"}]] - (merge - pulse-defaults - {:name "Original Pulse Name" - :creator_id (user->id :rasta) - :creator (user-details (fetch-user :rasta)) - :cards (mapv (comp #(assoc % :collection_id true) pulse-card-details) [card-1 card-2]) - :channels [] - :collection_id true}) - (with-pulses-in-writeable-collection [pulse] - (card-api-test/with-cards-in-readable-collection [card-1 card-2] - ;; The FE will include the original HybridPulseCard, similar to how the API returns the card via GET - (let [pulse-cards (:cards ((user->client :rasta) :get 200 (format "pulse/%d" (u/get-id pulse))))] - (-> ((user->client :rasta) :put 200 (format "pulse/%d" (u/get-id pulse)) - {:cards (concat pulse-cards - [{:id (u/get-id card-2) - :include_csv false - :include_xls false}])}) - pulse-response - (update :channels remove-extra-channels-fields)))))) - -;; Can we update *just* the Collection ID of a Pulse? -(expect - (tt/with-temp* [Pulse [pulse] - Collection [collection]] - ((user->client :crowberto) :put 200 (str "pulse/" (u/get-id pulse)) - {:collection_id (u/get-id collection)}) - (= (db/select-one-field :collection_id Pulse :id (u/get-id pulse)) - (u/get-id collection)))) - -;; Can we change the Collection a Pulse is in (assuming we have the permissions to do so)? -(expect - (pulse-test/with-pulse-in-collection [db collection pulse] - (tt/with-temp Collection [new-collection] - ;; grant Permissions for both new and old collections - (doseq [coll [collection new-collection]] - (perms/grant-collection-readwrite-permissions! (perms-group/all-users) coll)) - ;; now make an API call to move collections - ((user->client :rasta) :put 200 (str "pulse/" (u/get-id pulse)) {:collection_id (u/get-id new-collection)}) - ;; Check to make sure the ID has changed in the DB - (= (db/select-one-field :collection_id Pulse :id (u/get-id pulse)) - (u/get-id new-collection))))) - -;; ...but if we don't have the Permissions for the old collection, we should get an Exception -(expect - "You don't have permissions to do that." - (pulse-test/with-pulse-in-collection [db collection pulse] - (tt/with-temp Collection [new-collection] - ;; grant Permissions for only the *new* collection - (perms/grant-collection-readwrite-permissions! (perms-group/all-users) new-collection) - ;; now make an API call to move collections. Should fail - ((user->client :rasta) :put 403 (str "pulse/" (u/get-id pulse)) {:collection_id (u/get-id new-collection)})))) - -;; ...and if we don't have the Permissions for the new collection, we should get an Exception -(expect - "You don't have permissions to do that." - (pulse-test/with-pulse-in-collection [db collection pulse] - (tt/with-temp Collection [new-collection] - ;; grant Permissions for only the *old* collection + (with-pulses-in-writeable-collection [pulse] + (card-api-test/with-cards-in-readable-collection [card-1 card-2] + ;; The FE will include the original HybridPulseCard, similar to how the API returns the card via GET + (let [pulse-cards (:cards ((mt/user->client :rasta) :get 200 (format "pulse/%d" (u/get-id pulse))))] + (is (= (merge + pulse-defaults + {:name "Original Pulse Name" + :creator_id (mt/user->id :rasta) + :creator (user-details (mt/fetch-user :rasta)) + :cards (mapv (comp #(assoc % :collection_id true) pulse-card-details) [card-1 card-2]) + :channels [] + :collection_id true}) + (-> ((mt/user->client :rasta) :put 200 (format "pulse/%d" (u/get-id pulse)) + {:cards (concat pulse-cards + [{:id (u/get-id card-2) + :include_csv false + :include_xls false}])}) + pulse-response + (update :channels remove-extra-channels-fields))))))))))) + +(deftest update-collection-id-test + (testing "Can we update *just* the Collection ID of a Pulse?" + (mt/with-temp* [Pulse [pulse] + Collection [collection]] + ((mt/user->client :crowberto) :put 200 (str "pulse/" (u/get-id pulse)) + {:collection_id (u/get-id collection)}) + (is (= (db/select-one-field :collection_id Pulse :id (u/get-id pulse)) + (u/get-id collection)))))) + +(deftest change-collection-test + (testing "Can we change the Collection a Pulse is in (assuming we have the permissions to do so)?" + (pulse-test/with-pulse-in-collection [db collection pulse] + (mt/with-temp Collection [new-collection] + ;; grant Permissions for both new and old collections + (doseq [coll [collection new-collection]] + (perms/grant-collection-readwrite-permissions! (perms-group/all-users) coll)) + ;; now make an API call to move collections + ((mt/user->client :rasta) :put 200 (str "pulse/" (u/get-id pulse)) {:collection_id (u/get-id new-collection)}) + ;; Check to make sure the ID has changed in the DB + (is (= (db/select-one-field :collection_id Pulse :id (u/get-id pulse)) + (u/get-id new-collection))))) + + (testing "...but if we don't have the Permissions for the old collection, we should get an Exception" + (pulse-test/with-pulse-in-collection [db collection pulse] + (mt/with-temp Collection [new-collection] + ;; grant Permissions for only the *new* collection + (perms/grant-collection-readwrite-permissions! (perms-group/all-users) new-collection) + ;; now make an API call to move collections. Should fail + (is (= "You don't have permissions to do that." + ((mt/user->client :rasta) :put 403 (str "pulse/" (u/get-id pulse)) {:collection_id (u/get-id new-collection)})))))) + + (testing "...and if we don't have the Permissions for the new collection, we should get an Exception" + (pulse-test/with-pulse-in-collection [db collection pulse] + (mt/with-temp Collection [new-collection] + ;; grant Permissions for only the *old* collection + (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) + ;; now make an API call to move collections. Should fail + (is (= "You don't have permissions to do that." + ((mt/user->client :rasta) :put 403 (str "pulse/" (u/get-id pulse)) {:collection_id (u/get-id new-collection)})))))))) + +(deftest update-collection-position-test + (testing "Can we change the Collection position of a Pulse?" + (pulse-test/with-pulse-in-collection [_ collection pulse] (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - ;; now make an API call to move collections. Should fail - ((user->client :rasta) :put 403 (str "pulse/" (u/get-id pulse)) {:collection_id (u/get-id new-collection)})))) - -;; Can we change the Collection position of a Pulse? -(expect - 1 - (pulse-test/with-pulse-in-collection [_ collection pulse] - (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - ((user->client :rasta) :put 200 (str "pulse/" (u/get-id pulse)) - {:collection_position 1}) - (db/select-one-field :collection_position Pulse :id (u/get-id pulse)))) - -;; ...and unset (unpin) it as well? -(expect - nil - (pulse-test/with-pulse-in-collection [_ collection pulse] - (db/update! Pulse (u/get-id pulse) :collection_position 1) - (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - ((user->client :rasta) :put 200 (str "pulse/" (u/get-id pulse)) - {:collection_position nil}) - (db/select-one-field :collection_position Pulse :id (u/get-id pulse)))) - -;; ...we shouldn't be able to if we don't have permissions for the Collection -(expect - nil - (pulse-test/with-pulse-in-collection [_ collection pulse] - ((user->client :rasta) :put 403 (str "pulse/" (u/get-id pulse)) - {:collection_position 1}) - (db/select-one-field :collection_position Pulse :id (u/get-id pulse)))) - -(expect - 1 - (pulse-test/with-pulse-in-collection [_ collection pulse] - (db/update! Pulse (u/get-id pulse) :collection_position 1) - ((user->client :rasta) :put 403 (str "pulse/" (u/get-id pulse)) - {:collection_position nil}) - (db/select-one-field :collection_position Pulse :id (u/get-id pulse)))) - -;; Can we archive a Pulse? -(expect - (pulse-test/with-pulse-in-collection [_ collection pulse] - (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - ((user->client :rasta) :put 200 (str "pulse/" (u/get-id pulse)) - {:archived true}) - (db/select-one-field :archived Pulse :id (u/get-id pulse)))) - -;; Can we unarchive a Pulse? -(expect - false - (pulse-test/with-pulse-in-collection [_ collection pulse] - (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - (db/update! Pulse (u/get-id pulse) :archived true) - ((user->client :rasta) :put 200 (str "pulse/" (u/get-id pulse)) - {:archived false}) - (db/select-one-field :archived Pulse :id (u/get-id pulse)))) - -;; Does unarchiving a Pulse affect its Cards & Recipients? It shouldn't. This should behave as a PATCH-style endpoint! -(expect - (tu/with-non-admin-groups-no-root-collection-perms - (tt/with-temp* [Collection [collection] - Pulse [pulse {:collection_id (u/get-id collection)}] - PulseChannel [pc {:pulse_id (u/get-id pulse)}] - PulseChannelRecipient [pcr {:pulse_channel_id (u/get-id pc), :user_id (user->id :rasta)}] - Card [card]] + ((mt/user->client :rasta) :put 200 (str "pulse/" (u/get-id pulse)) + {:collection_position 1}) + (is (= 1 + (db/select-one-field :collection_position Pulse :id (u/get-id pulse))))) + + (testing "...and unset (unpin) it as well?" + (pulse-test/with-pulse-in-collection [_ collection pulse] + (db/update! Pulse (u/get-id pulse) :collection_position 1) + (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) + ((mt/user->client :rasta) :put 200 (str "pulse/" (u/get-id pulse)) + {:collection_position nil}) + (is (= nil + (db/select-one-field :collection_position Pulse :id (u/get-id pulse)))))) + + (testing "...we shouldn't be able to if we don't have permissions for the Collection" + (pulse-test/with-pulse-in-collection [_ collection pulse] + ((mt/user->client :rasta) :put 403 (str "pulse/" (u/get-id pulse)) + {:collection_position 1}) + (is (= nil + (db/select-one-field :collection_position Pulse :id (u/get-id pulse)))) + + (testing "shouldn't be able to unset (unpin) a Pulse" + (db/update! Pulse (u/get-id pulse) :collection_position 1) + ((mt/user->client :rasta) :put 403 (str "pulse/" (u/get-id pulse)) + {:collection_position nil}) + (is (= 1 + (db/select-one-field :collection_position Pulse :id (u/get-id pulse))))))))) + +(deftest archive-test + (testing "Can we archive a Pulse?" + (pulse-test/with-pulse-in-collection [_ collection pulse] (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - ((user->client :rasta) :put 200 (str "pulse/" (u/get-id pulse)) + ((mt/user->client :rasta) :put 200 (str "pulse/" (u/get-id pulse)) {:archived true}) - ((user->client :rasta) :put 200 (str "pulse/" (u/get-id pulse)) + (is (= true + (db/select-one-field :archived Pulse :id (u/get-id pulse))))))) + +(deftest unarchive-test + (testing "Can we unarchive a Pulse?" + (pulse-test/with-pulse-in-collection [_ collection pulse] + (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) + (db/update! Pulse (u/get-id pulse) :archived true) + ((mt/user->client :rasta) :put 200 (str "pulse/" (u/get-id pulse)) {:archived false}) - (and (db/exists? PulseChannel :id (u/get-id pc)) - (db/exists? PulseChannelRecipient :id (u/get-id pcr)))))) + (is (= false + (db/select-one-field :archived Pulse :id (u/get-id pulse)))))) + + (testing "Does unarchiving a Pulse affect its Cards & Recipients? It shouldn't. This should behave as a PATCH-style endpoint!" + (mt/with-non-admin-groups-no-root-collection-perms + (mt/with-temp* [Collection [collection] + Pulse [pulse {:collection_id (u/get-id collection)}] + PulseChannel [pc {:pulse_id (u/get-id pulse)}] + PulseChannelRecipient [pcr {:pulse_channel_id (u/get-id pc), :user_id (mt/user->id :rasta)}] + Card [card]] + (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) + ((mt/user->client :rasta) :put 200 (str "pulse/" (u/get-id pulse)) + {:archived true}) + ((mt/user->client :rasta) :put 200 (str "pulse/" (u/get-id pulse)) + {:archived false}) + (is (db/exists? PulseChannel :id (u/get-id pc))) + (is (db/exists? PulseChannelRecipient :id (u/get-id pcr))))))) ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | UPDATING PULSE COLLECTION POSITIONS | ;;; +----------------------------------------------------------------------------------------------------------------+ -;; Check that we can update a pulse's position in a collection only pulses -(expect - {"d" 1 - "a" 2 - "b" 3 - "c" 4} - (tu/with-non-admin-groups-no-root-collection-perms - (tt/with-temp Collection [{coll-id :id :as collection}] - (card-api-test/with-ordered-items collection [Pulse a - Pulse b - Pulse c - Pulse d] - (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - ((user->client :rasta) :put 200 (str "pulse/" (u/get-id d)) - {:collection_position 1}) - (card-api-test/get-name->collection-position :rasta coll-id))))) - -;; Change the position of b to 4, will dec c and d -(expect - {"a" 1 - "c" 2 - "d" 3 - "b" 4} - (tu/with-non-admin-groups-no-root-collection-perms - (tt/with-temp Collection [{coll-id :id :as collection}] - (card-api-test/with-ordered-items collection [Card a - Pulse b - Card c - Dashboard d] - (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - ((user->client :rasta) :put 200 (str "pulse/" (u/get-id b)) - {:collection_position 4}) - (card-api-test/get-name->collection-position :rasta coll-id))))) - -;; Change the position of d to the 2, should inc b and c -(expect - {"a" 1 - "d" 2 - "b" 3 - "c" 4} - (tu/with-non-admin-groups-no-root-collection-perms - (tt/with-temp Collection [{coll-id :id :as collection}] - (card-api-test/with-ordered-items collection [Card a - Card b - Dashboard c - Pulse d] - (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - ((user->client :rasta) :put 200 (str "pulse/" (u/get-id d)) - {:collection_position 2}) - (card-api-test/get-name->collection-position :rasta coll-id))))) - -;; Change the position of a to the 4th, will decrement all existing items -(expect - {"b" 1 - "c" 2 - "d" 3 - "a" 4} - (tu/with-non-admin-groups-no-root-collection-perms - (tt/with-temp Collection [{coll-id :id :as collection}] - (card-api-test/with-ordered-items collection [Pulse a - Dashboard b - Card c - Pulse d] - (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - ((user->client :rasta) :put 200 (str "pulse/" (u/get-id a)) - {:collection_position 4}) - (card-api-test/get-name->collection-position :rasta coll-id))))) - -;; Change the position of the d to the 1st, will increment all existing items -(expect - {"d" 1 - "a" 2 - "b" 3 - "c" 4} - (tu/with-non-admin-groups-no-root-collection-perms - (tt/with-temp Collection [{coll-id :id :as collection}] - (card-api-test/with-ordered-items collection [Dashboard a - Dashboard b - Card c - Pulse d] - (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - ((user->client :rasta) :put 200 (str "pulse/" (u/get-id d)) - {:collection_position 1}) - (card-api-test/get-name->collection-position :rasta coll-id))))) - -;; Check that no position change, but changing collections still triggers a fixup of both collections -;; Moving `c` from collection-1 to collection-2, `c` is now at position 3 in collection 2 -(expect - [{"a" 1 - "b" 2 - "d" 3} - {"e" 1 - "f" 2 - "c" 3 - "g" 4 - "h" 5}] - (tu/with-non-admin-groups-no-root-collection-perms - (tt/with-temp* [Collection [collection-1] - Collection [collection-2]] - (card-api-test/with-ordered-items collection-1 [Pulse a - Card b - Pulse c - Dashboard d] - (card-api-test/with-ordered-items collection-2 [Card e - Card f - Dashboard g - Dashboard h] - (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection-1) - (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection-2) - ((user->client :rasta) :put 200 (str "pulse/" (u/get-id c)) - {:collection_id (u/get-id collection-2)}) - [(card-api-test/get-name->collection-position :rasta (u/get-id collection-1)) - (card-api-test/get-name->collection-position :rasta (u/get-id collection-2))]))))) - -;; Check that moving a pulse to another collection, with a changed position will fixup both collections -;; Moving `b` to collection 2, giving it a position of 1 -(expect - [{"a" 1 - "c" 2 - "d" 3} - {"b" 1 - "e" 2 - "f" 3 - "g" 4 - "h" 5}] - (tu/with-non-admin-groups-no-root-collection-perms - (tt/with-temp* [Collection [collection-1] - Collection [collection-2]] - (card-api-test/with-ordered-items collection-1 [Pulse a - Pulse b - Dashboard c - Card d] - (card-api-test/with-ordered-items collection-2 [Card e - Card f - Pulse g - Dashboard h] - (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection-1) - (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection-2) - ((user->client :rasta) :put 200 (str "pulse/" (u/get-id b)) - {:collection_id (u/get-id collection-2), :collection_position 1}) - [(card-api-test/get-name->collection-position :rasta (u/get-id collection-1)) - (card-api-test/get-name->collection-position :rasta (u/get-id collection-2))]))))) - -;; Add a new pulse at position 2, causing existing pulses to be incremented -(expect - [{"a" 1 - "c" 2 - "d" 3} - {"a" 1 - "b" 2 - "c" 3 - "d" 4}] - (tu/with-non-admin-groups-no-root-collection-perms - (tt/with-temp* [Collection [{coll-id :id :as collection}] - Card [card-1]] - (card-api-test/with-cards-in-readable-collection [card-1] - (card-api-test/with-ordered-items collection [Card a - Dashboard c - Pulse d] - (tu/with-model-cleanup [Pulse] - (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - [(card-api-test/get-name->collection-position :rasta coll-id) - (do ((user->client :rasta) :post 200 "pulse" {:name "b" - :collection_id (u/get-id collection) - :cards [{:id (u/get-id card-1) - :include_csv false - :include_xls false}] - :channels [daily-email-channel] - :skip_if_empty false - :collection_position 2}) - (card-api-test/get-name->collection-position :rasta coll-id))])))))) - -;; Add a new pulse without a position, should leave existing positions unchanged -(expect - [{"a" 1 - "c" 2 - "d" 3} - {"a" 1 - "b" nil - "c" 2 - "d" 3}] - (tu/with-non-admin-groups-no-root-collection-perms - (tt/with-temp* [Collection [{coll-id :id :as collection}] - Card [card-1]] - (card-api-test/with-cards-in-readable-collection [card-1] - (card-api-test/with-ordered-items collection [Pulse a - Card c - Dashboard d] - (tu/with-model-cleanup [Pulse] - (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - [(card-api-test/get-name->collection-position :rasta coll-id) - (do ((user->client :rasta) :post 200 "pulse" {:name "b" - :collection_id (u/get-id collection) - :cards [{:id (u/get-id card-1) - :include_csv false - :include_xls false}] - :channels [daily-email-channel] - :skip_if_empty false}) - (card-api-test/get-name->collection-position :rasta coll-id))])))))) +(defmulti ^:private move-pulse-test-action + {:arglists '([action context & args])} + (fn [action & _] + action)) + +(defmethod move-pulse-test-action :move + [_ context pulse & {:keys [collection position]}] + (let [pulse (get-in context [:pulse pulse]) + response ((mt/user->client :rasta) :put 200 (str "pulse/" (u/get-id pulse)) + (merge + (when collection + {:collection_id (u/get-id (get-in context [:collection collection]))}) + (when position + {:collection_position position})))] + (is (= nil + (:errors response))))) + +(defmethod move-pulse-test-action :insert-pulse + [_ context collection & {:keys [position]}] + (let [collection (get-in context [:collection collection]) + response ((mt/user->client :rasta) :post 200 "pulse" + (merge + {:name "x" + :collection_id (u/get-id collection) + :cards [{:id (u/get-id (get-in context [:card 1])) + :include_csv false + :include_xls false}] + :channels [daily-email-channel] + :skip_if_empty false} + (when position + {:collection_position position})))] + (is (= nil + (:errors response))))) + +(def ^:private move-test-definitions + [{:message "Check that we can update a Pulse's position in a Collection" + :action [:move :d :position 1] + :expected {"d" 1 + "a" 2 + "b" 3 + "c" 4}} + {:message "Change the position of b to 4, will dec c and d" + :action [:move :b :position 4] + :expected {"a" 1 + "c" 2 + "d" 3 + "b" 4}} + {:message "Change the position of d to 2, should inc b and c" + :action [:move :d :position 2] + :expected {"a" 1 + "d" 2 + "b" 3 + "c" 4}} + {:message "Change the position of a to 4th, will decrement all existing items" + :action [:move :a :position 4] + :expected {"b" 1 + "c" 2 + "d" 3 + "a" 4}} + {:message "Change the position of the d to the 1st, will increment all existing items" + :action [:move :d :position 1] + :expected {"d" 1 + "a" 2 + "b" 3 + "c" 4}} + {:message (str "Check that no position change, but changing collections still triggers a fixup of both " + "collections Moving `c` from collection-1 to collection-2, `c` is now at position 3 in " + "collection 2") + :action [:move :c :collection 2] + :expected [{"a" 1 + "b" 2 + "d" 3} + {"e" 1 + "f" 2 + "c" 3 + "g" 4 + "h" 5}]} + {:message (str "Check that moving a pulse to another collection, with a changed position will fixup " + "both collections Moving `b` to collection 2, giving it a position of 1") + :action [:move :b :collection 2, :position 1] + :expected [{"a" 1 + "c" 2 + "d" 3} + {"b" 1 + "e" 2 + "f" 3 + "g" 4 + "h" 5}]} + {:message "Add a new pulse at position 2, causing existing pulses to be incremented" + :action [:insert-pulse 1 :position 2] + :expected {"a" 1 + "x" 2 + "b" 3 + "c" 4 + "d" 5}} + + {:message "Add a new pulse without a position, should leave existing positions unchanged" + :action [:insert-pulse 1] + :expected {"x" nil + "a" 1 + "b" 2 + "c" 3 + "d" 4}}]) + +(deftest move-pulse-test + (testing "PUT /api/pulse/:id" + (doseq [{:keys [message action expected]} move-test-definitions + :let [expected (if (map? expected) [expected] expected)]] + (testing (str "\n" message) + (mt/with-temp* [Collection [collection-1] + Collection [collection-2] + Card [card-1]] + (card-api-test/with-ordered-items collection-1 [Pulse a + Pulse b + Pulse c + Pulse d] + (card-api-test/with-ordered-items collection-2 [Card e + Card f + Dashboard g + Dashboard h] + (let [[action & args] action + context {:pulse {:a a, :b b, :c c, :d d, :e e, :f f, :g g, :h h} + :collection {1 collection-1, 2 collection-2} + :card {1 card-1}}] + (testing (str "\n" (pr-str (cons action args))) + (apply move-pulse-test-action action context args))) + (testing "\nPositions after actions for" + (testing "Collection 1" + (is (= (first expected) + (card-api-test/get-name->collection-position :rasta (u/get-id collection-1))))) + (when (second expected) + (testing "Collection 2" + (is (= (second expected) + (card-api-test/get-name->collection-position :rasta (u/get-id collection-2)))))))))))))) ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | DELETE /api/pulse/:id | ;;; +----------------------------------------------------------------------------------------------------------------+ -;; check that a regular user can delete a Pulse if they have write permissions for its collection (!) -(expect - nil - (tt/with-temp* [Pulse [pulse] - PulseChannel [pc {:pulse_id (u/get-id pulse)}] - PulseChannelRecipient [_ {:pulse_channel_id (u/get-id pc), :user_id (user->id :rasta)}]] - (with-pulses-in-writeable-collection [pulse] - ((user->client :rasta) :delete 204 (format "pulse/%d" (u/get-id pulse))) - (pulse/retrieve-pulse (u/get-id pulse))))) - -;; Check that a rando (e.g. someone without collection write access) isn't allowed to delete a pulse -(expect - "You don't have permissions to do that." - (tt/with-temp* [Database [db (select-keys (data/db) [:engine :details])] - Table [table (-> (Table (data/id :venues)) - (dissoc :id) - (assoc :db_id (u/get-id db)))] - Card [card {:dataset_query {:database (u/get-id db) - :type "query" - :query {:source-table (u/get-id table) - :aggregation [[:count]]}}}] - Pulse [pulse {:name "Daily Sad Toucans"}] - PulseCard [_ {:pulse_id (u/get-id pulse), :card_id (u/get-id card)}]] - (with-pulses-in-readable-collection [pulse] - ;; revoke permissions for default group to this database - (perms/revoke-permissions! (perms-group/all-users) (u/get-id db)) - ;; now a user without permissions to the Card in question should *not* be allowed to delete the pulse - ((user->client :rasta) :delete 403 (format "pulse/%d" (u/get-id pulse)))))) +(deftest delete-test + (testing "DELETE /api/pulse/:id" + (testing "check that a regular user can delete a Pulse if they have write permissions for its collection (!)" + (mt/with-temp* [Pulse [pulse] + PulseChannel [pc {:pulse_id (u/get-id pulse)}] + PulseChannelRecipient [_ {:pulse_channel_id (u/get-id pc), :user_id (mt/user->id :rasta)}]] + (with-pulses-in-writeable-collection [pulse] + ((mt/user->client :rasta) :delete 204 (format "pulse/%d" (u/get-id pulse))) + (is (= nil + (pulse/retrieve-pulse (u/get-id pulse))))))) + + (testing "check that a rando (e.g. someone without collection write access) isn't allowed to delete a pulse" + (mt/with-temp-copy-of-db + (mt/with-temp* [Card [card {:dataset_query {:database (mt/id) + :type "query" + :query {:source-table (mt/id :venues) + :aggregation [[:count]]}}}] + Pulse [pulse {:name "Daily Sad Toucans"}] + PulseCard [_ {:pulse_id (u/get-id pulse), :card_id (u/get-id card)}]] + (with-pulses-in-readable-collection [pulse] + ;; revoke permissions for default group to this database + (perms/revoke-permissions! (perms-group/all-users) (mt/id)) + ;; now a user without permissions to the Card in question should *not* be allowed to delete the pulse + (is (= "You don't have permissions to do that." + ((mt/user->client :rasta) :delete 403 (format "pulse/%d" (u/get-id pulse))))))))))) ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | GET /api/pulse | ;;; +----------------------------------------------------------------------------------------------------------------+ -;; should come back in alphabetical order -(tt/expect-with-temp [Pulse [pulse-1 {:name "ABCDEF"}] - Pulse [pulse-2 {:name "GHIJKL"}]] - [(assoc (pulse-details pulse-1) :can_write false, :collection_id true) - (assoc (pulse-details pulse-2) :can_write false, :collection_id true)] - (with-pulses-in-readable-collection [pulse-1 pulse-2] - ;; delete anything else in DB just to be sure; this step may not be necessary any more - (db/delete! Pulse :id [:not-in #{(u/get-id pulse-1) - (u/get-id pulse-2)}]) - (for [pulse ((user->client :rasta) :get 200 "pulse")] - (update pulse :collection_id boolean)))) - -;; `can_write` property should get updated correctly based on whether current user can write -(tt/expect-with-temp [Pulse [pulse-1 {:name "ABCDEF"}] - Pulse [pulse-2 {:name "GHIJKL"}]] - [(assoc (pulse-details pulse-1) :can_write true) - (assoc (pulse-details pulse-2) :can_write true)] - (do - ;; delete anything else in DB just to be sure; this step may not be necessary any more - (db/delete! Pulse :id [:not-in #{(u/get-id pulse-1) - (u/get-id pulse-2)}]) - ((user->client :crowberto) :get 200 "pulse"))) - -;; should not return alerts -(tt/expect-with-temp [Pulse [pulse-1 {:name "ABCDEF"}] +(deftest list-test + (testing "GET /api/pulse" + (mt/with-temp* [Pulse [pulse-1 {:name "ABCDEF"}] + Pulse [pulse-2 {:name "GHIJKL"}]] + (testing "should come back in alphabetical order" + (with-pulses-in-readable-collection [pulse-1 pulse-2] + ;; delete anything else in DB just to be sure; this step may not be necessary any more + (db/delete! Pulse :id [:not-in #{(u/get-id pulse-1) + (u/get-id pulse-2)}]) + (is (= [(assoc (pulse-details pulse-1) :can_write false, :collection_id true) + (assoc (pulse-details pulse-2) :can_write false, :collection_id true)] + (for [pulse ((mt/user->client :rasta) :get 200 "pulse")] + (update pulse :collection_id boolean)))))) + + (testing "`can_write` property should get updated correctly based on whether current user can write" + ;; delete anything else in DB just to be sure; this step may not be necessary any more + (db/delete! Pulse :id [:not-in #{(u/get-id pulse-1) + (u/get-id pulse-2)}]) + (is (= [(assoc (pulse-details pulse-1) :can_write true) + (assoc (pulse-details pulse-2) :can_write true)] + ((mt/user->client :crowberto) :get 200 "pulse"))))) + + (testing "should not return alerts" + (mt/with-temp* [Pulse [pulse-1 {:name "ABCDEF"}] Pulse [pulse-2 {:name "GHIJKL"}] Pulse [pulse-3 {:name "AAAAAA" :alert_condition "rows"}]] - [(assoc (pulse-details pulse-1) :can_write false, :collection_id true) - (assoc (pulse-details pulse-2) :can_write false, :collection_id true)] - (with-pulses-in-readable-collection [pulse-1 pulse-2 pulse-3] - (for [pulse ((user->client :rasta) :get 200 "pulse")] - (update pulse :collection_id boolean)))) - -;; by default, archived Pulses should be excluded -(expect - #{"Not Archived"} - (tt/with-temp* [Pulse [not-archived-pulse {:name "Not Archived"}] - Pulse [archived-pulse {:name "Archived", :archived true}]] - (with-pulses-in-readable-collection [not-archived-pulse archived-pulse] - (set (map :name ((user->client :rasta) :get 200 "pulse")))))) - -;; can we fetch archived Pulses? -(expect - #{"Archived"} - (tt/with-temp* [Pulse [not-archived-pulse {:name "Not Archived"}] - Pulse [archived-pulse {:name "Archived", :archived true}]] - (with-pulses-in-readable-collection [not-archived-pulse archived-pulse] - (set (map :name ((user->client :rasta) :get 200 "pulse?archived=true")))))) + (with-pulses-in-readable-collection [pulse-1 pulse-2 pulse-3] + (is (= [(assoc (pulse-details pulse-1) :can_write false, :collection_id true) + (assoc (pulse-details pulse-2) :can_write false, :collection_id true)] + (for [pulse ((mt/user->client :rasta) :get 200 "pulse")] + (update pulse :collection_id boolean))))))) + + (testing "by default, archived Pulses should be excluded" + (mt/with-temp* [Pulse [not-archived-pulse {:name "Not Archived"}] + Pulse [archived-pulse {:name "Archived", :archived true}]] + (with-pulses-in-readable-collection [not-archived-pulse archived-pulse] + (is (= #{"Not Archived"} + (set (map :name ((mt/user->client :rasta) :get 200 "pulse")))))))) + + (testing "can we fetch archived Pulses?" + (mt/with-temp* [Pulse [not-archived-pulse {:name "Not Archived"}] + Pulse [archived-pulse {:name "Archived", :archived true}]] + (with-pulses-in-readable-collection [not-archived-pulse archived-pulse] + (is (= #{"Archived"} + (set (map :name ((mt/user->client :rasta) :get 200 "pulse?archived=true")))))))))) ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | GET /api/pulse/:id | ;;; +----------------------------------------------------------------------------------------------------------------+ -(tt/expect-with-temp [Pulse [pulse]] - (assoc (pulse-details pulse) - :can_write false - :collection_id true) - (with-pulses-in-readable-collection [pulse] - (-> ((user->client :rasta) :get 200 (str "pulse/" (u/get-id pulse))) - (update :collection_id boolean)))) +(deftest get-pulse-test + (testing "GET /api/pulse/:id" + (mt/with-temp Pulse [pulse] + (with-pulses-in-readable-collection [pulse] + (is (= (assoc (pulse-details pulse) + :can_write false + :collection_id true) + (-> ((mt/user->client :rasta) :get 200 (str "pulse/" (u/get-id pulse))) + (update :collection_id boolean)))))) -;; Should 404 for an Alert -(expect - "Not found." - (tt/with-temp Pulse [{pulse-id :id} {:alert_condition "rows"}] - (with-pulses-in-readable-collection [pulse-id] - ((user->client :rasta) :get 404 (str "pulse/" pulse-id))))) + (testing "should 404 for an Alert" + (mt/with-temp Pulse [{pulse-id :id} {:alert_condition "rows"}] + (is (= "Not found." + (with-pulses-in-readable-collection [pulse-id] + ((mt/user->client :rasta) :get 404 (str "pulse/" pulse-id))))))))) ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | POST /api/pulse/test | ;;; +----------------------------------------------------------------------------------------------------------------+ -(deftest create-pulse-test - (tu/with-non-admin-groups-no-root-collection-perms - (tu/with-model-cleanup [Pulse] - (et/with-fake-inbox - (data/dataset sad-toucan-incidents - (tt/with-temp* [Collection [collection] - Card [card {:dataset_query {:database (data/id) - :type "query" - :query {:source-table (data/id :incidents) - :aggregation [[:count]]}}}]] +(deftest send-test-pulse-test + (testing "POST /api/pulse/test" + (mt/with-non-admin-groups-no-root-collection-perms + (mt/with-fake-inbox + (mt/dataset sad-toucan-incidents + (mt/with-temp* [Collection [collection] + Card [card {:dataset_query (mt/mbql-query incidents {:aggregation [[:count]]})}]] (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) (card-api-test/with-cards-in-readable-collection [card] (is (= {:ok true} - ((user->client :rasta) :post 200 "pulse/test" {:name "Daily Sad Toucans" - :collection_id (u/get-id collection) - :cards [{:id (u/get-id card) - :include_csv false - :include_xls false}] - :channels [{:enabled true - :channel_type "email" - :schedule_type "daily" - :schedule_hour 12 - :schedule_day nil - :recipients [(fetch-user :rasta)]}] - :skip_if_empty false}))) - (is (= (et/email-to :rasta {:subject "Pulse: Daily Sad Toucans" + ((mt/user->client :rasta) :post 200 "pulse/test" {:name "Daily Sad Toucans" + :collection_id (u/get-id collection) + :cards [{:id (u/get-id card) + :include_csv false + :include_xls false}] + :channels [{:enabled true + :channel_type "email" + :schedule_type "daily" + :schedule_hour 12 + :schedule_day nil + :recipients [(mt/fetch-user :rasta)]}] + :skip_if_empty false}))) + (is (= (mt/email-to :rasta {:subject "Pulse: Daily Sad Toucans" :body {"Daily Sad Toucans" true}}) - (et/regex-email-bodies #"Daily Sad Toucans")))))))))) + (mt/regex-email-bodies #"Daily Sad Toucans")))))))))) ;; This test follows a flow that the user/UI would follow by first creating a pulse, then making a small change to ;; that pulse and testing it. The primary purpose of this test is to ensure tha the pulse/test endpoint accepts data ;; of the same format that the pulse GET returns -(tt/expect-with-temp [Card [card-1 {:dataset_query - {:database (data/id), :type :query, :query {:source-table (data/id :venues)}}}] - Card [card-2 {:dataset_query - {:database (data/id), :type :query, :query {:source-table (data/id :venues)}}}]] - {:response {:ok true} - :emails (et/email-to :rasta {:subject "Pulse: A Pulse" - :body {"A Pulse" true}})} - (card-api-test/with-cards-in-readable-collection [card-1 card-2] - (et/with-fake-inbox - (tu/with-model-cleanup [Pulse] - (tt/with-temp Collection [collection] - (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) - ;; First create the pulse - (let [{pulse-id :id} ((user->client :rasta) :post 200 "pulse" - {:name "A Pulse" - :collection_id (u/get-id collection) - :skip_if_empty false - :cards [{:id (u/get-id card-1) - :include_csv false - :include_xls false} - {:id (u/get-id card-2) - :include_csv false - :include_xls false}] - - :channels [(assoc daily-email-channel :recipients [(fetch-user :rasta) - (fetch-user :crowberto)])]}) - ;; Retrieve the pulse via GET - result ((user->client :rasta) :get 200 (str "pulse/" pulse-id)) - ;; Change our fetched copy of the pulse to only have Rasta for the recipients - email-channel (assoc (-> result :channels first) :recipients [(fetch-user :rasta)])] - ;; Don't update the pulse, but test the pulse with the updated recipients - {:response ((user->client :rasta) :post 200 "pulse/test" (assoc result :channels [email-channel])) - :emails (et/regex-email-bodies #"A Pulse")})))))) - -;; A Card saved with `:async?` true should not be ran async for a Pulse -(expect - map? - (#'pulse-api/pulse-card-query-results - {:id 1 - :dataset_query {:database (data/id) - :type :query - :query {:source-table (data/id :venues) - :limit 1} - :async? true}})) +(deftest update-flow-test + (mt/with-temp* [Card [card-1 {:dataset_query + {:database (mt/id), :type :query, :query {:source-table (mt/id :venues)}}}] + Card [card-2 {:dataset_query + {:database (mt/id), :type :query, :query {:source-table (mt/id :venues)}}}]] + + (card-api-test/with-cards-in-readable-collection [card-1 card-2] + (mt/with-fake-inbox + (mt/with-model-cleanup [Pulse] + (mt/with-temp Collection [collection] + (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) + ;; First create the pulse + (let [{pulse-id :id} ((mt/user->client :rasta) :post 200 "pulse" + {:name "A Pulse" + :collection_id (u/get-id collection) + :skip_if_empty false + :cards [{:id (u/get-id card-1) + :include_csv false + :include_xls false} + {:id (u/get-id card-2) + :include_csv false + :include_xls false}] + + :channels [(assoc daily-email-channel :recipients [(mt/fetch-user :rasta) + (mt/fetch-user :crowberto)])]}) + ;; Retrieve the pulse via GET + result ((mt/user->client :rasta) :get 200 (str "pulse/" pulse-id)) + ;; Change our fetched copy of the pulse to only have Rasta for the recipients + email-channel (assoc (-> result :channels first) :recipients [(mt/fetch-user :rasta)])] + ;; Don't update the pulse, but test the pulse with the updated recipients + (is (= {:ok true} + ((mt/user->client :rasta) :post 200 "pulse/test" (assoc result :channels [email-channel])))) + (is (= (mt/email-to :rasta {:subject "Pulse: A Pulse" + :body {"A Pulse" true}}) + (mt/regex-email-bodies #"A Pulse")))))))))) + +(deftest dont-run-cards-async-test + (testing "A Card saved with `:async?` true should not be ran async for a Pulse" + (is (map? (#'pulse-api/pulse-card-query-results + {:id 1 + :dataset_query {:database (mt/id) + :type :query + :query {:source-table (mt/id :venues) + :limit 1} + :async? true}}))))) ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | GET /api/pulse/form_input | ;;; +----------------------------------------------------------------------------------------------------------------+ -;; Check that Slack channels come back when configured -(expect - [{:name "channel", :type "select", :displayName "Post to", :options ["#foo" "@bar"], :required true}] - (tu/with-temporary-setting-values [slack-token "something"] - (with-redefs [slack/channels-list (constantly [{:name "foo"}]) - slack/users-list (constantly [{:name "bar"}])] - (-> ((user->client :rasta) :get 200 "pulse/form_input") - (get-in [:channels :slack :fields]))))) - -;; When slack is not configured, `form_input` returns just the #genreal slack channel -(expect - [{:name "channel", :type "select", :displayName "Post to", :options ["#general"], :required true}] - (tu/with-temporary-setting-values [slack-token nil] - (-> ((user->client :rasta) :get 200 "pulse/form_input") - (get-in [:channels :slack :fields])))) +(deftest form-input-test + (testing "GET /api/pulse/form_input" + (testing "Check that Slack channels come back when configured" + (mt/with-temporary-setting-values [slack-token "something"] + (with-redefs [slack/channels-list (constantly [{:name "foo"}]) + slack/users-list (constantly [{:name "bar"}])] + (is (= [{:name "channel", :type "select", :displayName "Post to", :options ["#foo" "@bar"], :required true}] + (-> ((mt/user->client :rasta) :get 200 "pulse/form_input") + (get-in [:channels :slack :fields]))))))) + + (testing "When slack is not configured, `form_input` returns just the #genreal slack channel" + (mt/with-temporary-setting-values [slack-token nil] + (is (= [{:name "channel", :type "select", :displayName "Post to", :options ["#general"], :required true}] + (-> ((mt/user->client :rasta) :get 200 "pulse/form_input") + (get-in [:channels :slack :fields])))))))) + + +;;; +----------------------------------------------------------------------------------------------------------------+ +;;; | GET /api/pulse/preview_card/:id | +;;; +----------------------------------------------------------------------------------------------------------------+ + +(deftest preview-pulse-test + (testing "GET /api/pulse/preview_card/:id" + (mt/with-temp* [Collection [collection] + Card [card {:dataset_query (mt/mbql-query checkins {:limit 5})}]] + (letfn [(preview [expected-status-code] + (http/client-full-response (mt/user->credentials :rasta) + :get expected-status-code (format "pulse/preview_card_png/%d" (u/get-id card))))] + (testing "Should be able to preview a Pulse" + (let [{{:strs [Content-Type]} :headers, :keys [body]} (preview 200)] + (is (= "image/png" + Content-Type)) + (is (some? body)))) + + (testing "If rendering a Pulse fails (e.g. because font registration failed) the endpoint should return the error message" + (with-redefs [png/register-fonts-if-needed! (fn [] + (throw (ex-info "Can't register fonts!" + {} + (NullPointerException.))))] + (let [{{:strs [Content-Type]} :headers, :keys [body]} (preview 500)] + (is (= "application/json;charset=utf-8" + Content-Type)) + (is (schema= {:message (s/eq "Can't register fonts!") + :trace s/Any + :via s/Any + s/Keyword s/Any} + body))))))))) + ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | DELETE /api/pulse/:pulse-id/subscription | ;;; +----------------------------------------------------------------------------------------------------------------+ -(expect - nil - (tt/with-temp* [Pulse [{pulse-id :id} {:name "Lodi Dodi" :creator_id (user->id :crowberto)}] - PulseChannel [{channel-id :id} {:pulse_id pulse-id - :channel_type "email" - :schedule_type "daily" - :details {:other "stuff" - :emails ["foo@bar.com"]}}] - PulseChannelRecipient [pcr {:pulse_channel_id channel-id :user_id (user->id :rasta)}]] - ((user->client :rasta) :delete 204 (str "pulse/" pulse-id "/subscription/email")))) - -;; Users can't delete someone else's pulse subscription -(expect - "Not found." - (tt/with-temp* [Pulse [{pulse-id :id} {:name "Lodi Dodi" :creator_id (user->id :crowberto)}] - PulseChannel [{channel-id :id} {:pulse_id pulse-id - :channel_type "email" - :schedule_type "daily" - :details {:other "stuff" - :emails ["foo@bar.com"]}}] - PulseChannelRecipient [pcr {:pulse_channel_id channel-id :user_id (user->id :rasta)}]] - ((user->client :lucky) :delete 404 (str "pulse/" pulse-id "/subscription/email")))) +(deftest delete-subscription-test + (testing "DELETE /api/pulse/:pulse-id/subscription" + (mt/with-temp* [Pulse [{pulse-id :id} {:name "Lodi Dodi" :creator_id (mt/user->id :crowberto)}] + PulseChannel [{channel-id :id} {:pulse_id pulse-id + :channel_type "email" + :schedule_type "daily" + :details {:other "stuff" + :emails ["foo@bar.com"]}}]] + (testing "Should be able to delete your own subscription" + (mt/with-temp PulseChannelRecipient [pcr {:pulse_channel_id channel-id :user_id (mt/user->id :rasta)}] + (is (= nil + ((mt/user->client :rasta) :delete 204 (str "pulse/" pulse-id "/subscription/email")))))) + + (testing "Users can't delete someone else's pulse subscription" + (mt/with-temp PulseChannelRecipient [pcr {:pulse_channel_id channel-id :user_id (mt/user->id :rasta)}] + (is (= "Not found." + ((mt/user->client :lucky) :delete 404 (str "pulse/" pulse-id "/subscription/email"))))))))) diff --git a/test/metabase/async/api_response_test.clj b/test/metabase/async/api_response_test.clj index ac4f6fb1e8a14c2e420f8a2a586edef1a6134d5a..3f84bb5e59b2562727db34beae159c780ace3f06 100644 --- a/test/metabase/async/api_response_test.clj +++ b/test/metabase/async/api_response_test.clj @@ -1,10 +1,12 @@ (ns metabase.async.api-response-test (:require [cheshire.core :as json] [clojure.core.async :as a] + [clojure.test :refer :all] [expectations :refer [expect]] [metabase.async.api-response :as async-response] [metabase.test.util.async :as tu.async] - [ring.core.protocols :as ring.protocols]) + [ring.core.protocols :as ring.protocols] + [schema.core :as s]) (:import [java.io ByteArrayOutputStream Closeable])) (def ^:private long-timeout-ms @@ -48,13 +50,7 @@ true) (defn- os->response [^ByteArrayOutputStream os] - (some-> - os - .toString - (json/parse-string keyword) - ((fn [response] - (cond-> response - (:stacktrace response) (update :stacktrace (partial every? string?))))))) + (some-> os .toString (json/parse-string keyword))) ;;; ------------------------------ Normal responses: message sent to the input channel ------------------------------- @@ -98,43 +94,37 @@ ;;; ----------------------------------------- Input-chan closed unexpectedly ----------------------------------------- -;; if we close input-channel prematurely, output-channel should get closed -(expect - (tu.async/with-chans [input-chan] - (with-response [{:keys [output-chan]} input-chan] - ;; output-chan may or may not get the InterruptedException written to it -- it's a race condition -- we're just - ;; want to make sure it closes - (a/close! input-chan) - (not= ::tu.async/timed-out (tu.async/wait-for-result output-chan))))) - -;; ...as should the output stream -(expect - (tu.async/with-chans [input-chan] - (with-response [{:keys [os-closed-chan]} input-chan] - (a/close! input-chan) - (wait-for-close os-closed-chan)))) - -;; An error should be written to the output stream -(expect - {:message "Input channel unexpectedly closed." - :type "class java.lang.InterruptedException" - :_status 500 - :stacktrace true} - (tu.async/with-chans [input-chan] - (with-response [{:keys [os os-closed-chan]} input-chan] - (a/close! input-chan) - (wait-for-close os-closed-chan) - (os->response os)))) +(deftest input-chan-closed-unexpectedly-test + (testing "When input-channel is closed prematurely" + (tu.async/with-chans [input-chan] + (with-response [{:keys [os output-chan os-closed-chan]} input-chan] + (a/close! input-chan) + (testing "output-channel should get closed" + ;; output-chan may or may not get the InterruptedException written to it -- it's a race condition -- we're just + ;; want to make sure it closes + (not= ::tu.async/timed-out (tu.async/wait-for-result output-chan)) + + (testing "...as should the output stream" + (is (= true + (wait-for-close os-closed-chan))))) + + (testing "An error should be written to the output stream" + (is (schema= {:message (s/eq "Input channel unexpectedly closed.") + :type (s/eq "class java.lang.InterruptedException") + :_status (s/eq 500) + :trace s/Any + s/Any s/Any} + (os->response os)))))))) ;;; ------------------------------ Output-chan closed early (i.e. API request canceled) ------------------------------ ;; If output-channel gets closed (presumably because the API request is canceled), input-chan should also get closed (expect - (tu.async/with-chans [input-chan] - (with-response [{:keys [output-chan]} input-chan] - (a/close! output-chan) - (wait-for-close input-chan)))) + (tu.async/with-chans [input-chan] + (with-response [{:keys [output-chan]} input-chan] + (a/close! output-chan) + (wait-for-close input-chan)))) ;; if output chan gets closed, output-stream should also get closed (expect @@ -156,43 +146,42 @@ ;;; --------------------------------------- input chan message is an Exception --------------------------------------- -;; If the message sent to input-chan is an Exception an appropriate response should be generated -(expect - {:message "Broken", :type "class java.lang.Exception", :stacktrace true, :_status 500} - (tu.async/with-chans [input-chan] - (with-response [{:keys [os os-closed-chan]} input-chan] - (a/>!! input-chan (Exception. "Broken")) - (wait-for-close os-closed-chan) - (os->response os)))) - - -;;; ------------------------------------------ input-chan never written to ------------------------------------------- - -;; If we write a bad API endpoint and return a channel but never write to it, the request should be canceled after -;; `absolute-max-keepalive-ms` -(expect - {:message "No response after waiting 500.0 ms. Canceling request." - :type "class java.util.concurrent.TimeoutException" - :_status 500 - :stacktrace true} - (with-redefs [async-response/absolute-max-keepalive-ms 500] +(deftest input-change-message-is-exception-test + (testing "If the message sent to input-chan is an Exception an appropriate response should be generated" (tu.async/with-chans [input-chan] (with-response [{:keys [os os-closed-chan]} input-chan] + (a/>!! input-chan (Exception. "Broken")) (wait-for-close os-closed-chan) - (os->response os))))) + (is (schema= {:message (s/eq "Broken") + :type (s/eq "class java.lang.Exception") + :trace s/Any + :_status (s/eq 500) + s/Keyword s/Any} + (os->response os))))))) -;; input chan should get closed -(expect - (with-redefs [async-response/absolute-max-keepalive-ms 500] - (tu.async/with-chans [input-chan] - (with-response [{:keys [os-closed-chan]} input-chan] - (wait-for-close os-closed-chan) - (wait-for-close input-chan))))) -;; output chan should get closed -(expect - (with-redefs [async-response/absolute-max-keepalive-ms 500] - (tu.async/with-chans [input-chan] - (with-response [{:keys [output-chan os-closed-chan]} input-chan] - (wait-for-close os-closed-chan) - (wait-for-close output-chan))))) +;;; ------------------------------------------ input-chan never written to ------------------------------------------- + +(deftest input-chan-never-written-to-test + (testing (str "If we write a bad API endpoint and return a channel but never write to it, the request should be " + "canceled after `absolute-max-keepalive-ms`") + (with-redefs [async-response/absolute-max-keepalive-ms 50] + (tu.async/with-chans [input-chan] + (with-response [{:keys [os os-closed-chan output-chan]} input-chan] + (is (= true + (wait-for-close os-closed-chan))) + (testing "error should be written to output stream" + (is (schema= {:message (s/eq "No response after waiting 50.0 ms. Canceling request.") + :type (s/eq "class java.util.concurrent.TimeoutException") + :_status (s/eq 500) + :trace s/Any + s/Keyword s/Any} + (os->response os)))) + + (testing "input chan should get closed" + (is (= true + (wait-for-close input-chan)))) + + (testing "output chan should get closed" + (is (= true + (wait-for-close output-chan))))))))) diff --git a/test/metabase/pulse/render/png_test.clj b/test/metabase/pulse/render/png_test.clj new file mode 100644 index 0000000000000000000000000000000000000000..34aa760d20915619876869e56be58a3e14cffc83 --- /dev/null +++ b/test/metabase/pulse/render/png_test.clj @@ -0,0 +1,24 @@ +(ns metabase.pulse.render.png-test + (:require [clojure.test :refer :all] + [metabase.pulse.render.png :as png] + [metabase.test :as mt] + [schema.core :as s])) + +(deftest register-fonts-test + (testing "Under normal circumstances, font registration should work as expected" + (is (= nil + (#'png/register-fonts-if-needed!)))) + + (testing "If font regsitration fails, we should an Exception with a useful error message" + (with-redefs [png/register-font! (fn [& _] + (throw (ex-info "Oops!" {})))] + (let [messages (mt/with-log-messages + (is (thrown-with-msg? + clojure.lang.ExceptionInfo + #"Error registering fonts: Metabase will not be able to send Pulses" + (#'png/register-fonts!))))] + (testing "Should log the Exception" + (is (schema= [(s/one (s/eq :error) "log type") + (s/one Throwable "exception") + (s/one #"^Error registering fonts" "message")] + (first messages)))))))) diff --git a/test/metabase/test.clj b/test/metabase/test.clj index 2b2bd8c73f979889dff6cc7865e5cf46c4cb725e..fcf734580c24e980b280fb3cdb3ac8eccaacf636 100644 --- a/test/metabase/test.clj +++ b/test/metabase/test.clj @@ -8,6 +8,7 @@ [medley.core :as m] [metabase [driver :as driver] + [email-test :as et] [query-processor :as qp] [query-processor-test :as qp.test]] [metabase.driver.sql-jdbc.test-util :as sql-jdbc.tu] @@ -37,12 +38,13 @@ data/keep-me datasets/keep-me driver/keep-me + et/keep-me initialize/keep-me qp/keep-me qp.test-util/keep-me qp.test/keep-me sql-jdbc.tu/keep-me - [test-users/keep-me] + test-users/keep-me tt/keep-me tu/keep-me tu.async/keep-me @@ -76,6 +78,16 @@ *driver* with-driver] + [et + email-to + fake-inbox-email-fn + inbox + regex-email-bodies + reset-inbox! + summarize-multipart-email + with-expected-messages + with-fake-inbox] + [initialize initialize-if-needed!] @@ -107,6 +119,7 @@ sql-jdbc-drivers] [test-users + fetch-user user->id user->client user->credentials diff --git a/test/metabase/test/util.clj b/test/metabase/test/util.clj index 81a7810f6aa5d32927bb6e6dd64289d3197e7e86..f780a833a61c57113b9d41bdb0070b127c3a4f56 100644 --- a/test/metabase/test/util.clj +++ b/test/metabase/test/util.clj @@ -361,7 +361,7 @@ @messages)) (defmacro with-log-messages - "Execute BODY, and return a vector of all messages logged using the `log/` family of functions. Messages are of the + "Execute `body`, and return a vector of all messages logged using the `log/` family of functions. Messages are of the format `[:level throwable message]`, and are returned in chronological order from oldest to newest. (with-log-messages (log/warn \"WOW\")) ; -> [[:warn nil \"WOW\"]]"