diff --git a/enterprise/backend/test/metabase_enterprise/serialization/cmd_test.clj b/enterprise/backend/test/metabase_enterprise/serialization/cmd_test.clj index 6089512e1ba6bb23e755d66e8ff68f42cc28e670..20b7331014500c08256250c1476a871cf8400069 100644 --- a/enterprise/backend/test/metabase_enterprise/serialization/cmd_test.clj +++ b/enterprise/backend/test/metabase_enterprise/serialization/cmd_test.clj @@ -3,40 +3,18 @@ [clojure.test :refer :all] [clojure.tools.logging :as log] [metabase-enterprise.serialization.load :as load] + [metabase-enterprise.serialization.test-util :as ts] [metabase.cmd :as cmd] - [metabase.db :as mdb] - [metabase.db.connection :as mdb.connection] - [metabase.db.data-source :as mdb.data-source] - [metabase.db.schema-migrations-test.impl :as schema-migrations-test.impl] [metabase.models :refer [Card Dashboard DashboardCard Database User]] - [metabase.models.permissions-group :as perms-group] [metabase.test :as mt] [metabase.test.fixtures :as fixtures] [metabase.util :as u] [toucan.db :as db] - [toucan.util.test :as t.test] [yaml.core :as yaml]) (:import java.util.UUID)) (use-fixtures :once (fixtures/initialize :db :test-users)) -(defmacro ^:private with-empty-h2-app-db - "Runs `body` under a new, blank, H2 application database (randomly named), in which all model tables have been - created via Liquibase schema migrations. After `body` is finished, the original app DB bindings are restored. - - Makes use of functionality in the [[metabase.db.schema-migrations-test.impl]] namespace since that already does what - we need." - [& body] - `(schema-migrations-test.impl/with-temp-empty-app-db [conn# :h2] - (schema-migrations-test.impl/run-migrations-in-range! conn# [0 "v99.00-000"]) ; this should catch all migrations) - ;; since the actual group defs are not dynamic, we need with-redefs to change them here - (with-redefs [perms-group/all-users (#'perms-group/magic-group perms-group/all-users-group-name) - perms-group/admin (#'perms-group/magic-group perms-group/admin-group-name)] - ~@body))) - -(defn- random-dump-dir [] - (str (System/getProperty "java.io.tmpdir") "/" (mt/random-name))) - (deftest no-collections-test (testing "Dumping a card when there are no active collection should work properly (#16931)" ;; we need a blank H2 app db, temporarily, in order to run this test (to ensure we have no collections present, @@ -45,7 +23,7 @@ ;; ;; making use of the functionality in the [[metabase.db.schema-migrations-test.impl]] namespace for this (since it ;; already does what we need) - (with-empty-h2-app-db + (ts/with-empty-h2-app-db ;; create a single dummy User to own a Card and a Database for it to reference (let [user (db/simple-insert! User :email "nobody@nowhere.com" @@ -74,15 +52,15 @@ :created_at :%now :updated_at :%now) ;; serialize "everything" (which should just be the card and user), which should succeed if #16931 is fixed - (is (nil? (cmd/dump (random-dump-dir)))))))) + (is (nil? (cmd/dump (ts/random-dump-dir)))))))) (deftest blank-target-db-test (testing "Loading a dump into an empty app DB still works (#16639)" - (let [dump-dir (random-dump-dir) + (let [dump-dir (ts/random-dump-dir) user-pre-insert-called? (atom false)] (log/infof "Dumping to %s" dump-dir) (cmd/dump dump-dir "--user" "crowberto@metabase.com") - (with-empty-h2-app-db + (ts/with-empty-h2-app-db (with-redefs [load/pre-insert-user (fn [user] (reset! user-pre-insert-called? true) (assoc user :password "test-password"))] @@ -90,92 +68,29 @@ "--on-error" :abort) (is (true? @user-pre-insert-called?))))))) -(defn- create! [model & {:as properties}] - (db/insert! model (merge (t.test/with-temp-defaults model) properties))) - -(defn- do-with-in-memory-h2-db [db-name-prefix f] - (let [db-name (str db-name-prefix (mt/random-name)) - connection-string (format "jdbc:h2:mem:%s" db-name) - data-source (mdb.data-source/raw-connection-string->DataSource connection-string)] - ;; DB should stay open as long as `conn` is held open. - (with-open [_conn (.getConnection data-source)] - (letfn [(do-with-app-db [thunk] - (binding [mdb.connection/*application-db* (mdb.connection/application-db :h2 data-source)] - (testing (format "\nApp DB = %s" (pr-str connection-string)) - (thunk))))] - (do-with-app-db - (fn [] - (mdb/setup-db!))) - (f do-with-app-db))))) - -(defn- do-with-source-and-dest-dbs [f] - (do-with-in-memory-h2-db - "source-" - (fn [do-with-source-db] - (do-with-in-memory-h2-db - "dest-" - (fn [do-with-dest-db] - (f do-with-source-db do-with-dest-db)))))) - -(defmacro ^:private with-source-and-dest-dbs - "Creates and sets up two in-memory H2 application databases, a source database and an application database. For - testing load/dump/serialization stuff. To use the source DB, use [[with-source-db]], which makes binds it as the - current application database; [[with-dest-db]] binds the destination DB as the current application database." - {:style/indent 0} - [& body] - ;; this is implemented by introducing the anaphors `&do-with-source-db` and `&do-with-dest-db` which are used by - ;; [[with-source-db]] and [[with-dest-db]] - `(do-with-source-and-dest-dbs - (fn [~'&do-with-source-db ~'&do-with-dest-db] - ~@body))) - -(defmacro ^:private with-source-db - "For use with [[with-source-and-dest-dbs]]. Makes the source DB the current application database." - {:style/indent 0} - [& body] - `(~'&do-with-source-db (fn [] ~@body))) - -(defmacro ^:private with-dest-db - "For use with [[with-source-and-dest-dbs]]. Makes the destination DB the current application database." - {:style/indent 0} - [& body] - `(~'&do-with-dest-db (fn [] ~@body))) - -(defn- do-with-random-dump-dir [f] - (let [dump-dir (random-dump-dir)] - (testing (format "\nDump dir = %s" (pr-str dump-dir)) - (try - (f dump-dir) - (finally - (when (.exists (io/file dump-dir)) - (.delete (io/file dump-dir)))))))) - -(defmacro ^:private with-random-dump-dir {:style/indent 1} [[dump-dir-binding] & body] - `(do-with-random-dump-dir (fn [~dump-dir-binding] ~@body))) - (deftest mode-update-remove-cards-test (testing "--mode update should remove Cards in a Dashboard if they're gone from the serialized YAML (#20786)" - (with-random-dump-dir [dump-dir] + (ts/with-random-dump-dir [dump-dir] (let [dashboard-yaml-filename (str dump-dir "/collections/root/dashboards/Dashboard.yaml")] - (with-source-and-dest-dbs + (ts/with-source-and-dest-dbs (testing "create 2 questions in the source and add them to a dashboard" - (with-source-db - (let [{db-id :id, :as db} (create! Database :name "My_Database")] + (ts/with-source-db + (let [{db-id :id, :as db} (ts/create! Database :name "My_Database")] (mt/with-db db - (let [{user-id :id} (create! User, :is_superuser true) - {card-1-id :id} (create! Card - :database_id db-id - :creator_id user-id - :name "Card_1" - :dataset_query {:database db-id, :type :native, :native {:query "SELECT 1;"}}) - {card-2-id :id} (create! Card - :database_id db-id - :creator_id user-id - :name "Card_2" - :dataset_query {:database db-id, :type :native, :native {:query "SELECT 1;"}}) - {dashboard-id :id} (create! Dashboard, :creator_id user-id, :name "Dashboard")] + (let [{user-id :id} (ts/create! User, :is_superuser true) + {card-1-id :id} (ts/create! Card + :database_id db-id + :creator_id user-id + :name "Card_1" + :dataset_query {:database db-id, :type :native, :native {:query "SELECT 1;"}}) + {card-2-id :id} (ts/create! Card + :database_id db-id + :creator_id user-id + :name "Card_2" + :dataset_query {:database db-id, :type :native, :native {:query "SELECT 1;"}}) + {dashboard-id :id} (ts/create! Dashboard, :creator_id user-id, :name "Dashboard")] (doseq [card-id [card-1-id card-2-id]] - (create! DashboardCard :dashboard_id dashboard-id, :card_id card-id)) + (ts/create! DashboardCard :dashboard_id dashboard-id, :card_id card-id)) (testing "dump in source" (is (nil? (cmd/dump dump-dir))))))))) (testing "verify the Dashboard was dumped as expected" @@ -185,9 +100,9 @@ {:card_id "/collections/root/cards/Card_2"}]} yaml)))) (testing "load into destination" - (with-dest-db + (ts/with-dest-db (testing "Create admin user" - (is (some? (create! User, :is_superuser true))) + (is (some? (ts/create! User, :is_superuser true))) (is (db/exists? User :is_superuser true))) (is (nil? (cmd/load dump-dir "--on-error" :abort))) (testing "verify that things were loaded as expected" @@ -195,12 +110,12 @@ (is (= 2 (db/count Card)) "# Cards") (is (= 2 (db/count DashboardCard)) "# DashboardCards")>))) (testing "remove one of the questions in the source's dashboard" - (with-source-db + (ts/with-source-db (db/delete! Card :name "Card_2") (is (= 1 (db/count Card)) "# Cards") (is (= 1 (db/count DashboardCard)) "# DashboardCards"))) (testing "dump again" - (with-source-db + (ts/with-source-db (cmd/dump dump-dir)) (testing "Verify dump only contains one Card" (is (.exists (io/file dashboard-yaml-filename))) @@ -208,7 +123,7 @@ (is (partial= {:dashboard_cards [{:card_id "/collections/root/cards/Card_1"}]} yaml))))) (testing "load again, with --mode update, destination Dashboard should now only have one question." - (with-dest-db + (ts/with-dest-db (is (nil? (cmd/load dump-dir "--mode" :update, "--on-error" :abort))) (is (= 1 (db/count Dashboard)) "# Dashboards") (testing "Don't delete the Card even tho it was deleted. Just delete the DashboardCard" diff --git a/enterprise/backend/test/metabase_enterprise/serialization/test_util.clj b/enterprise/backend/test/metabase_enterprise/serialization/test_util.clj index 4a00d54bdbf147587ae1a091055d0a00393e014f..7647b7e22ffe7710615ce0a1867339631c01da31 100644 --- a/enterprise/backend/test/metabase_enterprise/serialization/test_util.clj +++ b/enterprise/backend/test/metabase_enterprise/serialization/test_util.clj @@ -1,8 +1,15 @@ (ns metabase-enterprise.serialization.test-util - (:require [metabase-enterprise.serialization.names :as names] + (:require [clojure.java.io :as io] + [clojure.test :refer :all] + [metabase-enterprise.serialization.names :as names] + [metabase.db :as mdb] + [metabase.db.connection :as mdb.connection] + [metabase.db.data-source :as mdb.data-source] + [metabase.db.schema-migrations-test.impl :as schema-migrations-test.impl] [metabase.models :refer [Card Collection Dashboard DashboardCard DashboardCardSeries Database Dependency Field Metric NativeQuerySnippet Pulse PulseCard Segment Table User]] [metabase.models.collection :as collection] + [metabase.models.permissions-group :as perms-group] [metabase.query-processor.store :as qp.store] [metabase.shared.models.visualization-settings :as mb.viz] [metabase.test :as mt] @@ -45,6 +52,86 @@ `(binding [collection/*allow-deleting-personal-collections* true] (tt/with-temp* ~model-bindings ~@body))) +(defmacro with-empty-h2-app-db + "Runs `body` under a new, blank, H2 application database (randomly named), in which all model tables have been + created via Liquibase schema migrations. After `body` is finished, the original app DB bindings are restored. + + Makes use of functionality in the [[metabase.db.schema-migrations-test.impl]] namespace since that already does what + we need." + [& body] + `(schema-migrations-test.impl/with-temp-empty-app-db [conn# :h2] + (schema-migrations-test.impl/run-migrations-in-range! conn# [0 "v99.00-000"]) ; this should catch all migrations) + ;; since the actual group defs are not dynamic, we need with-redefs to change them here + (with-redefs [perms-group/all-users (#'perms-group/magic-group perms-group/all-users-group-name) + perms-group/admin (#'perms-group/magic-group perms-group/admin-group-name)] + ~@body))) + +(defn create! [model & {:as properties}] + (db/insert! model (merge (tt/with-temp-defaults model) properties))) + +(defn- do-with-in-memory-h2-db [db-name-prefix f] + (let [db-name (str db-name-prefix (mt/random-name)) + connection-string (format "jdbc:h2:mem:%s" db-name) + data-source (mdb.data-source/raw-connection-string->DataSource connection-string)] + ;; DB should stay open as long as `conn` is held open. + (with-open [_conn (.getConnection data-source)] + (letfn [(do-with-app-db [thunk] + (binding [mdb.connection/*application-db* (mdb.connection/application-db :h2 data-source)] + (testing (format "\nApp DB = %s" (pr-str connection-string)) + (thunk))))] + (do-with-app-db + (fn [] + (mdb/setup-db!))) + (f do-with-app-db))))) + +(defn do-with-source-and-dest-dbs [f] + (do-with-in-memory-h2-db + "source-" + (fn [do-with-source-db] + (do-with-in-memory-h2-db + "dest-" + (fn [do-with-dest-db] + (f do-with-source-db do-with-dest-db)))))) + +(defmacro with-source-and-dest-dbs + "Creates and sets up two in-memory H2 application databases, a source database and an application database. For + testing load/dump/serialization stuff. To use the source DB, use [[with-source-db]], which makes binds it as the + current application database; [[with-dest-db]] binds the destination DB as the current application database." + {:style/indent 0} + [& body] + ;; this is implemented by introducing the anaphors `&do-with-source-db` and `&do-with-dest-db` which are used by + ;; [[with-source-db]] and [[with-dest-db]] + `(do-with-source-and-dest-dbs + (fn [~'&do-with-source-db ~'&do-with-dest-db] + ~@body))) + +(defmacro with-source-db + "For use with [[with-source-and-dest-dbs]]. Makes the source DB the current application database." + {:style/indent 0} + [& body] + `(~'&do-with-source-db (fn [] ~@body))) + +(defmacro with-dest-db + "For use with [[with-source-and-dest-dbs]]. Makes the destination DB the current application database." + {:style/indent 0} + [& body] + `(~'&do-with-dest-db (fn [] ~@body))) + +(defn random-dump-dir [] + (str (System/getProperty "java.io.tmpdir") "/" (mt/random-name))) + +(defn do-with-random-dump-dir [f] + (let [dump-dir (random-dump-dir)] + (testing (format "\nDump dir = %s" (pr-str dump-dir)) + (try + (f dump-dir) + (finally + (when (.exists (io/file dump-dir)) + (.delete (io/file dump-dir)))))))) + +(defmacro with-random-dump-dir {:style/indent 1} [[dump-dir-binding] & body] + `(do-with-random-dump-dir (fn [~dump-dir-binding] ~@body))) + (defmacro with-world "Run test in the context of a minimal Metabase instance connected to our test database." [& body]