Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
e2e_test.clj 35.49 KiB
(ns ^:mb/once metabase-enterprise.serialization.v2.e2e-test
  (:require
   [clojure.java.io :as io]
   [clojure.test :refer :all]
   [medley.core :as m]
   [metabase-enterprise.serialization.test-util :as ts]
   [metabase-enterprise.serialization.v2.extract :as extract]
   [metabase-enterprise.serialization.v2.ingest :as ingest]
   [metabase-enterprise.serialization.v2.load :as serdes.load]
   [metabase-enterprise.serialization.v2.storage :as storage]
   [metabase.models :refer [Card
                            Collection
                            Dashboard
                            DashboardCard
                            Database
                            ParameterCard
                            Field
                            Table]]
   [metabase.models.action :as action]
   [metabase.models.serialization :as serdes]
   [metabase.test :as mt]
   [metabase.test.generate :as test-gen]
   [metabase.util.yaml :as yaml]
   [reifyhealth.specmonstah.core :as rs]
   [toucan2.core :as t2]
   [toucan2.tools.with-temp :as t2.with-temp])
 (:import
  (java.io File)
  (java.nio.file Path)))

(set! *warn-on-reflection* true)

(defn- dir->contents-set [p dir]
  (->> dir
       .listFiles
       (filter p)
       (map #(.getName %))
       set))

(defn- dir->file-set [dir]
  (dir->contents-set #(.isFile %) dir))

(defn- dir->dir-set [dir]
  (dir->contents-set #(.isDirectory %) dir))

(defn- subdirs [dir]
  (->> dir
       .listFiles
       (remove #(.isFile %))))

(defn- by-model [entities model-name]
  (filter #(-> % :serdes/meta last :model (= model-name))
          entities))

(defn- collections [dir]
  (for [coll-dir (subdirs dir)
        :when (->> ["cards" "dashboards" "timelines"]
                   (map #(io/file coll-dir %))
                   (filter #(= % coll-dir))
                   empty?)]
    coll-dir))

(defn- file-set [^File dir]
  (let [^Path base (.toPath dir)]
    (set (for [^File file (file-seq dir)
               :when (.isFile file)
               :let [rel (.relativize base (.toPath file))]]
           (mapv str rel)))))

(defn- random-keyword
  ([prefix n] (random-keyword prefix n 0))
  ([prefix n floor] (keyword (str (name prefix) (+ floor (rand-int n))))))

(defn- random-fks
  "Generates a specmonstah query with the :refs populated with the randomized bindings.
  `(random-fks {:spec-gen {:foo :bar}}
               {:creator_id [:u 10]
                :db_id      [:db 20 15]})`
  this will return a query like:
  `{:spec-gen {:foo :bar}
    :refs {:creator_id :u6  :db_id 17}}`

  The bindings map has the same keys as `:refs`, but the values are `[base-keyword width]` pairs or
  `[base-keyword width floor]` triples. These are passed to [[random-keyword]]."
  [base bindings]
  (update base :refs merge (m/map-vals #(apply random-keyword %) bindings)))

(defn- many-random-fks [n base bindings]
  (vec (repeatedly n #(vector 1 (random-fks base bindings)))))

(defn- table->db [{:keys [table_id] :as refs}]
  (let [table-number (-> table_id
                         name
                         (subs 1)
                         (Integer/parseInt))]
    (assoc refs :database_id (keyword (str "db" (quot table-number 10))))))

(defn- clean-entity
 "Removes any comparison-confounding fields, like `:created_at`."
 [entity]
 (dissoc entity :created_at :result_metadata))

(deftest e2e-storage-ingestion-test
  (ts/with-random-dump-dir [dump-dir "serdesv2-"]
    (let [extraction (atom nil)
          entities   (atom nil)]
      (ts/with-source-and-dest-dbs
        ;; TODO Generating some nested collections would make these tests more robust, but that's difficult.
        ;; There are handwritten tests for storage and ingestion that check out the nesting, at least.
        (ts/with-source-db
          (testing "insert"
            (test-gen/insert!
              {;; Actions are special case where there is a 1:1 relationship between an action and an action subtype (query, implicit, or http)
               ;; We generate 10 actions for each subtype, and 10 of each subtype.
               ;; actions 0-9 are query actions, 10-19 are implicit actions, and 20-29 are http actions.
               :action                  (apply concat
                                               (for [type [:query :implicit :http]]
                                                 (many-random-fks 10
                                                                  {:spec-gen {:type type}}
                                                                  {:model_id   [:sm 10]
                                                                   :creator_id [:u 10]})))
               :query-action            (map-indexed
                                         (fn [idx x]
                                           (assoc-in x [1 :refs :action_id] (keyword (str "action" idx))))
                                         (many-random-fks 10 {} {:database_id [:db 10]}))
               :implicit-action         (map-indexed
                                         (fn [idx x]
                                           (update-in x [1 :refs]
                                                      (fn [refs]
                                                        (assoc refs :action_id (keyword (str "action" (+ 10 idx)))))))
                                         (many-random-fks 10 {} {}))
               :http-action             (map-indexed
                                         (fn [idx x]
                                           (update-in x [1 :refs]
                                                      (fn [refs]
                                                        (assoc refs :action_id (keyword (str "action" (+ 20 idx)))))))
                                         (many-random-fks 10 {} {}))
               :collection              [[100 {:refs     {:personal_owner_id ::rs/omit}}]
                                         [10  {:refs     {:personal_owner_id ::rs/omit}
                                               :spec-gen {:namespace :snippets}}]]
               :database                [[10]]
               ;; Tables are special - we define table 0-9 under db0, 10-19 under db1, etc. The :card spec below
               ;; depends on this relationship.
               :table                   (into [] (for [db [:db0 :db1 :db2 :db3 :db4 :db5 :db6 :db7 :db8 :db9]]
                                                   [10 {:refs {:db_id db}}]))
               :field                   (many-random-fks 1000 {} {:table_id [:t 100]})
               :core-user               [[100]]
               :card                    (mapv #(update-in % [1 :refs] table->db)
                                              (many-random-fks
                                               100
                                               {:spec-gen {:dataset_query {:database 1
                                                                           :query {:source-table 3
                                                                                   :aggregation [[:count]]
                                                                                   :breakout [[:field 16 nil]]}
                                                                           :type :query}
                                                           :dataset       true}}
                                               {:table_id      [:t    100]
                                                :collection_id [:coll 100]
                                                :creator_id    [:u    10]}))
               ;; Simple model is primary used for actions.
               ;; We can't use :card for actions because implicit actions require the model's query to contain
               ;; nothing but a source table
               :simple-model            (mapv #(update-in % [1 :refs] table->db)
                                               (many-random-fks
                                                10
                                                {:spec-gen {:dataset_query {:database 1
                                                                            :query {:source-table 3}
                                                                            :type :query}
                                                            :dataset       true}}
                                                {:table_id      [:t    10]
                                                 :collection_id [:coll 10]
                                                 :creator_id    [:u    10]}))
               :dashboard               (concat (many-random-fks 100 {} {:collection_id [:coll 100]
                                                                         :creator_id    [:u    10]})
                                                ;; create some root collection dashboards
                                                (many-random-fks 50 {} {:creator_id    [:u 10]}))
               :dashboard-card          (many-random-fks 300 {} {:card_id      [:c 100]
                                                                 :dashboard_id [:d 100]})
               :dimension               (vec (concat
                                               ;; 20 with both IDs set
                                              (many-random-fks 20 {}
                                                               {:field_id                [:field 1000]
                                                                :human_readable_field_id [:field 1000]})
                                               ;; 20 with just :field_id
                                              (many-random-fks 20 {:refs {:human_readable_field_id ::rs/omit}}
                                                               {:field_id [:field 1000]})))
               :metric                  (many-random-fks 30 {:spec-gen {:definition {:aggregation  [[:count]]
                                                                                     :source-table 9}}}
                                                         {:table_id   [:t 100]
                                                          :creator_id [:u 10]})
               :segment                 (many-random-fks 30 {:spec-gen {:definition {:filter [:!= [:field 60 nil] 50],
                                                                                     :source-table 4}}}
                                                         {:table_id   [:t 100]
                                                          :creator_id [:u 10]})
               :native-query-snippet    (many-random-fks 10 {} {:creator_id    [:u 10]
                                                                :collection_id [:coll 10 100]})
               :timeline                (many-random-fks 10 {} {:creator_id    [:u 10]
                                                                :collection_id [:coll 100]})
               :timeline-event          (many-random-fks 90 {} {:timeline_id   [:timeline 10]})
               :pulse                   (vec (concat
                                               ;; 10 classic pulses, from collections
                                              (many-random-fks 10 {} {:collection_id [:coll 100]})
                                               ;; 10 classic pulses, no collection
                                              (many-random-fks 10 {:refs {:collection_id ::rs/omit}} {})
                                               ;; 10 dashboard subs
                                              (many-random-fks 10 {:refs {:collection_id ::rs/omit}}
                                                               {:dashboard_id  [:d 100]})))
               :pulse-card              (vec (concat
                                               ;; 60 pulse cards for the classic pulses
                                              (many-random-fks 60 {} {:card_id       [:c 100]
                                                                      :pulse_id      [:pulse 10]})
                                               ;; 60 pulse cards connected to dashcards for the dashboard subs
                                              (many-random-fks 60 {} {:card_id           [:c 100]
                                                                      :pulse_id          [:pulse 10 20]
                                                                      :dashboard_card_id [:dc 300]})))
               :pulse-channel           (vec (concat
                                               ;; 15 channels for the classic pulses
                                              (many-random-fks 15 {} {:pulse_id  [:pulse 10]})
                                               ;; 15 channels for the dashboard subs
                                              (many-random-fks 15 {} {:pulse_id  [:pulse 10 20]})))
               :pulse-channel-recipient (many-random-fks 40 {} {:pulse_channel_id [:pulse-channel 30]
                                                                :user_id          [:u 100]})}))

          (is (= 100 (count (t2/select-fn-set :email 'User))))

          (testing "extraction"
            (reset! extraction (into [] (extract/extract-metabase {})))
            (reset! entities   (reduce (fn [m entity]
                                         (update m (-> entity :serdes/meta last :model)
                                                 (fnil conj []) entity))
                                       {} @extraction))
            (is (= 110 (-> @entities (get "Collection") count))))

          (testing "storage"
            (storage/store! (seq @extraction) dump-dir)

            (testing "for Actions"
              (is (= 30 (count (dir->file-set (io/file dump-dir "actions"))))))

            (testing "for Collections"
              (is (= 110 (count (for [f (file-set (io/file dump-dir))
                                      :when (and (= (first f) "collections")
                                                 (let [[a b] (take-last 2 f)]
                                                   (= b (str a ".yaml"))))]
                                  f)))
                  "which all go in collections/, even the snippets ones"))

            (testing "for Databases"
              (is (= 10 (count (dir->dir-set (io/file dump-dir "databases"))))))

            (testing "for Tables"
              (is (= 100
                     (reduce + (for [db    (get @entities "Database")
                                     :let [tables (dir->dir-set (io/file dump-dir "databases" (:name db) "tables"))]]
                                 (count tables))))
                  "Tables are scattered, so the directories are harder to count"))

            (testing "for Fields"
              (is (= 1000
                     (reduce + (for [db    (get @entities "Database")
                                     table (subdirs (io/file dump-dir "databases" (:name db) "tables"))]
                                 (->> (io/file table "fields")
                                      dir->file-set
                                      count))))
                  "Fields are scattered, so the directories are harder to count"))

            (testing "for cards"
              ;; 100 from card, and 10 from simple-model
              (is (= 110 (->> (io/file dump-dir "collections")
                              collections
                              (map (comp count dir->file-set #(io/file % "cards")))
                              (reduce +)))))

            (testing "for dashboards"
              (is (= 150 (->> (io/file dump-dir "collections")
                              collections
                              (map (comp count dir->file-set #(io/file % "dashboards")))
                              (reduce +)))))

            (testing "for timelines"
              (is (= 10 (->> (io/file dump-dir "collections")
                             collections
                             (map (comp count dir->file-set #(io/file % "timelines")))
                             (reduce +)))))

            (testing "for metrics"
              (is (= 30 (reduce + (for [db    (dir->dir-set (io/file dump-dir "databases"))
                                        table (dir->dir-set (io/file dump-dir "databases" db "tables"))
                                        :let [metrics-dir (io/file dump-dir "databases" db "tables" table "metrics")]
                                        :when (.exists metrics-dir)]
                                    (count (dir->file-set metrics-dir)))))))

            (testing "for segments"
              (is (= 30 (reduce + (for [db    (dir->dir-set (io/file dump-dir "databases"))
                                        table (dir->dir-set (io/file dump-dir "databases" db "tables"))
                                        :let [segments-dir (io/file dump-dir "databases" db "tables" table "segments")]
                                        :when (.exists segments-dir)]
                                    (count (dir->file-set segments-dir)))))))

            (testing "for native query snippets"
              (is (= 10 (->> (io/file dump-dir "snippets")
                             collections
                             (map (comp count dir->file-set))
                             (reduce +)))))

            (testing "for settings"
              (is (.exists (io/file dump-dir "settings.yaml")))))

          (testing "ingest and load"
            (ts/with-dest-db
              (testing "ingested set matches extracted set"
                (let [extracted-set (set (map (comp #'ingest/strip-labels serdes/path) @extraction))]
                  (is (= (count extracted-set)
                         (count @extraction)))
                  (is (= extracted-set
                         (set (ingest/ingest-list (ingest/ingest-yaml dump-dir)))))))

              (testing "doing ingestion"
                (is (serdes.load/load-metabase (ingest/ingest-yaml dump-dir))
                    "successful"))

              (testing "for Actions"
                (doseq [{:keys [entity_id] :as coll} (get @entities "Action")]
                  (is (= (clean-entity coll)
                         (->> (t2/select-one 'Action :entity_id entity_id)
                              (@#'action/hydrate-subtype)
                              (serdes/extract-one "Action" {})
                              clean-entity)))))

              (testing "for Collections"
                (doseq [{:keys [entity_id] :as coll} (get @entities "Collection")]
                  (is (= (clean-entity coll)
                         (->> (t2/select-one 'Collection :entity_id entity_id)
                              (serdes/extract-one "Collection" {})
                              clean-entity)))))

              (testing "for Databases"
                (doseq [{:keys [name] :as coll} (get @entities "Database")]
                  (is (= (clean-entity coll)
                         (->> (t2/select-one 'Database :name name)
                              (serdes/extract-one "Database" {})
                              clean-entity)))))

              (testing "for Tables"
                (doseq [{:keys [db_id name] :as coll} (get @entities "Table")]
                  (is (= (clean-entity coll)
                         (->> (t2/select-one-fn :id 'Database :name db_id)
                              (t2/select-one 'Table :name name :db_id)
                              (serdes/extract-one "Table" {})
                              clean-entity)))))

              (testing "for Fields"
                (doseq [{[db schema table] :table_id name :name :as coll} (get @entities "Field")]
                  (is (nil? schema))
                  (is (= (clean-entity coll)
                         (->> (t2/select-one-fn :id 'Database :name db)
                              (t2/select-one-fn :id 'Table :schema schema :name table :db_id)
                              (t2/select-one 'Field :name name :table_id)
                              (serdes/extract-one "Field" {})
                              clean-entity)))))

              (testing "for cards"
                (doseq [{:keys [entity_id] :as card} (get @entities "Card")]
                  (is (= (clean-entity card)
                         (->> (t2/select-one 'Card :entity_id entity_id)
                              (serdes/extract-one "Card" {})
                              clean-entity)))))

              (testing "for dashboards"
                (doseq [{:keys [entity_id] :as dash} (get @entities "Dashboard")]
                  (is (= (clean-entity dash)
                         (->> (t2/select-one 'Dashboard :entity_id entity_id)
                              (serdes/extract-one "Dashboard" {})
                              clean-entity)))))

              (testing "for dashboard cards"
                (doseq [{:keys [entity_id] :as dashcard} (get @entities "DashboardCard")]
                  (is (= (clean-entity dashcard)
                         (->> (t2/select-one 'DashboardCard :entity_id entity_id)
                              (serdes/extract-one "DashboardCard" {})
                              clean-entity)))))

              (testing "for dimensions"
                (doseq [{:keys [entity_id] :as dim} (get @entities "Dimension")]
                  (is (= (clean-entity dim)
                         (->> (t2/select-one 'Dimension :entity_id entity_id)
                              (serdes/extract-one "Dimension" {})
                              clean-entity)))))

              (testing "for metrics"
                (doseq [{:keys [entity_id] :as metric} (get @entities "Metric")]
                  (is (= (clean-entity metric)
                         (->> (t2/select-one 'Metric :entity_id entity_id)
                              (serdes/extract-one "Metric" {})
                              clean-entity)))))

              (testing "for segments"
                (doseq [{:keys [entity_id] :as segment} (get @entities "Segment")]
                  (is (= (clean-entity segment)
                         (->> (t2/select-one 'Segment :entity_id entity_id)
                              (serdes/extract-one "Segment" {})
                              clean-entity)))))

              (testing "for native query snippets"
                (doseq [{:keys [entity_id] :as snippet} (get @entities "NativeQuerySnippet")]
                  (is (= (clean-entity snippet)
                         (->> (t2/select-one 'NativeQuerySnippet :entity_id entity_id)
                              (serdes/extract-one "NativeQuerySnippet" {})
                              clean-entity)))))

              (testing "for timelines and events"
                (doseq [{:keys [entity_id] :as timeline} (get @entities "Timeline")]
                  (is (= (clean-entity timeline)
                         (->> (t2/select-one 'Timeline :entity_id entity_id)
                              (serdes/extract-one "Timeline" {})
                              clean-entity)))))

              (testing "for settings"
                (is (= (into {} (for [{:keys [key value]} (get @entities "Setting")]
                                  [key value]))
                       (yaml/from-file (io/file dump-dir "settings.yaml"))))))))))))

;; This is a seperate test instead of a `testing` block inside `e2e-storage-ingestion-test`
;; because it's quite tricky to set up the generative test to generate parameters with source is card
(deftest card-and-dashboard-has-parameter-with-source-is-card-test
  (testing "Dashboard and Card that has parameter with source is a card must be deserialized correctly"
    (ts/with-random-dump-dir [dump-dir "serdesv2-"]
      (ts/with-source-and-dest-dbs
        (ts/with-source-db
          ;; preparation
          (mt/with-temp*
            [Database   [db1s {:name "my-db"}]
             Collection [coll1s {:name "My Collection"}]
             Table      [table1s {:name  "CUSTOMERS"
                                  :db_id (:id db1s)}]
             Field      [field1s {:name     "NAME"
                                  :table_id (:id table1s)}]
             Card       [card1s  {:name "Source card"}]
             Card       [card2s  {:name "Card with parameter"
                                  :database_id (:id db1s)
                                  :table_id (:id table1s)
                                  :collection_id (:id coll1s)
                                  :parameters [{:id                   "abc"
                                                :type                 "category"
                                                :name                 "CATEGORY"
                                                :values_source_type   "card"
                                                ;; card_id is in a different collection with dashboard's collection
                                                :values_source_config {:card_id     (:id card1s)
                                                                       :value_field [:field (:id field1s) nil]}}]}]
             Dashboard  [dash1s {:name "A dashboard"
                                 :collection_id (:id coll1s)
                                 :parameters [{:id                   "abc"
                                               :type                 "category"
                                               :name                 "CATEGORY"
                                               :values_source_type   "card"
                                               ;; card_id is in a different collection with dashboard's collection
                                               :values_source_config {:card_id     (:id card1s)
                                                                      :value_field [:field (:id field1s) nil]}}]}]]

            (testing "make sure we insert ParameterCard when insert Dashboard/Card"
              ;; one for parameter on card card2s, and one for parmeter on dashboard dash1s
              (is (= 2 (t2/count ParameterCard))))

            (testing "extract and store"
              (let [extraction (into [] (extract/extract-metabase {}))]
                (is (= [{:id                   "abc",
                         :name                 "CATEGORY",
                         :type                 :category,
                         :values_source_config {:card_id     (:entity_id card1s),
                                                :value_field [:field
                                                              ["my-db" nil "CUSTOMERS" "NAME"]
                                                              nil]},
                         :values_source_type "card"}]
                       (:parameters (first (by-model extraction "Dashboard")))))

                (is (= [{:id                   "abc",
                         :name                 "CATEGORY",
                         :type                 :category,
                         :values_source_config {:card_id     (:entity_id card1s),
                                                :value_field [:field
                                                              ["my-db" nil "CUSTOMERS" "NAME"]
                                                              nil]},
                         :values_source_type "card"}]
                       (:parameters (first (by-model extraction "Card")))))

                (storage/store! (seq extraction) dump-dir)))

            (testing "ingest and load"
              (ts/with-dest-db
                ;; ingest
                (testing "doing ingestion"
                  (is (serdes.load/load-metabase (ingest/ingest-yaml dump-dir))
                      "successful"))

                (let [dash1d (t2/select-one Dashboard :name (:name dash1s))
                      card1d (t2/select-one Card :name (:name card1s))
                      card2d (t2/select-one Card :name (:name card2s))
                      field1d (t2/select-one Field :name (:name field1s))]
                  (testing "parameter on dashboard is loaded correctly"
                    (is (= {:card_id     (:id card1d),
                            :value_field [:field (:id field1d) nil]}
                           (-> dash1d
                               :parameters
                               first
                               :values_source_config)))
                    (is (some? (t2/select-one 'ParameterCard :parameterized_object_type "dashboard" :parameterized_object_id (:id dash1d)))))

                  (testing "parameter on card is loaded correctly"
                    (is (= {:card_id     (:id card1d),
                            :value_field [:field (:id field1d) nil]}
                           (-> card2d
                               :parameters
                               first
                               :values_source_config)))
                    (is (some? (t2/select-one 'ParameterCard :parameterized_object_type "card" :parameterized_object_id (:id card2d))))))))))))))

(deftest dashcards-with-link-cards-test
  (ts/with-random-dump-dir [dump-dir "serdesv2-"]
    (ts/with-source-and-dest-dbs
      (ts/with-source-db
        (let [link-card-viz-setting (fn [model id]
                                      {:virtual_card {:display "link"}
                                       :link         {:entity {:id    id
                                                               :model model}}})
              dashboard->link-cards (fn [dashboard]
                                      (map #(get-in % [:visualization_settings :link :entity]) (:ordered_cards dashboard)))]
          (t2.with-temp/with-temp
            [Collection    {coll-id   :id
                            coll-name :name
                            coll-eid  :entity_id}    {:name        "Link collection"
                                                      :description "Linked Collection"}
             Database      {db-id   :id
                            db-name :name}           {:name        "Linked database"
                                                      :description "Linked database desc"}
             Table         {table-id   :id
                            table-name :name}        {:db_id        db-id
                                                      :schema      "Public"
                                                      :name        "Linked table"
                                                      :description "Linked table desc"}
             Card          {card-id   :id
                            card-name :name
                            card-eid  :entity_id}    {:name          "Linked card"
                                                      :description   "Linked card desc"
                                                      :display       "bar"}

             Card          {model-id   :id
                            model-name :name
                            model-eid  :entity_id}   {:dataset       true
                                                      :name          "Linked model"
                                                      :description   "Linked model desc"
                                                      :display       "table"}

             Dashboard     {dash-id   :id
                            dash-name :name
                            dash-eid  :entity_id}    {:name          "Linked Dashboard"
                                                      :collection_id coll-id
                                                      :description   "Linked Dashboard desc"}
             Dashboard     {dashboard-id   :id
                            dashboard-name :name}    {:name          "Test Dashboard"
                                                      :collection_id coll-id}
             DashboardCard _                         {:dashboard_id           dashboard-id
                                                      :visualization_settings (link-card-viz-setting "collection" coll-id)}
             DashboardCard _                         {:dashboard_id           dashboard-id
                                                      :visualization_settings (link-card-viz-setting "database" db-id)}
             DashboardCard _                         {:dashboard_id           dashboard-id
                                                      :visualization_settings (link-card-viz-setting "table" table-id)}
             DashboardCard _                         {:dashboard_id           dashboard-id
                                                      :visualization_settings (link-card-viz-setting "dashboard" dash-id)}
             DashboardCard _                         {:dashboard_id           dashboard-id
                                                      :visualization_settings (link-card-viz-setting "card" card-id)}
             DashboardCard _                         {:dashboard_id           dashboard-id
                                                      :visualization_settings (link-card-viz-setting "dataset" model-id)}]
            (testing "extract and store"
              (let [extraction          (into [] (extract/extract-metabase {}))
                    extracted-dashboard (first (filter #(= (:name %) "Test Dashboard") (by-model extraction "Dashboard")))]
                (is (= [{:model "collection" :id coll-eid}
                        {:model "database"   :id "Linked database"}
                        {:model "table"      :id ["Linked database" "Public" "Linked table"]}
                        {:model "dashboard"  :id dash-eid}
                        {:model "card"       :id card-eid}
                        {:model "dataset"    :id model-eid}]
                       (dashboard->link-cards extracted-dashboard)))

               (is (= #{[{:id dash-eid          :model "Dashboard"}]
                        [{:id coll-eid          :model "Collection"}]
                        [{:id model-eid         :model "Card"}]
                        [{:id card-eid          :model "Card"}]
                        [{:id "Linked database" :model "Database"}]
                        [{:model "Database" :id "Linked database"}
                         {:model "Schema"   :id "Public"}
                         {:model "Table"    :id "Linked table"}]}
                    (set (serdes/dependencies extracted-dashboard))))

               (storage/store! (seq extraction) dump-dir)))

            (testing "ingest and load"
              ;; ingest
              (ts/with-dest-db
                (testing "doing ingestion"
                  (is (serdes.load/load-metabase (ingest/ingest-yaml dump-dir))
                      "successful"))

                (doseq [[name model]
                        [[db-name    'Database]
                         [table-name 'Table]
                         [card-name  'Card]
                         [model-name 'Card]
                         [dash-name  'Dashboard]]]
                  (testing (format "model %s from link cards are loaded properly" model)
                   (is (some? (t2/select model :name name)))))

                (testing "linkcards are loaded with correct fk"
                  (let [new-db-id    (t2/select-one-pk Database :name db-name)
                        new-table-id (t2/select-one-pk Table :name table-name)
                        new-card-id  (t2/select-one-pk Card :name card-name)
                        new-model-id (t2/select-one-pk Card :name model-name)
                        new-dash-id  (t2/select-one-pk Dashboard :name dash-name)
                        new-coll-id  (t2/select-one-pk Collection :name coll-name)]
                    (is (= [{:id new-coll-id  :model "collection"}
                            {:id new-db-id    :model "database"}
                            {:id new-table-id :model "table"}
                            {:id new-dash-id  :model "dashboard"}
                            {:id new-card-id  :model "card"}
                            {:id new-model-id :model "dataset"}]
                           (-> (t2/select-one Dashboard :name dashboard-name)
                               (t2/hydrate :ordered_cards)
                               dashboard->link-cards)))))))))))))