From 128a4d00d083da72b50eb9396074726a1cf74d64 Mon Sep 17 00:00:00 2001
From: Cam Saul <1455846+camsaul@users.noreply.github.com>
Date: Tue, 21 Jan 2020 15:30:07 -0800
Subject: [PATCH] Add MySQL latest, MariaDB 10.2, and MariaDB latest to CI 
 (#11769)

* Add MySQL latest, MariaDB 10.2, and MariaDB latest to CI [ci mysql]

* Add upper limit to number of results returned by GET /api/search endpoint

* Rewrite tests in metabase.api.search-test to use new style

* Rework DB & test component init code so they can be retried after failing without having to restart REPL/redefine vars

* Log application DB product/version info; include in troubleshooting info

* Log DB version info when starting tests
---
 .circleci/config.yml                 |  79 +++++-
 src/metabase/api/search.clj          |  41 ++-
 src/metabase/api/util.clj            |   2 +-
 src/metabase/db.clj                  |  29 +-
 src/metabase/driver/mysql.clj        |   2 -
 src/metabase/troubleshooting.clj     |  22 +-
 test/metabase/api/search_test.clj    | 385 +++++++++++++--------------
 test/metabase/test/data/mysql.clj    |   5 -
 test/metabase/test/initialize.clj    | 113 ++++----
 test/metabase/test/initialize/db.clj |  13 +-
 10 files changed, 387 insertions(+), 304 deletions(-)

diff --git a/.circleci/config.yml b/.circleci/config.yml
index f3cf2230515..d25f8e644ad 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -71,6 +71,49 @@ executors:
           MB_ENCRYPTION_SECRET_KEY: << parameters.encryption-key >>
       - image: circleci/mysql:5.7.23
 
+  mysql-latest:
+    working_directory: /home/circleci/metabase/metabase/
+    docker:
+      - image: circleci/clojure:lein-2.8.1
+        environment:
+          MB_DB_TYPE: mysql
+          MB_DB_HOST: localhost
+          MB_DB_PORT: 3306
+          MB_DB_DBNAME: circle_test
+          MB_DB_USER: root
+          MB_MYSQL_TEST_USER: root
+      - image: circleci/mysql:latest
+
+  mariadb-10-2:
+    working_directory: /home/circleci/metabase/metabase/
+    docker:
+      - image: circleci/clojure:lein-2.8.1
+        environment:
+          MB_DB_TYPE: mysql
+          MB_DB_HOST: localhost
+          MB_DB_PORT: 3306
+          MB_DB_DBNAME: circle_test
+          MB_DB_USER: root
+          MB_MYSQL_TEST_USER: root
+      - image: circleci/mariadb:10.2.23
+
+  mariadb-latest:
+    working_directory: /home/circleci/metabase/metabase/
+    docker:
+      - image: circleci/clojure:lein-2.8.1
+        environment:
+          MB_DB_TYPE: mysql
+          MB_DB_HOST: localhost
+          MB_DB_PORT: 3306
+          MB_DB_DBNAME: metabase_test
+          MB_DB_USER: root
+          MB_MYSQL_TEST_USER: root
+      - image: mariadb:latest
+        environment:
+          MYSQL_DATABASE: metabase_test
+          MYSQL_USER: root
+          MYSQL_ALLOW_EMPTY_PASSWORD: yes
+
   mongo:
      working_directory: /home/circleci/metabase/metabase/
      docker:
@@ -290,6 +333,9 @@ jobs:
       auto-retry:
         type: boolean
         default: false
+      description:
+        type: string
+        default: ""
     executor: << parameters.e >>
     steps:
       - attach-workspace
@@ -319,7 +365,7 @@ jobs:
           condition: << parameters.auto-retry >>
           steps:
             - run:
-                name: Test << parameters.driver >> driver
+                name: Test << parameters.driver >> driver << parameters.description >>
                 environment:
                   DRIVERS: h2,<< parameters.driver >>
                 command: >
@@ -593,6 +639,7 @@ workflows:
 
       - test-driver:
           name: be-tests-mysql
+          description: "(MySQL 5.7)"
           requires:
             - be-tests
           e:
@@ -600,6 +647,33 @@ workflows:
             encryption-key: Orw0AAyzkO/kPTLJRxiyKoBHXa/d6ZcO+p+gpZO/wSQ=
           driver: mysql
 
+      - test-driver:
+          name: be-tests-mysql-latest
+          description: "(MySQL latest)"
+          requires:
+            - be-tests
+          e:
+            name: mysql-latest
+          driver: mysql
+
+      - test-driver:
+          name: be-tests-mariadb
+          description: "(MariaDB 10.2)"
+          requires:
+            - be-tests
+          e:
+            name: mariadb-10-2
+          driver: mysql
+
+      - test-driver:
+          name: be-tests-mariadb-latest
+          description: "(MariaDB latest)"
+          requires:
+            - be-tests
+          e:
+            name: mariadb-latest
+          driver: mysql
+
       - test-driver:
           name: be-tests-oracle
           requires:
@@ -737,6 +811,9 @@ workflows:
             - be-tests-googleanalytics
             - be-tests-mongo
             - be-tests-mysql
+            - be-tests-mysql-latest
+            - be-tests-mariadb
+            - be-tests-mariadb-latest
             - be-tests-oracle
             - be-tests-postgres
             - be-tests-presto
diff --git a/src/metabase/api/search.clj b/src/metabase/api/search.clj
index 524fa8fd359..d17d943cd7b 100644
--- a/src/metabase/api/search.clj
+++ b/src/metabase/api/search.clj
@@ -1,5 +1,6 @@
 (ns metabase.api.search
   (:require [clojure.string :as str]
+            [clojure.tools.logging :as log]
             [compojure.core :refer [GET]]
             [flatland.ordered.map :as ordered-map]
             [honeysql
@@ -26,6 +27,12 @@
             [schema.core :as s]
             [toucan.db :as db]))
 
+(def ^:private ^:const search-max-results
+  "Absolute maximum number of search results to return. This number is in place to prevent massive application DB load
+  by returning tons of results; this number should probably be adjusted downward once we have UI in place to indicate
+  that results are truncated."
+  1000)
+
 (def ^:private SearchContext
   "Map with the various allowed search parameters, used to construct the SQL query"
   {:search-string      (s/maybe su/NonBlankString)
@@ -33,8 +40,15 @@
    :current-user-perms #{perms/UserPath}})
 
 (def ^:private searchable-models
+  "Models that can be searched. Results also come back in this order (i.e., all matching Cards, followed by all matching
+  Dashboards, etc.)"
   [Card Dashboard Pulse Collection Segment Metric Table])
 
+(def ^:private model->sort-position
+  (into {} (map-indexed (fn [i model]
+                          [(str/lower-case (name model)) i])
+                        searchable-models)))
+
 (def ^:private SearchableModel
   (apply s/enum searchable-models))
 
@@ -326,15 +340,24 @@
 (s/defn ^:private search
   "Builds a search query that includes all of the searchable entities and runs it"
   [search-ctx :- SearchContext]
-  (for [row (db/query {:union-all (for [model searchable-models
-                                        :let  [query (search-query-for-model model search-ctx)]
-                                        :when (seq query)]
-                                    query)})]
-    ;; MySQL returns `:favorite` as `1` or `0` so convert those to boolean as needed
-    (update row :favorite (fn [favorite]
-                            (if (integer? favorite)
-                              (not (zero? favorite))
-                              favorite)))))
+  (letfn [(bit->boolean [v]
+            (if (number? v)
+              (not (zero? v))
+              v))]
+    (let [search-query {:union-all (for [model searchable-models
+                                         :let  [query (search-query-for-model model search-ctx)]
+                                         :when (seq query)]
+                                     query)}
+          _            (log/tracef "Searching with query:\n%s" (u/pprint-to-str search-query))
+          ;; sort results by [model name]
+          results      (sort-by (juxt (comp model->sort-position :model)
+                                      :name)
+                                (db/query search-query :max-rows search-max-results))]
+      (for [row results]
+        ;; MySQL returns `:favorite` and `:archived` as `1` or `0` so convert those to boolean as needed
+        (-> row
+            (update :favorite bit->boolean)
+            (update :archived bit->boolean))))))
 
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
diff --git a/src/metabase/api/util.clj b/src/metabase/api/util.clj
index 2270e7428f3..118b1266794 100644
--- a/src/metabase/api/util.clj
+++ b/src/metabase/api/util.clj
@@ -39,7 +39,7 @@
 (api/defendpoint GET "/bug_report_details"
   []
   (api/check-superuser)
-  {:system-info (troubleshooting/system-info)
+  {:system-info   (troubleshooting/system-info)
    :metabase-info (troubleshooting/metabase-info)})
 
 (api/define-routes)
diff --git a/src/metabase/db.clj b/src/metabase/db.clj
index 94acb70de1c..2554d22329c 100644
--- a/src/metabase/db.clj
+++ b/src/metabase/db.clj
@@ -256,7 +256,7 @@
   false)
 
 (s/defn ^:private verify-db-connection
-  "Test connection to database with DETAILS and throw an exception if we have any troubles connecting."
+  "Test connection to database with `details` and throw an exception if we have any troubles connecting."
   ([db-details]
    (verify-db-connection (:type db-details) db-details))
 
@@ -266,7 +266,10 @@
              (classloader/require 'metabase.driver.util)
              ((resolve 'metabase.driver.util/can-connect-with-details?) driver details :throw-exceptions))
      (trs "Unable to connect to Metabase {0} DB." (name driver)))
-   (log/info (trs "Verify Database Connection ... ") (u/emoji "✅"))))
+   (jdbc/with-db-metadata [metadata (jdbc-spec details)]
+     (log/info (trs "Successfully verified {0} {1} application database connection."
+                    (.getDatabaseProductName metadata) (.getDatabaseProductVersion metadata))
+               (u/emoji "✅")))))
 
 (def ^:dynamic ^Boolean *disable-data-migrations*
   "Should we skip running data migrations when setting up the DB? (Default is `false`).
@@ -309,7 +312,8 @@
     ((resolve 'metabase.db.migrations/run-all!))))
 
 (defn setup-db!*
-  "Connects to db and runs migrations."
+  "Connects to db and runs migrations. Don't use this directly, unless you know what you're doing; use `setup-db!`
+  instead, which can be called more than once without issue and is thread-safe."
   [db-details auto-migrate]
   (u/profile (trs "Database setup")
     (u/with-us-locale
@@ -326,13 +330,22 @@
     (reset! db-setup-finished? true))
   nil)
 
-(defonce ^{:arglists '([]), :doc "Do general preparation of database by validating that we can connect. Caller can
-  specify if we should run any pending database migrations. If DB is already set up, this function will no-op."}
-  setup-db!
-  (partial deref (delay (setup-db-from-env!*))))
+(defonce ^:private db-setup-complete? (atom false))
+(defonce ^:private setup-db-lock (Object.))
+
+(defn setup-db!
+  "Do general preparation of database by validating that we can connect. Caller can specify if we should run any pending
+  database migrations. If DB is already set up, this function will no-op. Thread-safe."
+  []
+  (when-not @db-setup-complete?
+    (locking setup-db-lock
+      (when-not @db-setup-complete?
+        (setup-db-from-env!*)
+        (reset! db-setup-complete? true))))
+  :done)
 
 
-;;; Various convenience fns (experiMENTAL)
+;;; Various convenience fns
 
 (defn join
   "Convenience for generating a HoneySQL `JOIN` clause.
diff --git a/src/metabase/driver/mysql.clj b/src/metabase/driver/mysql.clj
index 67d9332f08d..cc0fcd6e663 100644
--- a/src/metabase/driver/mysql.clj
+++ b/src/metabase/driver/mysql.clj
@@ -254,12 +254,10 @@
        (-> (dbspec/mysql details)
            (sql-jdbc.common/handle-additional-options details))))))
 
-
 (defmethod sql-jdbc.sync/active-tables :mysql
   [& args]
   (apply sql-jdbc.sync/post-filtered-active-tables args))
 
-
 (defmethod sql-jdbc.sync/excluded-schemas :mysql
   [_]
   #{"INFORMATION_SCHEMA"})
diff --git a/src/metabase/troubleshooting.clj b/src/metabase/troubleshooting.clj
index c6a30838e46..437ea8af643 100644
--- a/src/metabase/troubleshooting.clj
+++ b/src/metabase/troubleshooting.clj
@@ -1,10 +1,11 @@
 (ns metabase.troubleshooting
-  (:require [metabase
+  (:require [clojure.java.jdbc :as jdbc]
+            [metabase
              [config :as mc]
              [db :as mdb]]
             [metabase.models.setting :as setting]
             [metabase.util.stats :as mus]
-            [toucan.db :as tdb]))
+            [toucan.db :as db]))
 
 (defn system-info
   "System info we ask for for bug reports"
@@ -25,9 +26,14 @@
 (defn metabase-info
   "Make it easy for the user to tell us what they're using"
   []
-  {:databases            (->> (tdb/select 'Database) (map :engine) distinct)
-   :hosting-env          (mus/environment-type)
-   :application-database (mdb/db-type)
-   :run-mode             (mc/config-kw :mb-run-mode)
-   :version              mc/mb-version-info
-   :settings             {:report-timezone (setting/get :report-timezone)}})
+  {:databases                    (->> (db/select 'Database) (map :engine) distinct)
+   :hosting-env                  (mus/environment-type)
+   :application-database         (mdb/db-type)
+   :application-database-details (jdbc/with-db-metadata [metadata (db/connection)]
+                                   {:database    {:name    (.getDatabaseProductName metadata)
+                                                  :version (.getDatabaseProductVersion metadata)}
+                                    :jdbc-driver {:name    (.getDriverName metadata)
+                                                  :version (.getDriverVersion metadata)}})
+   :run-mode                     (mc/config-kw :mb-run-mode)
+   :version                      mc/mb-version-info
+   :settings                     {:report-timezone (setting/get :report-timezone)}})
diff --git a/test/metabase/api/search_test.clj b/test/metabase/api/search_test.clj
index 7119db87a57..24cb63b3c92 100644
--- a/test/metabase/api/search_test.clj
+++ b/test/metabase/api/search_test.clj
@@ -1,29 +1,19 @@
 (ns metabase.api.search-test
   (:require [clojure
              [set :as set]
-             [string :as str]]
-            [expectations :refer [expect]]
+             [string :as str]
+             [test :refer :all]]
+            [metabase
+             [models :refer [Card CardFavorite Collection Dashboard DashboardFavorite Database Metric PermissionsGroup
+                             PermissionsGroupMembership Pulse Segment Table]]
+             [test :as mt]
+             [util :as u]]
+            [metabase.api.search :as api.search]
             [metabase.models
-             [card :refer [Card]]
-             [card-favorite :refer [CardFavorite]]
-             [collection :as coll :refer [Collection]]
-             [dashboard :refer [Dashboard]]
-             [dashboard-favorite :refer [DashboardFavorite]]
-             [database :refer [Database]]
-             [metric :refer [Metric]]
              [permissions :as perms]
-             [permissions-group :as group :refer [PermissionsGroup]]
-             [permissions-group-membership :refer [PermissionsGroupMembership]]
-             [pulse :refer [Pulse]]
-             [segment :refer [Segment]]
-             [table :refer [Table]]]
-            [metabase.test
-             [data :as data]
-             [util :as tu]]
+             [permissions-group :as group]]
             [metabase.test.data.users :as test-users]
-            [metabase.util :as u]
-            [toucan.db :as db]
-            [toucan.util.test :as tt]))
+            [toucan.db :as db]))
 
 (def ^:private default-search-row
   {:id                  true
@@ -47,10 +37,10 @@
   (merge
    {:table_id true, :database_id true}
    (db/select-one [Table [:name :table_name] [:schema :table_schema] [:description :table_description]]
-     :id (data/id :checkins))))
+     :id (mt/id :checkins))))
 
 (defn- sorted-results [results]
-  (sort-by (juxt :model :name) results))
+  (sort-by (juxt (comp (var-get #'api.search/model->sort-position) :model) :name) results))
 
 (defn- default-search-results []
   (sorted-results
@@ -101,7 +91,7 @@
                         (merge (data-map instance-name)
                                (when-not in-root-collection?
                                  {:collection_id (u/get-id collection)})))]
-    (tt/with-temp* [Collection [coll      (data-map "collection %s collection")]
+    (mt/with-temp* [Collection [coll      (data-map "collection %s collection")]
                     Card       [card      (coll-data-map "card %s card" coll)]
                     Dashboard  [dashboard (coll-data-map "dashboard %s dashboard" coll)]
                     Pulse      [pulse     (coll-data-map "pulse %s pulse" coll)]
@@ -126,176 +116,167 @@
     (let [raw-results (apply (test-users/user->client user-kwd) :get 200 "search" params)]
       (for [result raw-results
             ;; filter out any results not from the usual test data DB (e.g. results from other drivers)
-            :when  (contains? #{(data/id) nil} (:database_id result))]
+            :when  (contains? #{(mt/id) nil} (:database_id result))]
         (-> result
-            tu/boolean-ids-and-timestamps
+            mt/boolean-ids-and-timestamps
             (update :collection_name #(some-> % string?))))))))
 
-;; Basic search, should find 1 of each entity type, all items in the root collection
-(expect
-  (default-search-results)
-  (with-search-items-in-root-collection "test"
-    (search-request :crowberto :q "test")))
-
-;; Search with no search string. Note this search everything in the DB, including any stale data left behind from
-;; previous tests. Instead of an = comparison here, just ensure our default results are included
-(expect
-  (set/subset?
-   (set (default-search-results))
-   (set (with-search-items-in-root-collection "test"
-          (search-request :crowberto)))))
-
-;; Ensure that users without perms for the root collection don't get results
-;; NOTE: Metrics and segments don't have collections, so they'll be returned
-(expect
-  (default-metric-segment-results)
-  (tu/with-non-admin-groups-no-root-collection-perms
+(deftest basic-test
+  (testing "Basic search, should find 1 of each entity type, all items in the root collection"
     (with-search-items-in-root-collection "test"
-      (search-request :rasta :q "test"))))
+      (is (= (default-search-results)
+             (search-request :crowberto :q "test")))))
 
-;; Users that have root collection permissions should get root collection search results
-(expect
-  (remove (comp #{"collection"} :model) (default-search-results))
-  (tu/with-non-admin-groups-no-root-collection-perms
+  (testing (str "Search with no search string. Note this search everything in the DB, including any stale data left "
+                "behind from previous tests. Instead of an = comparison here, just ensure our default results are "
+                "included")
     (with-search-items-in-root-collection "test"
-      (tt/with-temp* [PermissionsGroup           [group]
-                      PermissionsGroupMembership [_ {:user_id (test-users/user->id :rasta), :group_id (u/get-id group)}]]
-        (perms/grant-permissions! group (perms/collection-read-path {:metabase.models.collection/is-root? true}))
-        (search-request :rasta :q "test")))))
-
-;; Users without root collection permissions should still see other collections they have access to
-(expect
-  (sorted-results
-   (into
-    (default-results-with-collection)
-    (map #(merge default-search-row % (table-search-results))
-         [{:name "metric test2 metric", :description "Lookin' for a blueberry", :model "metric"}
-          {:name "segment test2 segment", :description "Lookin' for a blueberry", :model "segment"}])))
-  (tu/with-non-admin-groups-no-root-collection-perms
-    (with-search-items-in-collection {:keys [collection]} "test"
-      (with-search-items-in-root-collection "test2"
-        (tt/with-temp* [PermissionsGroup           [group]
-                        PermissionsGroupMembership [_ {:user_id (test-users/user->id :rasta), :group_id (u/get-id group)}]]
-          (perms/grant-collection-read-permissions! group (u/get-id collection))
-          (search-request :rasta :q "test"))))))
+      (is (set/subset?
+           (set (default-search-results))
+           (set (search-request :crowberto))))))
 
-;; Users with root collection permissions should be able to search root collection data long with collections they
-;; have access to
-(expect
-  (sorted-results
-   (into
-    (default-results-with-collection)
-    (for [row   (default-search-results)
-          :when (not= "collection" (:model row))]
-      (update row :name #(str/replace % "test" "test2")))))
-  (tu/with-non-admin-groups-no-root-collection-perms
-    (with-search-items-in-collection {:keys [collection]} "test"
-      (with-search-items-in-root-collection "test2"
-        (tt/with-temp* [PermissionsGroup           [group]
+  (testing "Basic search should only return substring matches"
+    (with-search-items-in-root-collection "test"
+      (with-search-items-in-root-collection "something different"
+        (is (= (default-search-results)
+               (search-request :crowberto :q "test")))))))
+
+(deftest permissions-test
+  (testing (str "Ensure that users without perms for the root collection don't get results NOTE: Metrics and segments "
+                "don't have collections, so they'll be returned")
+    (mt/with-non-admin-groups-no-root-collection-perms
+      (with-search-items-in-root-collection "test"
+        (is (= (default-metric-segment-results)
+               (search-request :rasta :q "test"))))))
+
+  (testing "Users that have root collection permissions should get root collection search results"
+    (mt/with-non-admin-groups-no-root-collection-perms
+      (with-search-items-in-root-collection "test"
+        (mt/with-temp* [PermissionsGroup           [group]
                         PermissionsGroupMembership [_ {:user_id (test-users/user->id :rasta), :group_id (u/get-id group)}]]
           (perms/grant-permissions! group (perms/collection-read-path {:metabase.models.collection/is-root? true}))
-          (perms/grant-collection-read-permissions! group collection)
-          (search-request :rasta :q "test"))))))
-
-;; Users with access to multiple collections should see results from all collections they have access to
-(expect
-  (sorted-results
-   (into
-    (default-results-with-collection)
-    (map (fn [row] (update row :name #(str/replace % "test" "test2")))
-         (default-results-with-collection))))
-  (with-search-items-in-collection {coll-1 :collection} "test"
-    (with-search-items-in-collection {coll-2 :collection} "test2"
-      (tt/with-temp* [PermissionsGroup           [group]
-                      PermissionsGroupMembership [_ {:user_id (test-users/user->id :rasta), :group_id (u/get-id group)}]]
-        (perms/grant-collection-read-permissions! group (u/get-id coll-1))
-        (perms/grant-collection-read-permissions! group (u/get-id coll-2))
-        (search-request :rasta :q "test")))))
-
-;; User should only see results in the collection they have access to
-(expect
-  (sorted-results
-   (into
-    (default-results-with-collection)
-    (map #(merge default-search-row % (table-search-results))
-         [{:name "metric test2 metric", :description "Lookin' for a blueberry", :model "metric"}
-          {:name "segment test2 segment", :description "Lookin' for a blueberry", :model "segment"}])))
-  (tu/with-non-admin-groups-no-root-collection-perms
+          (is (= (remove (comp #{"collection"} :model) (default-search-results))
+                 (search-request :rasta :q "test")))))))
+
+  (testing "Users without root collection permissions should still see other collections they have access to"
+    (mt/with-non-admin-groups-no-root-collection-perms
+      (with-search-items-in-collection {:keys [collection]} "test"
+        (with-search-items-in-root-collection "test2"
+          (mt/with-temp* [PermissionsGroup           [group]
+                          PermissionsGroupMembership [_ {:user_id (test-users/user->id :rasta), :group_id (u/get-id group)}]]
+            (perms/grant-collection-read-permissions! group (u/get-id collection))
+            (is (= (sorted-results
+                    (into
+                     (default-results-with-collection)
+                     (map #(merge default-search-row % (table-search-results))
+                          [{:name "metric test2 metric", :description "Lookin' for a blueberry", :model "metric"}
+                           {:name "segment test2 segment", :description "Lookin' for a blueberry", :model "segment"}])))
+                   (search-request :rasta :q "test"))))))))
+
+  (testing (str "Users with root collection permissions should be able to search root collection data long with "
+                "collections they have access to")
+    (mt/with-non-admin-groups-no-root-collection-perms
+      (with-search-items-in-collection {:keys [collection]} "test"
+        (with-search-items-in-root-collection "test2"
+          (mt/with-temp* [PermissionsGroup           [group]
+                          PermissionsGroupMembership [_ {:user_id (test-users/user->id :rasta), :group_id (u/get-id group)}]]
+            (perms/grant-permissions! group (perms/collection-read-path {:metabase.models.collection/is-root? true}))
+            (perms/grant-collection-read-permissions! group collection)
+            (is (= (sorted-results
+                    (into
+                     (default-results-with-collection)
+                     (for [row   (default-search-results)
+                           :when (not= "collection" (:model row))]
+                       (update row :name #(str/replace % "test" "test2")))))
+                   (search-request :rasta :q "test"))))))))
+
+  (testing "Users with access to multiple collections should see results from all collections they have access to"
     (with-search-items-in-collection {coll-1 :collection} "test"
       (with-search-items-in-collection {coll-2 :collection} "test2"
-        (tt/with-temp* [PermissionsGroup           [group]
+        (mt/with-temp* [PermissionsGroup           [group]
                         PermissionsGroupMembership [_ {:user_id (test-users/user->id :rasta), :group_id (u/get-id group)}]]
           (perms/grant-collection-read-permissions! group (u/get-id coll-1))
-          (search-request :rasta :q "test"))))))
-
-;; Favorites are per user, so other user's favorites don't cause search results to be favorited
-(expect
-  (default-results-with-collection)
-  (with-search-items-in-collection {:keys [card dashboard]} "test"
-    (tt/with-temp* [CardFavorite      [_ {:card_id (u/get-id card)
-                                          :owner_id (test-users/user->id :rasta)}]
-                    DashboardFavorite [_ {:dashboard_id (u/get-id dashboard)
-                                          :user_id (test-users/user->id :rasta)}]]
-      (search-request :crowberto :q "test"))))
-
-;; Basic search, should find 1 of each entity type and include favorites when available
-(expect
-  (on-search-types #{"dashboard" "card"}
-                   #(assoc % :favorite true)
-                   (default-results-with-collection))
-  (with-search-items-in-collection {:keys [card dashboard]} "test"
-    (tt/with-temp* [CardFavorite      [_ {:card_id  (u/get-id card)
-                                          :owner_id (test-users/user->id :crowberto)}]
-                    DashboardFavorite [_ {:dashboard_id (u/get-id dashboard)
-                                          :user_id      (test-users/user->id :crowberto)}]]
-      (search-request :crowberto :q "test"))))
-
-;; Basic search should only return substring matches
-(expect
-  (default-search-results)
-  (with-search-items-in-root-collection "test"
-    (with-search-items-in-root-collection "something different"
-      (search-request :crowberto :q "test"))))
+          (perms/grant-collection-read-permissions! group (u/get-id coll-2))
+          (is (= (sorted-results
+                  (into
+                   (default-results-with-collection)
+                   (map (fn [row] (update row :name #(str/replace % "test" "test2")))
+                        (default-results-with-collection))))
+                 (search-request :rasta :q "test")))))))
+
+  (testing "User should only see results in the collection they have access to"
+    (mt/with-non-admin-groups-no-root-collection-perms
+      (with-search-items-in-collection {coll-1 :collection} "test"
+        (with-search-items-in-collection {coll-2 :collection} "test2"
+          (mt/with-temp* [PermissionsGroup           [group]
+                          PermissionsGroupMembership [_ {:user_id (test-users/user->id :rasta), :group_id (u/get-id group)}]]
+            (perms/grant-collection-read-permissions! group (u/get-id coll-1))
+            (is (= (sorted-results
+                    (into
+                     (default-results-with-collection)
+                     (map #(merge default-search-row % (table-search-results))
+                          [{:name "metric test2 metric", :description "Lookin' for a blueberry", :model "metric"}
+                           {:name "segment test2 segment", :description "Lookin' for a blueberry", :model "segment"}])))
+                   (search-request :rasta :q "test")))))))))
+
+(deftest favorites-test
+  (testing "Favorites are per user, so other user's favorites don't cause search results to be favorited"
+    (with-search-items-in-collection {:keys [card dashboard]} "test"
+      (mt/with-temp* [CardFavorite      [_ {:card_id  (u/get-id card)
+                                            :owner_id (test-users/user->id :rasta)}]
+                      DashboardFavorite [_ {:dashboard_id (u/get-id dashboard)
+                                            :user_id      (test-users/user->id :rasta)}]]
+        (is (= (default-results-with-collection)
+               (search-request :crowberto :q "test"))))))
+
+  (testing "Basic search, should find 1 of each entity type and include favorites when available"
+    (with-search-items-in-collection {:keys [card dashboard]} "test"
+      (mt/with-temp* [CardFavorite      [_ {:card_id  (u/get-id card)
+                                            :owner_id (test-users/user->id :crowberto)}]
+                      DashboardFavorite [_ {:dashboard_id (u/get-id dashboard)
+                                            :user_id      (test-users/user->id :crowberto)}]]
+        (is (= (on-search-types #{"dashboard" "card"}
+                                #(assoc % :favorite true)
+                                (default-results-with-collection))
+               (search-request :crowberto :q "test")))))))
 
 (defn- archived [m]
   (assoc m :archived true))
 
-;; Should return unarchived results by default
-(expect
-  (default-search-results)
-  (with-search-items-in-root-collection "test"
-    (tt/with-temp* [Card       [_ (archived {:name "card test card 2"})]
-                    Dashboard  [_ (archived {:name "dashboard test dashboard 2"})]
-                    Collection [_ (archived {:name "collection test collection 2"})]
-                    Metric     [_ (archived {:name "metric test metric 2"})]
-                    Segment    [_ (archived {:name "segment test segment 2"})]]
-      (search-request :crowberto :q "test"))))
-
-;; Should return archived results when specified
-(expect
-  (default-archived-results)
-  (with-search-items-in-root-collection "test2"
-    (tt/with-temp* [Card       [_ (archived {:name "card test card"})]
-                    Dashboard  [_ (archived {:name "dashboard test dashboard"})]
-                    Collection [_ (archived {:name "collection test collection"})]
-                    Metric     [_ (archived {:name "metric test metric"})]
-                    Segment    [_ (archived {:name "segment test segment"})]]
-      (search-request :crowberto :q "test", :archived "true"))))
-
-;; Search should not return alerts
-(expect
-  []
-  (with-search-items-in-root-collection "test"
-    (tt/with-temp* [Pulse [pulse {:alert_condition  "rows"
-                                  :alert_first_only false
-                                  :alert_above_goal nil
-                                  :name             nil}]]
-      (filter (fn [{:keys [model id]}]
-                (and (= id (u/get-id pulse))
-                     (= "pulse" model)))
-              ((test-users/user->client :crowberto) :get 200 "search")))))
+(deftest archived-results-test
+  (testing "Should return unarchived results by default"
+    (with-search-items-in-root-collection "test"
+      (mt/with-temp* [Card       [_ (archived {:name "card test card 2"})]
+                      Dashboard  [_ (archived {:name "dashboard test dashboard 2"})]
+                      Collection [_ (archived {:name "collection test collection 2"})]
+                      Metric     [_ (archived {:name "metric test metric 2"})]
+                      Segment    [_ (archived {:name "segment test segment 2"})]]
+        (is (= (default-search-results)
+               (search-request :crowberto :q "test"))))))
+
+  (testing "Should return archived results when specified"
+    (with-search-items-in-root-collection "test2"
+      (mt/with-temp* [Card       [_ (archived {:name "card test card"})]
+                      Dashboard  [_ (archived {:name "dashboard test dashboard"})]
+                      Collection [_ (archived {:name "collection test collection"})]
+                      Metric     [_ (archived {:name "metric test metric"})]
+                      Segment    [_ (archived {:name "segment test segment"})]]
+        (is (= (default-archived-results)
+               (search-request :crowberto :q "test", :archived "true")))))))
+
+(deftest alerts-test
+  (testing "Search should not return alerts"
+    (with-search-items-in-root-collection "test"
+      (mt/with-temp* [Pulse [pulse {:alert_condition  "rows"
+                                    :alert_first_only false
+                                    :alert_above_goal nil
+                                    :name             nil}]]
+        (is (= []
+               (filter (fn [{:keys [model id]}]
+                         (and (= id (u/get-id pulse))
+                              (= "pulse" model)))
+                       ((test-users/user->client :crowberto) :get 200 "search"))))))))
 
-;; You should see TABLES in the search results!
 (defn- default-table-search-row [table-name]
   (merge
    default-search-row
@@ -307,33 +288,27 @@
     :model        "table"
     :database_id  true}))
 
-(expect
-  [(default-table-search-row "Round Table")]
-  (tt/with-temp Table [table {:name "Round Table"}]
-    (search-request :crowberto :q "Round Table")))
-
-(expect
-  [(default-table-search-row "Kitchen Table")]
-  (tt/with-temp Table [table {:name "Kitchen Table"}]
-    (search-request :rasta :q "Kitchen Table")))
-
-;; But *archived* tables should not appear in search results
-(let [table-name (tu/random-name)]
-  (expect
-    []
-    (tt/with-temp Table [table {:name table-name}]
-      (search-request :crowberto :q table-name :archived true))))
-
-(let [table-name (tu/random-name)]
-  (expect
-    []
-    (tt/with-temp Table [table {:name table-name}]
-      (search-request :rasta :q table-name :archived true))))
-
-;; you should not be able to see a Table if the current user doesn't have permissions for that Table
-(expect
-  []
-  (tt/with-temp* [Database [{db-id :id}]
-                  Table    [table {:db_id db-id}]]
-    (perms/revoke-permissions! (group/all-users) db-id)
-    (search-request :rasta :q (:name table))))
+(deftest table-test
+  (testing "You should see Tables in the search results!"
+    (mt/with-temp Table [table {:name "Round Table"}]
+      (doseq [user [:crowberto :rasta]]
+        (is (= [(default-table-search-row "Round Table")]
+               (search-request user :q "Round Table")))))
+    (testing "When searching with ?archived=true, normal Tables should not show up in the results"
+      (let [table-name (mt/random-name)]
+        (mt/with-temp Table [table {:name table-name}]
+          (doseq [user [:crowberto :rasta]]
+            (is (= []
+                   (search-request user :q table-name :archived true)))))))
+    (testing "*archived* tables should not appear in search results"
+      (let [table-name (mt/random-name)]
+        (mt/with-temp Table [table {:name table-name, :active false}]
+          (doseq [user [:crowberto :rasta]]
+            (is (= []
+                   (search-request user :q table-name)))))))
+    (testing "you should not be able to see a Table if the current user doesn't have permissions for that Table"
+      (mt/with-temp* [Database [{db-id :id}]
+                      Table    [table {:db_id db-id}]]
+        (perms/revoke-permissions! (group/all-users) db-id)
+        (is (= []
+               (search-request :rasta :q (:name table))))))))
diff --git a/test/metabase/test/data/mysql.clj b/test/metabase/test/data/mysql.clj
index 63c4f3a5ab1..87d7f53fd20 100644
--- a/test/metabase/test/data/mysql.clj
+++ b/test/metabase/test/data/mysql.clj
@@ -48,9 +48,4 @@
   [& args]
   (apply load-data/load-data-all-at-once! args))
 
-#_(defmethod load-data/do-insert! :mysql
-  [driver spec table-identifier row-or-rows]
-  (jdbc/execute! spec "SET @@session.time_zone = 'UTC'");
-  ((get-method load-data/do-insert! :sql-jdbc/test-extensions) driver spec table-identifier row-or-rows))
-
 (defmethod sql.tx/pk-sql-type :mysql [_] "INTEGER NOT NULL AUTO_INCREMENT")
diff --git a/test/metabase/test/initialize.clj b/test/metabase/test/initialize.clj
index 8bd75ffcf84..5a27d00df7d 100644
--- a/test/metabase/test/initialize.clj
+++ b/test/metabase/test/initialize.clj
@@ -7,45 +7,10 @@
              [util :as u]]
             [metabase.plugins.classloader :as classloader]))
 
-
-;; (def ^:private ^:dynamic *require-chain* nil)
-
-;; (defonce new-require
-;;   (let [orig-require (var-get #'clojure.core/require)]
-;;     (orig-require 'clojure.pprint)
-;;     (fn [& args]
-;;       (binding [*require-chain* (conj (vec *require-chain*) (ns-name *ns*))]
-;;         (let [require-chain-description (apply str (interpose " -> " *require-chain*))]
-;;           (println "\nin" require-chain-description)
-;;           ((resolve 'clojure.pprint/pprint) (cons 'require args))
-;;           (apply orig-require args)
-;;           (println "finished" require-chain-description))))))
-
-;; (intern 'clojure.core 'require new-require)
-
-(defmulti initialize-if-needed!
-  "Initialize one or more components.
-
-    (initialize-if-needed! :db :web-server)"
-  (fn
-    ([k]        (keyword k))
-    ([k & more] :many)))
-
-(defonce ^:private initialized (atom #{}))
-
-(defn initialized?
-  "Has this component been initialized?"
-  ([k]
-   (contains? @initialized k))
-
-  ([k & more]
-   (and (initialized? k)
-        (apply initialized? more))))
-
-(defmethod initialize-if-needed! :many
-  [& args]
-  (doseq [k args]
-    (initialize-if-needed! k)))
+(defmulti ^:private do-initialization!
+  "Perform component-specific initialization. This is guaranteed to only be called once."
+  {:arglists '([init-setp])}
+  keyword)
 
 (defn- log-init-message [task-name]
   (let [body   (format "| Initializing %s... |" task-name)
@@ -58,36 +23,57 @@
 
 (def ^:private init-timeout-ms (* 30 1000))
 
-(def ^:private ^:dynamic *initializing* [])
+(def ^:private ^:dynamic *initializing*
+  "Collection of components that are being currently initialized by the current thread."
+  [])
+
+(defonce ^:private initialized (atom #{}))
 
-(defn- deref-init-delay [task-name a-delay]
+(defn- check-for-circular-deps [step]
+  (when (contains? (set *initializing*) step)
+    (throw (Exception. (format "Circular initialization dependencies! %s"
+                               (str/join " -> " (conj *initializing* step)))))))
+
+(defn- initialize-if-needed!* [step]
   (try
-    (when (contains? (set *initializing*) task-name)
-      (throw (Exception. (format "Circular initialization dependencies! %s"
-                                 (str/join " -> " (conj *initializing* task-name))))))
-    (binding [*initializing* (conj *initializing* task-name)]
+    (log-init-message step)
+    (binding [*initializing* (conj *initializing* step)]
       (u/with-timeout init-timeout-ms
-        @a-delay))
+        (do-initialization! step)))
     (catch Throwable e
-      (println "Error initializing" task-name)
+      (println "Error initializing" step)
       (println e)
       (when config/is-test?
         (System/exit -1))
       (throw e))))
 
+(defn initialize-if-needed!
+  "Initialize one or more components.
+
+    (initialize-if-needed! :db :web-server)"
+  [& steps]
+  (doseq [step steps
+          :let [step (keyword step)]]
+    (when-not (@initialized step)
+      (check-for-circular-deps step)
+      (locking step
+        (when-not (@initialized step)
+          (initialize-if-needed!* step)
+          (swap! initialized conj step))))))
+
+(defn initialized?
+  "Has this component been initialized?"
+  ([k]
+   (contains? @initialized k))
+
+  ([k & more]
+   (and (initialized? k)
+        (apply initialized? more))))
+
 (defmacro ^:private define-initialization [task-name & body]
-  (let [delay-symb (-> (symbol (format "init-%s-%d" (name task-name) (hash &form)))
-                       (with-meta {:private true}))]
-    `(do
-       (defonce ~delay-symb
-         (delay
-           (log-init-message ~(keyword task-name))
-           (swap! initialized conj ~(keyword task-name))
-           ~@body
-           ~(keyword task-name)))
-       (defmethod initialize-if-needed! ~(keyword task-name)
-         [~'_]
-         (deref-init-delay ~(keyword task-name) ~delay-symb)))))
+  `(defmethod do-initialization! ~(keyword task-name)
+     [~'_]
+     ~@body))
 
 (define-initialization :plugins
   (classloader/require 'metabase.test.initialize.plugins)
@@ -114,5 +100,10 @@
   (classloader/require 'metabase.test.initialize.test-users-personal-collections)
   ((resolve 'metabase.test.initialize.test-users-personal-collections/init!)))
 
-(alter-meta! #'initialize-if-needed! assoc :arglists (list (into ['&] (sort (disj (set (keys (methods initialize-if-needed!)))
-                                                                                  :many)))))
+(defn- all-components
+  "Set of all components/initialization steps that are defined."
+  []
+  (set (keys (methods do-initialization!))))
+
+;; change the arglists for `initialize-if-needed!` to list all the possible args for REPL-usage convenience
+(alter-meta! #'initialize-if-needed! assoc :arglists (list (into ['&] (sort (all-components)))))
diff --git a/test/metabase/test/initialize/db.clj b/test/metabase/test/initialize/db.clj
index e562a55ce7d..03b6735cc28 100644
--- a/test/metabase/test/initialize/db.clj
+++ b/test/metabase/test/initialize/db.clj
@@ -1,9 +1,14 @@
 (ns metabase.test.initialize.db
-  (:require [metabase
+  (:require [clojure.java.jdbc :as jdbc]
+            [metabase
              [db :as mdb]
-             [task :as task]]))
+             [task :as task]
+             [util :as u]]
+            [toucan.db :as db]))
 
 (defn init! []
-  (println (format "Setting up %s test DB and running migrations..." (mdb/db-type)))
+  (println (u/format-color 'blue "Setting up %s test DB and running migrations..." (mdb/db-type)))
   (#'task/set-jdbc-backend-properties!)
-  (mdb/setup-db!))
+  (mdb/setup-db!)
+  (jdbc/with-db-metadata [metadata (db/connection)]
+    (println (u/format-color 'blue "Application DB is %s %s" (.getDatabaseProductName metadata) (.getDatabaseProductVersion metadata)))))
-- 
GitLab