diff --git a/resources/migrations/000_migrations.yaml b/resources/migrations/000_migrations.yaml
index db651b4e310dcc7f1f04377a87d5b7474347a203..20a23d4d346fb82cc4846e382203db11e37f2aae 100644
--- a/resources/migrations/000_migrations.yaml
+++ b/resources/migrations/000_migrations.yaml
@@ -6380,9 +6380,8 @@ databaseChangeLog:
             columnName: started_at
             newDataType: ${timestamp_type}
 
-# Remove `Table.rows`, which hasn't been used for years now. Older versions of
-# Metabase used to store the row count in this column but we disabled it a long
-# time ago for performance reasons. Now it's time to remove it entirely.
+# Remove `Table.rows`, which hasn't been used for years now. Older versions of Metabase used to store the row count in
+# this column but we disabled it a long time ago for performance reasons. Now it's time to remove it entirely.
 
   - changeSet:
       id: 169
@@ -6392,3 +6391,45 @@ databaseChangeLog:
         - dropColumn:
             tableName: metabase_table
             columnName: rows
+
+# In EE, NativeQuerySnippets have a permissions system based on "snippet folders" which are Collections under the
+# hood. However, these Collections live in a separate "namespace" -- a completely separate hierarchy of Collections.
+
+  - changeSet:
+      id: 171
+      author: camsaul
+      comment: Added 1.36.0
+      changes:
+        - addColumn:
+            tableName: native_query_snippet
+            columns:
+              - column:
+                  name: collection_id
+                  type: int
+                  remarks: 'ID of the Snippet Folder (Collection) this Snippet is in, if any'
+                  constraints:
+                    nullable: true
+                    references: collection(id)
+                    foreignKeyName: fk_snippet_collection_id
+                    deleteCascade: true
+        - createIndex:
+            tableName: native_query_snippet
+            indexName: idx_snippet_collection_id
+            columns:
+              - column:
+                  name: collection_id
+
+  - changeSet:
+      id: 172
+      author: camsaul
+      comment: Added 1.36.0
+      changes:
+        - addColumn:
+            tableName: collection
+            columns:
+              - column:
+                  name: namespace
+                  type: varchar(254)
+                  remarks: 'The namespace (hierachy) this Collection belongs to. NULL means the Collection is in the default namespace.'
+                  constraints:
+                    nullable: true
diff --git a/src/metabase/api/collection.clj b/src/metabase/api/collection.clj
index 672d053d997a8b2292f1434eb44cb4a577332de8..cda01049a3fdecdcb7575cdedfa227662e75008a 100644
--- a/src/metabase/api/collection.clj
+++ b/src/metabase/api/collection.clj
@@ -1,5 +1,9 @@
 (ns metabase.api.collection
-  "/api/collection endpoints."
+  "`/api/collection` endpoints. By default, these endpoints operate on Collections in the 'default' namespace, which is
+  the one that has things like Dashboards and Cards. Other namespaces of Collections exist as well, such as the
+  `:snippet` namespace, (called 'Snippet folders' in the UI). These namespaces are completely independent hierarchies.
+  To use these endpoints for other Collections namespaces, you can pass the `?namespace=` parameter (e.g.
+  `?namespace=snippet`)."
   (:require [clojure.string :as str]
             [compojure.core :refer [GET POST PUT]]
             [metabase.api
@@ -10,8 +14,12 @@
              [collection :as collection :refer [Collection]]
              [dashboard :refer [Dashboard]]
              [interface :as mi]
+             [native-query-snippet :refer [NativeQuerySnippet]]
              [permissions :as perms]
              [pulse :as pulse :refer [Pulse]]]
+            [metabase.models.collection
+             [graph :as collection.graph]
+             [root :as collection.root]]
             [metabase.util :as u]
             [metabase.util.schema :as su]
             [schema.core :as s]
@@ -27,28 +35,35 @@
 
   By default, this returns non-archived Collections, but instead you can show archived ones by passing
   `?archived=true`."
-  [archived]
-  {archived (s/maybe su/BooleanString)}
+  [archived namespace]
+  {archived  (s/maybe su/BooleanString)
+   namespace (s/maybe su/NonBlankString)}
   (let [archived? (Boolean/parseBoolean archived)]
-    (as-> (db/select Collection :archived archived?
-                     {:order-by [[:%lower.name :asc]]}) collections
+    (as-> (db/select Collection
+            :archived archived?
+            :namespace namespace
+            {:order-by [[:%lower.name :asc]]}) collections
       (filter mi/can-read? collections)
       ;; include Root Collection at beginning or results if archived isn't `true`
       (if archived?
         collections
         (cons (root-collection) collections))
       (hydrate collections :can_write)
-      ;; remove the :metabase.models.collection/is-root? tag since FE doesn't need it
+      ;; remove the :metabase.models.collection.root/is-root? tag since FE doesn't need it
       (for [collection collections]
-        (dissoc collection ::collection/is-root?)))))
+        (dissoc collection ::collection.root/is-root?)))))
 
 
 ;;; --------------------------------- Fetching a single Collection & its 'children' ----------------------------------
 
+(def ^:private valid-model-param-values
+  "Valid values for the `?model=` param accepted by endpoints in this namespace."
+  #{"card" "collection" "dashboard" "pulse" "snippet"})
+
 (def ^:private CollectionChildrenOptions
   {:archived? s/Bool
    ;; when specified, only return results of this type.
-   :model     (s/maybe (s/enum :card :dashboard :pulse :collection))})
+   :model     (s/maybe (apply s/enum (map keyword valid-model-param-values)))})
 
 (defmulti ^:private fetch-collection-children
   "Functions for fetching the 'children' of a `collection`, for different types of objects. Possible options are listed
@@ -63,38 +78,59 @@
 (defmethod fetch-collection-children :card
   [_ collection {:keys [archived?]}]
   (-> (db/select [Card :id :name :description :collection_position :display]
+        ;; use `:id` here so if it's the root collection we get `id = nil` (no ID)
         :collection_id (:id collection)
-        :archived      archived?)
+        :archived      (boolean archived?))
       (hydrate :favorite)))
 
 (defmethod fetch-collection-children :dashboard
   [_ collection {:keys [archived?]}]
   (db/select [Dashboard :id :name :description :collection_position]
     :collection_id (:id collection)
-    :archived      archived?))
+    :archived      (boolean archived?)))
 
 (defmethod fetch-collection-children :pulse
   [_ collection {:keys [archived?]}]
   (db/select [Pulse :id :name :collection_position]
     :collection_id   (:id collection)
-    :archived        archived?
+    :archived        (boolean archived?)
     ;; exclude Alerts
     :alert_condition nil))
 
-(defmethod fetch-collection-children :collection
+(defmethod fetch-collection-children :snippet
   [_ collection {:keys [archived?]}]
-  (-> (for [child-collection (collection/effective-children collection [:= :archived archived?])]
+  (db/select [NativeQuerySnippet :id :name]
+    :collection_id (:id collection)
+    :archived      (boolean archived?)))
+
+(defmethod fetch-collection-children :collection
+  [_ collection {:keys [archived? collection-namespace]}]
+  (-> (for [child-collection (collection/effective-children collection
+                                                            [:= :archived archived?]
+                                                            [:= :namespace (u/qualified-name collection-namespace)])]
         (assoc child-collection :model "collection"))
       (hydrate :can_write)))
 
+(defn- model-name->toucan-model [model-name]
+  (case (keyword model-name)
+    :collection Collection
+    :card       Card
+    :dashboard  Dashboard
+    :pulse      Pulse
+    :snippet    NativeQuerySnippet))
+
 (s/defn ^:private collection-children
   "Fetch a sequence of 'child' objects belonging to a Collection, filtered using `options`."
-  [collection                                     :- collection/CollectionWithLocationAndIDOrRoot
-   {:keys [model collections-only?], :as options} :- CollectionChildrenOptions]
-  (->> (for [model-kw [:collection :card :dashboard :pulse]
-            ;; only fetch models that are specified by the `model` param; or everything if it's `nil`
-            :when    (or (not model) (= model model-kw))
-            item     (fetch-collection-children model-kw collection options)]
+  [{collection-namespace :namespace, :as collection} :- collection/CollectionWithLocationAndIDOrRoot
+   {:keys [model collections-only?], :as options}    :- CollectionChildrenOptions]
+  (->> (for [model-kw [:collection :card :dashboard :pulse :snippet]
+             ;; only fetch models that are specified by the `model` param; or everything if it's `nil`
+             :when    (or (not model) (= model model-kw))
+             :let     [toucan-model       (model-name->toucan-model model-kw)
+                       allowed-namespaces (collection/allowed-namespaces toucan-model)]
+             :when    (or (= model-kw :collection)
+                          (contains? allowed-namespaces (keyword collection-namespace)))
+             item     (fetch-collection-children model-kw collection (assoc options :collection-namespace collection-namespace))]
          (assoc item :model model-kw))
        ;; sorting by name should be fine for now.
        (sort-by (comp str/lower-case :name))))
@@ -106,12 +142,6 @@
   (-> collection
       (hydrate :parent_id :effective_location [:effective_ancestors :can_write] :can_write)))
 
-(s/defn ^:private collection-items
-  "Return items in the Collection, restricted by `children-options`.
-  Works for either a normal Collection or the Root Collection."
-  [collection :- collection/CollectionWithLocationAndIDOrRoot, children-options :- CollectionChildrenOptions]
-  (collection-children collection children-options))
-
 (api/defendpoint GET "/:id"
   "Fetch a specific Collection with standard details added"
   [id]
@@ -123,9 +153,9 @@
   *  `model` - only include objects of a specific `model`. If unspecified, returns objects of all models
   *  `archived` - when `true`, return archived objects *instead* of unarchived ones. Defaults to `false`."
   [id model archived]
-  {model    (s/maybe (s/enum "card" "dashboard" "pulse" "collection"))
+  {model    (s/maybe (apply s/enum valid-model-param-values))
    archived (s/maybe su/BooleanString)}
-  (collection-items
+  (collection-children
     (api/read-check Collection id)
     {:model     (keyword model)
      :archived? (Boolean/parseBoolean archived)}))
@@ -139,7 +169,7 @@
 (api/defendpoint GET "/root"
   "Return the 'Root' Collection object with standard details added"
   []
-  (dissoc (root-collection) ::collection/is-root?))
+  (dissoc (root-collection) ::collection.root/is-root?))
 
 (api/defendpoint GET "/root/items"
   "Fetch objects that the current user should see at their root level. As mentioned elsewhere, the 'Root' Collection
@@ -151,18 +181,22 @@
   location of `/`.
 
   This endpoint is intended to power a 'Root Folder View' for the Current User, so regardless you'll see all the
-  top-level objects you're allowed to access."
-  [model archived]
-  {model    (s/maybe (s/enum "card" "dashboard" "pulse" "collection"))
-   archived (s/maybe su/BooleanString)}
+  top-level objects you're allowed to access.
+
+  By default, this will show the 'normal' Collections namespace; to view a different Collections namespace, such as
+  `snippets`, you can pass the `?namespace=` parameter."
+  [model archived namespace]
+  {model     (s/maybe (apply s/enum valid-model-param-values))
+   archived  (s/maybe su/BooleanString)
+   namespace (s/maybe su/NonBlankString)}
   ;; Return collection contents, including Collections that have an effective location of being in the Root
   ;; Collection for the Current User.
-  (collection-items
-    collection/root-collection
-    {:model     (if (mi/can-read? collection/root-collection)
-                  (keyword model)
-                  :collection)
-     :archived? (Boolean/parseBoolean archived)}))
+  (collection-children
+   (assoc collection/root-collection :namespace namespace)
+   {:model     (if (mi/can-read? collection/root-collection)
+                 (keyword model)
+                 :collection)
+    :archived? (Boolean/parseBoolean archived)}))
 
 
 ;;; ----------------------------------------- Creating/Editing a Collection ------------------------------------------
@@ -177,11 +211,12 @@
 
 (api/defendpoint POST "/"
   "Create a new Collection."
-  [:as {{:keys [name color description parent_id]} :body}]
+  [:as {{:keys [name color description parent_id namespace]} :body}]
   {name        su/NonBlankString
    color       collection/hex-color-regex
    description (s/maybe su/NonBlankString)
-   parent_id   (s/maybe su/IntGreaterThanZero)}
+   parent_id   (s/maybe su/IntGreaterThanZero)
+   namespace   (s/maybe su/NonBlankString)}
   ;; To create a new collection, you need write perms for the location you are going to be putting it in...
   (write-check-collection-or-root-collection parent_id)
   ;; Now create the new Collection :)
@@ -189,7 +224,8 @@
     (merge
      {:name        name
       :color       color
-      :description description}
+      :description description
+      :namespace   namespace}
      (when parent_id
        {:location (collection/children-location (db/select-one [Collection :location :id] :id parent_id))}))))
 
@@ -270,10 +306,10 @@
 
 (api/defendpoint GET "/graph"
   "Fetch a graph of all Collection Permissions."
-  []
+  [namespace]
+  {namespace (s/maybe su/NonBlankString)}
   (api/check-superuser)
-  (collection/graph))
-
+  (collection.graph/graph namespace))
 
 (defn- ->int [id] (Integer/parseInt (name id)))
 
@@ -296,11 +332,12 @@
 
 (api/defendpoint PUT "/graph"
   "Do a batch update of Collections Permissions by passing in a modified graph."
-  [:as {body :body}]
-  {body su/Map}
+  [namespace :as {body :body}]
+  {body      su/Map
+   namespace (s/maybe su/NonBlankString)}
   (api/check-superuser)
-  (collection/update-graph! (dejsonify-graph body))
-  (collection/graph))
+  (collection.graph/update-graph! namespace (dejsonify-graph body))
+  (collection.graph/graph namespace))
 
 
 (api/define-routes)
diff --git a/src/metabase/api/permissions.clj b/src/metabase/api/permissions.clj
index 130fcd3aa28aef74cd95316de0091975444e6604..e152c7da1f7bd2f28ecced9e07e7b508369e2034 100644
--- a/src/metabase/api/permissions.clj
+++ b/src/metabase/api/permissions.clj
@@ -28,7 +28,6 @@
   (api/check-superuser)
   (perms/graph))
 
-
 (api/defendpoint PUT "/graph"
   "Do a batch update of Permissions by passing in a modified graph. This should return the same graph, in the same
   format, that you got from `GET /api/permissions/graph`, with any changes made in the wherever necessary. This
diff --git a/src/metabase/api/search.clj b/src/metabase/api/search.clj
index d17d943cd7bc8617326d5e36578a833d996661ee..a11f6ab3cdead049c08e0b3a33e9eefda01608ff 100644
--- a/src/metabase/api/search.clj
+++ b/src/metabase/api/search.clj
@@ -260,7 +260,9 @@
         collection-filter-clause (coll/visible-collection-ids->honeysql-filter-clause
                                   collection-id-column
                                   visible-collections)
-        honeysql-query           (h/merge-where honeysql-query collection-filter-clause)]
+        honeysql-query           (-> honeysql-query
+                                     (h/merge-where collection-filter-clause)
+                                     (h/merge-where [:= :collection.namespace nil]))]
     ;; add a JOIN against Collection *unless* the source table is already Collection
     (cond-> honeysql-query
       (not= collection-id-column :collection.id)
diff --git a/src/metabase/models/card.clj b/src/metabase/models/card.clj
index 014b12eb09cc6f0830682b55e5da2d084e0ab6c6..5024026ec1421b1b31c2c67e330bb0ea784e440a 100644
--- a/src/metabase/models/card.clj
+++ b/src/metabase/models/card.clj
@@ -11,6 +11,7 @@
              [normalize :as normalize]
              [util :as mbql.u]]
             [metabase.models
+             [collection :as collection]
              [dependency :as dependency]
              [field-values :as field-values]
              [interface :as i]
@@ -117,7 +118,8 @@
         (throw (Exception. (tru "You do not have permissions to run ad-hoc native queries against Database {0}."
                                 (:database query))))))
     ;; make sure this Card doesn't have circular source query references
-    (check-for-circular-source-query-references card)))
+    (check-for-circular-source-query-references card)
+    (collection/check-collection-namespace card)))
 
 (defn- post-insert [card]
   ;; if this Card has any native template tag parameters we need to update FieldValues for any Fields that are
@@ -151,7 +153,8 @@
             (field-values/update-field-values-for-on-demand-dbs! newly-added-param-field-ids)))))
     ;; make sure this Card doesn't have circular source query references if we're updating the query
     (when (:dataset_query card)
-      (check-for-circular-source-query-references card))))
+      (check-for-circular-source-query-references card))
+    (collection/check-collection-namespace card)))
 
 ;; Cards don't normally get deleted (they get archived instead) so this mostly affects tests
 (defn- pre-delete [{:keys [id]}]
@@ -161,7 +164,6 @@
   (db/delete! 'DashboardCard :card_id id)
   (db/delete! 'CardFavorite :card_id id))
 
-
 (u/strict-extend (class Card)
   models/IModel
   (merge models/IModelDefaults
diff --git a/src/metabase/models/collection.clj b/src/metabase/models/collection.clj
index d16d8c2590ec72326d77b2b29d5b87bf57c22f7d..37a4359225c38220e50adef70dcf854283e9ecb0 100644
--- a/src/metabase/models/collection.clj
+++ b/src/metabase/models/collection.clj
@@ -1,30 +1,31 @@
 (ns metabase.models.collection
   "Collections are used to organize Cards, Dashboards, and Pulses; as of v0.30, they are the primary way we determine
   permissions for these objects.
-
-  TODO - I think this namespace is too big now! Maybe move the graph stuff into somewhere like
-  `metabase.models.collection.graph`"
+  `metabase.models.collection.graph`. `metabase.models.collection.graph`"
   (:refer-clojure :exclude [ancestors descendants])
-  (:require [clojure
-             [data :as data]
-             [string :as str]]
-            [clojure.core.memoize :as memoize]
+  (:require [clojure.core.memoize :as memoize]
+            [clojure.string :as str]
             [clojure.tools.logging :as log]
             [honeysql.core :as hsql]
             [metabase.api.common :as api :refer [*current-user-id* *current-user-permissions-set*]]
             [metabase.models
-             [collection-revision :as collection-revision :refer [CollectionRevision]]
              [interface :as i]
              [permissions :as perms :refer [Permissions]]]
+            [metabase.models.collection.root :as collection.root]
             [metabase.util :as u]
             [metabase.util
              [i18n :as ui18n :refer [trs tru]]
              [schema :as su]]
-            [potemkin.types :as p.types]
+            [potemkin :as p]
             [schema.core :as s]
             [toucan
              [db :as db]
-             [models :as models]]))
+             [models :as models]])
+  (:import metabase.models.collection.root.RootCollection))
+
+(comment collection.root/keep-me)
+
+(p/import-vars [collection.root root-collection])
 
 (def ^:private ^:const collection-slug-max-length
   "Maximum number of characters allowed in a Collection `slug`."
@@ -45,7 +46,7 @@
   (when (or (not (string? hex-color))
             (not (re-matches hex-color-regex hex-color)))
     (throw (ex-info (tru "Invalid color")
-             {:status-code 400, :errors {:color (tru "must be a valid 6-character hex color code")}}))))
+                    {:status-code 400, :errors {:color (tru "must be a valid 6-character hex color code")}}))))
 
 (defn- slugify [collection-name]
   ;; double-check that someone isn't trying to use a blank string as the collection name
@@ -83,17 +84,17 @@
     (Integer/parseInt id-str)))
 
 (defn- valid-location-path? [s]
-  (and (string? s)
-       (seq s)
-       (or (= s "/")
-           (and (re-matches #"/(\d+/)*" s)
-                (apply distinct? (unchecked-location-path->ids s))))))
+  (boolean
+   (and (string? s)
+        (re-matches #"^/(\d+/)*$" s)
+        (let [ids (unchecked-location-path->ids s)]
+          (or (empty? ids)
+              (apply distinct? ids))))))
 
 (def LocationPath
   "Schema for a directory-style 'path' to the location of a Collection."
   (s/pred valid-location-path?))
 
-
 (s/defn location-path :- LocationPath
   "Build a 'location path' from a sequence of `collections-or-ids`.
 
@@ -142,59 +143,40 @@
   ;; if setting/updating the `location` of this Collection make sure it matches the schema for valid location paths
   (when (contains? collection :location)
     (when-not (valid-location-path? location)
-      (throw
-       (ex-info (tru "Invalid Collection location: path is invalid.")
-         {:status-code 400
-          :errors      {:location (tru "Invalid Collection location: path is invalid.")}})))
+      (let [msg (tru "Invalid Collection location: path is invalid.")]
+        (throw (ex-info msg {:status-code 400, :errors {:location msg}}))))
     ;; if this is a Personal Collection it's only allowed to go in the Root Collection: you can't put it anywhere else!
     (when (contains? collection :personal_owner_id)
       (when-not (= location "/")
-        (throw
-         (ex-info (tru "You cannot move a Personal Collection.")
-           {:status-code 400
-            :errors      {:location (tru "You cannot move a Personal Collection.")}}))))
+        (let [msg (tru "You cannot move a Personal Collection.")]
+          (throw (ex-info msg {:status-code 400, :errors {:location msg}})))))
     ;; Also make sure that all the IDs referenced in the Location path actually correspond to real Collections
     (when-not (all-ids-in-location-path-are-valid? location)
-      (throw
-       (ex-info (tru "Invalid Collection location: some or all ancestors do not exist.")
-         {:status-code 404
-          :errors      {:location (tru "Invalid Collection location: some or all ancestors do not exist.")}})))))
-
-
-;;; +----------------------------------------------------------------------------------------------------------------+
-;;; |                                   Root Collection Special Placeholder Object                                   |
-;;; +----------------------------------------------------------------------------------------------------------------+
-
-;; The Root Collection special placeholder object is used to represent the fact that we're working with the 'Root'
-;; Collection in many of the functions in this namespace. The Root Collection is not a true Collection, but instead
-;; represents things that have no collection_id, or are otherwise to be seen at the top-level by the current user.
-
-(p.types/defrecord+ ^:private RootCollection [])
-
-(u/strict-extend RootCollection
-  i/IObjectPermissions
-  (merge
-   i/IObjectPermissionsDefaults
-   {:perms-objects-set (fn [this read-or-write]
-                         #{((case read-or-write
-                               :read  perms/collection-read-path
-                               :write perms/collection-readwrite-path) this)})
-    :can-read?         (partial i/current-user-has-full-permissions? :read)
-    :can-write?        (partial i/current-user-has-full-permissions? :write)}))
-
-(def ^RootCollection root-collection
-  "Special placeholder object representing the Root Collection, which isn't really a real Collection."
-  (map->RootCollection {::is-root? true}))
+      (let [msg (tru "Invalid Collection location: some or all ancestors do not exist.")]
+        (throw (ex-info msg {:status-code 404, :errors {:location msg}}))))))
+
+(defn- assert-valid-namespace
+  "Check that the namespace of this Collection is valid -- it must belong to the same namespace as its parent
+  Collection."
+  [{:keys [location], owner-id :personal_owner_id, collection-namespace :namespace, :as collection}]
+  {:pre [(contains? collection :namespace)]}
+  (when location
+    (when-let [parent-id (location-path->parent-id location)]
+      (let [parent-namespace (db/select-one-field :namespace Collection :id parent-id)]
+        (when-not (= (keyword collection-namespace) (keyword parent-namespace))
+          (let [msg (tru "Collection must be in the same namespace as its parent")]
+            (throw (ex-info msg {:status-code 400, :errors {:location msg}})))))))
+  ;; non-default namespace Collections cannot be personal Collections
+  (when (and owner-id collection-namespace)
+    (let [msg (tru "Personal Collections must be in the default namespace")]
+      (throw (ex-info msg {:status-code 400, :errors {:personal_owner_id msg}})))))
 
 (defn root-collection-with-ui-details
   "The special Root Collection placeholder object with some extra details to facilitate displaying it on the FE."
   []
   (assoc root-collection
-    :name (tru "Our analytics")
-    :id   "root"))
-
-(defn- is-root-collection? [x]
-  (instance? RootCollection x))
+         :name (tru "Our analytics")
+         :id   "root"))
 
 (def ^:private CollectionWithLocationOrRoot
   (s/cond-pre
@@ -309,7 +291,7 @@
   `:effective_location`."
   {:hydrate :effective_location}
   ([collection :- CollectionWithLocationOrRoot]
-   (if (is-root-collection? collection)
+   (if (collection.root/is-root-collection? collection)
      nil
      (effective-location-path (:location collection)
                               (permissions-set->visible-collection-ids @*current-user-permissions-set*))))
@@ -352,7 +334,7 @@
   can effectively treat A as the parent of C."
   {:hydrate :effective_ancestors}
   [collection :- CollectionWithLocationAndIDOrRoot]
-  (if (is-root-collection? collection)
+  (if (collection.root/is-root-collection? collection)
     []
     (filter i/can-read? (cons (root-collection-with-ui-details) (ancestors collection)))))
 
@@ -371,7 +353,7 @@
      ;; To get children of this collection:
      (db/select Collection :location \"/10/20/30/\")"
   [{:keys [location], :as collection} :- CollectionWithLocationAndIDOrRoot]
-  (if (is-root-collection? collection)
+  (if (collection.root/is-root-collection? collection)
     "/"
     (str location (u/get-id collection) "/")))
 
@@ -487,7 +469,7 @@
   *  C, because by archiving its parent, you are archiving it as well"
   [collection :- CollectionWithLocationAndIDOrRoot]
   ;; Make sure we're not trying to archive the Root Collection...
-  (when (is-root-collection? collection)
+  (when (collection.root/is-root-collection? collection)
     (throw (Exception. (tru "You cannot archive the Root Collection."))))
   ;; also make sure we're not trying to archive a PERSONAL Collection
   (when (db/exists? Collection :id (u/get-id collection), :personal_owner_id [:not= nil])
@@ -512,7 +494,6 @@
                ===>
     D                D > B > C
 
-
   To move or archive B, you would need write permissions for A, B, C, and D:
 
   *  A, because we're moving something out of it
@@ -521,7 +502,7 @@
   *  D, because it's the new parent Collection, and moving something into it requires write perms."
   [collection :- CollectionWithLocationAndIDOrRoot, new-parent :- CollectionWithLocationAndIDOrRoot]
   ;; Make sure we're not trying to move the Root Collection...
-  (when (is-root-collection? collection)
+  (when (collection.root/is-root-collection? collection)
     (throw (Exception. (tru "You cannot move the Root Collection."))))
   ;; Needless to say, it makes no sense to move a Collection into itself or into one of its descendants. So let's make
   ;; sure we're not doing that...
@@ -563,7 +544,7 @@
       (db/update-where! Collection {:id       [:in affected-collection-ids]
                                     :archived false}
         :archived true)
-      (doseq [model '[Card Dashboard Pulse]]
+      (doseq [model '[Card Dashboard NativeQuerySnippet Pulse]]
         (db/update-where! model {:collection_id [:in affected-collection-ids]
                                  :archived      false}
           :archived true)))))
@@ -577,7 +558,7 @@
       (db/update-where! Collection {:id       [:in affected-collection-ids]
                                     :archived true}
         :archived false)
-      (doseq [model '[Card Dashboard Pulse]]
+      (doseq [model '[Card Dashboard NativeQuerySnippet Pulse]]
         (db/update-where! model {:collection_id [:in affected-collection-ids]
                                  :archived      true}
           :archived false)))))
@@ -613,6 +594,7 @@
 
 (defn- pre-insert [{collection-name :name, color :color, :as collection}]
   (assert-valid-location collection)
+  (assert-valid-namespace (merge {:namespace nil} collection))
   (assert-valid-hex-color color)
   (assoc collection :slug (slugify collection-name)))
 
@@ -641,8 +623,8 @@
 
 (defn- copy-parent-permissions!
   "When creating a new Collection, we shall copy the Permissions entries for its parent. That way, Groups who can see
-  its parent can see it; and Groups who can 'curate' its parent can 'curate' it, as a default state. (Of course,
-  admins can change these permissions after the fact.)
+  its parent can see it; and Groups who can 'curate' (write) its parent can 'curate' it, as a default state. (Of
+  course, admins can change these permissions after the fact.)
 
   This does *not* apply to Collections that are created inside a Personal Collection or one of its descendants.
   Descendants of Personal Collections, like Personal Collections themselves, cannot have permissions entries in the
@@ -777,11 +759,17 @@
       (check-changes-allowed-for-personal-collection collection-before-updates collection-updates))
     ;; (2) make sure the location is valid if we're changing it
     (assert-valid-location collection-updates)
-    ;; (3) If we're moving a Collection from a location on a Personal Collection hierarchy to a location not on one,
+    ;; (3) make sure Collection namespace is valid
+    (when (contains? collection-updates :namespace)
+      (when (not= (:namespace collection-before-updates) (:namespace collection-updates))
+        (let [msg (tru "You cannot move a Collection to a different namespace once it has been created.")]
+          (throw (ex-info msg {:status-code 400, :errors {:namespace msg}})))))
+    (assert-valid-namespace (merge (select-keys collection-before-updates [:namespace]) collection-updates))
+    ;; (4) If we're moving a Collection from a location on a Personal Collection hierarchy to a location not on one,
     ;; or vice versa, we need to grant/revoke permissions as appropriate (see above for more details)
     (when (api/column-will-change? :location collection-before-updates collection-updates)
       (update-perms-when-moving-across-personal-boundry! collection-before-updates collection-updates))
-    ;; (4) make sure hex color is valid
+    ;; (5) make sure hex color is valid
     (when (api/column-will-change? :color collection-before-updates collection-updates)
       (assert-valid-hex-color color))
     ;; OK, AT THIS POINT THE CHANGES ARE VALIDATED. NOW START ISSUING UPDATES
@@ -837,6 +825,7 @@
   models/IModel
   (merge models/IModelDefaults
          {:hydration-keys (constantly [:collection])
+          :types          (constantly {:namespace :keyword})
           :pre-insert     pre-insert
           :post-insert    post-insert
           :pre-update     pre-update
@@ -848,130 +837,6 @@
           :perms-objects-set perms-objects-set}))
 
 
-;;; +----------------------------------------------------------------------------------------------------------------+
-;;; |                                               PERMISSIONS GRAPH                                                |
-;;; +----------------------------------------------------------------------------------------------------------------+
-
-;;; ---------------------------------------------------- Schemas -----------------------------------------------------
-
-(def ^:private CollectionPermissions
-  (s/enum :write :read :none))
-
-(def ^:private GroupPermissionsGraph
-  "collection-id -> status"
-  {(s/optional-key :root) CollectionPermissions   ; when doing a delta between old graph and new graph root won't always
-   su/IntGreaterThanZero  CollectionPermissions}) ; be present, which is why it's *optional*
-
-(def ^:private PermissionsGraph
-  {:revision s/Int
-   :groups   {su/IntGreaterThanZero GroupPermissionsGraph}})
-
-
-;;; -------------------------------------------------- Fetch Graph ---------------------------------------------------
-
-(defn- group-id->permissions-set []
-  (into {} (for [[group-id perms] (group-by :group_id (db/select 'Permissions))]
-             {group-id (set (map :object perms))})))
-
-(s/defn ^:private perms-type-for-collection :- CollectionPermissions
-  [permissions-set collection-or-id]
-  (cond
-    (perms/set-has-full-permissions? permissions-set (perms/collection-readwrite-path collection-or-id)) :write
-    (perms/set-has-full-permissions? permissions-set (perms/collection-read-path collection-or-id))      :read
-    :else                                                                                                :none))
-
-(s/defn ^:private group-permissions-graph :- GroupPermissionsGraph
-  "Return the permissions graph for a single group having PERMISSIONS-SET."
-  [permissions-set collection-ids]
-  (into
-   {:root (perms-type-for-collection permissions-set root-collection)}
-   (for [collection-id collection-ids]
-     {collection-id (perms-type-for-collection permissions-set collection-id)})))
-
-(s/defn ^:private non-personal-collection-ids :- #{su/IntGreaterThanZero}
-  "Return a set of IDs of all Collections that are neither Personal Collections nor descendants of Personal
-  Collections (i.e., things that you can set Permissions for, and that should go in the graph.)"
-  []
-  (->> (db/query
-        (merge
-         {:select [[:id :id]]
-          :from   [Collection]}
-         (when-let [personal-collection-ids (seq (db/select-ids Collection :personal_owner_id [:not= nil]))]
-           {:where (apply
-                    vector
-                    :and
-                    [:not-in :id personal-collection-ids]
-                    (for [id personal-collection-ids]
-                      [:not [:like :location (format "/%d/%%" id)]]))})))
-       (map :id)
-       set))
-
-(s/defn graph :- PermissionsGraph
-  "Fetch a graph representing the current permissions status for every group and all permissioned collections. This
-  works just like the function of the same name in `metabase.models.permissions`; see also the documentation for that
-  function."
-  []
-  (let [group-id->perms (group-id->permissions-set)
-        collection-ids  (non-personal-collection-ids)]
-    {:revision (collection-revision/latest-id)
-     :groups   (into {} (for [group-id (db/select-ids 'PermissionsGroup)]
-                          {group-id (group-permissions-graph (group-id->perms group-id) collection-ids)}))}))
-
-
-;;; -------------------------------------------------- Update Graph --------------------------------------------------
-
-(s/defn ^:private update-collection-permissions!
-  [group-id             :- su/IntGreaterThanZero
-   collection-id        :- (s/cond-pre (s/eq :root) su/IntGreaterThanZero)
-   new-collection-perms :- CollectionPermissions]
-  (let [collection-id (if (= collection-id :root)
-                        root-collection
-                        collection-id)]
-    ;; remove whatever entry is already there (if any) and add a new entry if applicable
-    (perms/revoke-collection-permissions! group-id collection-id)
-    (case new-collection-perms
-      :write (perms/grant-collection-readwrite-permissions! group-id collection-id)
-      :read  (perms/grant-collection-read-permissions! group-id collection-id)
-      :none  nil)))
-
-(s/defn ^:private update-group-permissions!
-  [group-id :- su/IntGreaterThanZero, new-group-perms :- GroupPermissionsGraph]
-  (doseq [[collection-id new-perms] new-group-perms]
-    (update-collection-permissions! group-id collection-id new-perms)))
-
-(defn- save-perms-revision!
-  "Save changes made to the collection permissions graph for logging/auditing purposes.
-   This doesn't do anything if `*current-user-id*` is unset (e.g. for testing or REPL usage)."
-  [current-revision old new]
-  (when *current-user-id*
-    ;; manually specify ID here so if one was somehow inserted in the meantime in the fraction of a second since we
-    ;; called `check-revision-numbers` the PK constraint will fail and the transaction will abort
-    (db/insert! CollectionRevision
-      :id     (inc current-revision)
-      :before  old
-      :after   new
-      :user_id *current-user-id*)))
-
-(s/defn update-graph!
-  "Update the collections permissions graph. This works just like the function of the same name in
-  `metabase.models.permissions`, but for `Collections`; refer to that function's extensive documentation to get a
-  sense for how this works."
-  ([new-graph :- PermissionsGraph]
-   (let [old-graph (graph)
-         [old new] (data/diff (:groups old-graph) (:groups new-graph))]
-     (perms/log-permissions-changes old new)
-     (perms/check-revision-numbers old-graph new-graph)
-     (when (seq new)
-       (db/transaction
-         (doseq [[group-id changes] new]
-           (update-group-permissions! group-id changes))
-         (save-perms-revision! (:revision old-graph) old new)))))
-  ;; The following arity is provided soley for convenience for tests/REPL usage
-  ([ks new-value]
-   {:pre [(sequential? ks)]}
-   (update-graph! (assoc-in (graph) (cons :groups ks) new-value))))
-
-
 ;;; +----------------------------------------------------------------------------------------------------------------+
 ;;; |                                           Perms Checking Helper Fns                                            |
 ;;; +----------------------------------------------------------------------------------------------------------------+
@@ -1085,3 +950,33 @@
       (for [user users]
         (assoc user :personal_collection_id (or (user-id->collection-id (u/get-id user))
                                                 (user->personal-collection-id (u/get-id user))))))))
+
+(defmulti allowed-namespaces
+  "Set of Collection namespaces instances of this model are allowed to go in. By default, only the default
+  namespace (namespace = `nil`)."
+  {:arglists '([model])}
+  class)
+
+(defmethod allowed-namespaces :default
+  [_]
+  #{nil})
+
+(defn check-collection-namespace
+  "Check that object's `:collection_id` refers to a Collection in an allowed namespace (see
+  `allowed-namespaces`), or throw an Exception.
+
+    ;; Cards can only go in Collections in the default namespace (namespace = nil)
+    (check-collection-namespace card)"
+  [{collection-id :collection_id, :as object}]
+  (when collection-id
+    (let [collection-namespace (keyword (db/select-one-field :namespace 'Collection :id collection-id))
+          allowed-namespaces   (allowed-namespaces object)]
+      (when-not (contains? allowed-namespaces collection-namespace)
+        (let [msg (tru "A {0} can only go in Collections in the {1} namespace."
+                       (name object)
+                       (str/join (format " %s " (tru "or")) (map #(pr-str (or % (tru "default")))
+                                                                 allowed-namespaces)))]
+          (throw (ex-info msg {:status-code          400
+                               :errors               {:collection_id msg}
+                               :allowed-namespaces   allowed-namespaces
+                               :collection-namespace collection-namespace})))))))
diff --git a/src/metabase/models/collection/graph.clj b/src/metabase/models/collection/graph.clj
new file mode 100644
index 0000000000000000000000000000000000000000..f98d0b97a4d78b580100f75c98df6713f9d4bd17
--- /dev/null
+++ b/src/metabase/models/collection/graph.clj
@@ -0,0 +1,161 @@
+(ns metabase.models.collection.graph
+  (:require [clojure.data :as data]
+            [honeysql.helpers :as h]
+            [metabase.api.common :as api :refer [*current-user-id*]]
+            [metabase.models
+             [collection :as collection :refer [Collection]]
+             [collection-revision :as collection-revision :refer [CollectionRevision]]
+             [permissions :as perms :refer [Permissions]]
+             [permissions-group :refer [PermissionsGroup]]]
+            [metabase.util :as u]
+            [metabase.util.schema :as su]
+            [schema.core :as s]
+            [toucan.db :as db]))
+
+;;; +----------------------------------------------------------------------------------------------------------------+
+;;; |                                               PERMISSIONS GRAPH                                                |
+;;; +----------------------------------------------------------------------------------------------------------------+
+
+;;; ---------------------------------------------------- Schemas -----------------------------------------------------
+
+(def ^:private CollectionPermissions
+  (s/enum :write :read :none))
+
+(def ^:private GroupPermissionsGraph
+  "collection-id -> status"
+  {(s/optional-key :root) CollectionPermissions   ; when doing a delta between old graph and new graph root won't always
+   su/IntGreaterThanZero  CollectionPermissions}) ; be present, which is why it's *optional*
+
+(def ^:private PermissionsGraph
+  {:revision s/Int
+   :groups   {su/IntGreaterThanZero GroupPermissionsGraph}})
+
+
+;;; -------------------------------------------------- Fetch Graph ---------------------------------------------------
+
+(defn- group-id->permissions-set []
+  (into {} (for [[group-id perms] (group-by :group_id (db/select 'Permissions))]
+             {group-id (set (map :object perms))})))
+
+(s/defn ^:private perms-type-for-collection :- CollectionPermissions
+  [permissions-set collection-or-id]
+  (cond
+    (perms/set-has-full-permissions? permissions-set (perms/collection-readwrite-path collection-or-id)) :write
+    (perms/set-has-full-permissions? permissions-set (perms/collection-read-path collection-or-id))      :read
+    :else                                                                                                :none))
+
+(s/defn ^:private group-permissions-graph :- GroupPermissionsGraph
+  "Return the permissions graph for a single group having `permissions-set`."
+  [permissions-set collection-ids]
+  (into
+   {:root (perms-type-for-collection permissions-set collection/root-collection)}
+   (for [collection-id collection-ids]
+     {collection-id (perms-type-for-collection permissions-set collection-id)})))
+
+(s/defn ^:private non-personal-collection-ids :- #{su/IntGreaterThanZero}
+  "Return a set of IDs of all Collections that are neither Personal Collections nor descendants of Personal
+  Collections (i.e., things that you can set Permissions for, and that should go in the graph.)"
+  [collection-namespace :- (s/maybe su/KeywordOrString)]
+  (let [personal-collection-ids (db/select-ids Collection :personal_owner_id [:not= nil])
+        honeysql-form           (cond-> {:select [[:id :id]]
+                                         :from   [Collection]
+                                         :where  (if (= collection-namespace ::all)
+                                                   [:= 1 1]
+                                                   [:= :namespace (u/qualified-name collection-namespace)])}
+                                  (seq personal-collection-ids)
+                                  (h/merge-where [:not-in :id (set personal-collection-ids)]))
+        honeysql-form           (reduce
+                                 (fn [honeysql-form collection-id]
+                                   (h/merge-where honeysql-form [:not [:like :location (format "/%d/%%" collection-id)]]))
+                                 honeysql-form
+                                 personal-collection-ids)]
+    (set (map :id (db/query honeysql-form)))))
+
+(s/defn graph :- PermissionsGraph
+  "Fetch a graph representing the current permissions status for every group and all permissioned collections. This
+  works just like the function of the same name in `metabase.models.permissions`; see also the documentation for that
+  function.
+
+  The graph is restricted to a given namespace by the optional `collection-namespace` param; by default, `nil`, which
+  restricts it to the 'default' namespace containing normal Card/Dashboard/Pulse Collections. Pass `::all` to get a
+  combined graph of all namespaces, for the purposes of recording graph revisions.
+
+  Note: All Collections are returned at the same level of the 'graph', regardless of how the Collection hierarchy is
+  structured. Collections do not inherit permissions from ancestor Collections in the same way data permissions are
+  inherited (e.g. full `:read` perms for a Database implies `:read` perms for all its schemas); a 'child' object (e.g.
+  schema) *cannot* have more restrictive permissions than its parent (e.g. Database). Child Collections *can* have
+  more restrictive permissions than their parent."
+  ([]
+   (graph nil))
+
+  ([collection-namespace :- (s/maybe su/KeywordOrString)]
+   (let [group-id->perms (group-id->permissions-set)
+         collection-ids  (non-personal-collection-ids collection-namespace)]
+     {:revision (collection-revision/latest-id)
+      :groups   (into {} (for [group-id (db/select-ids PermissionsGroup)]
+                           {group-id (group-permissions-graph (group-id->perms group-id) collection-ids)}))})))
+
+
+;;; -------------------------------------------------- Update Graph --------------------------------------------------
+
+(s/defn ^:private update-collection-permissions!
+  [group-id             :- su/IntGreaterThanZero
+   collection-id        :- (s/cond-pre (s/eq :root) su/IntGreaterThanZero)
+   new-collection-perms :- CollectionPermissions]
+  (let [collection-id (if (= collection-id :root)
+                        collection/root-collection
+                        collection-id)]
+    ;; remove whatever entry is already there (if any) and add a new entry if applicable
+    (perms/revoke-collection-permissions! group-id collection-id)
+    (case new-collection-perms
+      :write (perms/grant-collection-readwrite-permissions! group-id collection-id)
+      :read  (perms/grant-collection-read-permissions! group-id collection-id)
+      :none  nil)))
+
+(s/defn ^:private update-group-permissions!
+  [group-id :- su/IntGreaterThanZero, new-group-perms :- GroupPermissionsGraph]
+  (doseq [[collection-id new-perms] new-group-perms]
+    (update-collection-permissions! group-id collection-id new-perms)))
+
+(defn- save-perms-revision!
+  "Save changes made to the collection permissions graph for logging/auditing purposes. This doesn't do anything if
+  `*current-user-id*` is unset (e.g. for testing or REPL usage).
+
+  *  `before-graph`-- the entire graph as it existed before the revision.
+  *  `changes` -- set of changes applied in this revision."
+  [current-revision before-graph changes]
+  (when *current-user-id*
+    ;; manually specify ID here so if one was somehow inserted in the meantime in the fraction of a second since we
+    ;; called `check-revision-numbers` the PK constraint will fail and the transaction will abort
+    (db/insert! CollectionRevision
+      :id     (inc current-revision)
+      :before  before-graph
+      :after   changes
+      :user_id *current-user-id*)))
+
+(s/defn update-graph!
+  "Update the Collections permissions graph for Collections of `collection-namespace` (default `nil`, the 'default'
+  namespace). This works just like the function of the same name in `metabase.models.permissions`, but for
+  Collections; refer to that function's extensive documentation to get a sense for how this works."
+  ([new-graph]
+   (update-graph! nil new-graph))
+
+  ([collection-namespace :- (s/maybe su/KeywordOrString), new-graph :- PermissionsGraph]
+   (let [old-graph          (graph collection-namespace)
+         ;; fetch the *entire* graph before making any updates. We'll record this in the perms revision.
+         entire-graph       (graph ::all)
+         old-perms          (:groups old-graph)
+         new-perms          (:groups new-graph)
+         ;; filter out any groups not in the old graph
+         new-perms          (select-keys new-perms (keys old-perms))
+         ;; filter out any collections not in the old graph
+         new-perms          (into {} (for [[group-id collection-id->perms] new-perms]
+                                       [group-id (select-keys collection-id->perms (keys (get old-perms group-id)))]))
+         [diff-old changes] (data/diff old-perms new-perms)]
+     (perms/log-permissions-changes diff-old changes)
+     (perms/check-revision-numbers old-graph new-graph)
+     (when (seq changes)
+       (db/transaction
+         (doseq [[group-id changes] changes]
+           (update-group-permissions! group-id changes))
+         (save-perms-revision! (:revision old-graph) entire-graph changes))))))
diff --git a/src/metabase/models/collection/root.clj b/src/metabase/models/collection/root.clj
new file mode 100644
index 0000000000000000000000000000000000000000..1c03d1f34b32dfbe2e77f82d7cac29d3c7c7061c
--- /dev/null
+++ b/src/metabase/models/collection/root.clj
@@ -0,0 +1,42 @@
+(ns metabase.models.collection.root
+  (:require [metabase.models
+             [interface :as i]
+             [permissions :as perms]]
+            [metabase.util :as u]
+            [potemkin.types :as p.types]
+            [toucan.models :as models]))
+
+;;; +----------------------------------------------------------------------------------------------------------------+
+;;; |                                   Root Collection Special Placeholder Object                                   |
+;;; +----------------------------------------------------------------------------------------------------------------+
+
+;; The Root Collection special placeholder object is used to represent the fact that we're working with the 'Root'
+;; Collection in many of the functions in this namespace. The Root Collection is not a true Collection, but instead
+;; represents things that have no collection_id, or are otherwise to be seen at the top-level by the current user.
+
+(p.types/defrecord+ RootCollection [])
+
+(u/strict-extend RootCollection
+  models/IModel
+  (merge
+   models/IModelDefaults
+   {:types {:type :keyword}})
+
+  i/IObjectPermissions
+  (merge
+   i/IObjectPermissionsDefaults
+   {:perms-objects-set (fn [this read-or-write]
+                         #{((case read-or-write
+                              :read  perms/collection-read-path
+                              :write perms/collection-readwrite-path) this)})
+    :can-read?         (partial i/current-user-has-full-permissions? :read)
+    :can-write?        (partial i/current-user-has-full-permissions? :write)}))
+
+(def ^RootCollection root-collection
+  "Special placeholder object representing the Root Collection, which isn't really a real Collection."
+  (map->RootCollection {::is-root? true}))
+
+(defn is-root-collection?
+  "Is `x` the special placeholder object representing the Root Collection?"
+  [x]
+  (instance? RootCollection x))
diff --git a/src/metabase/models/collection_revision.clj b/src/metabase/models/collection_revision.clj
index c8ca745b3c4398226f8cd6479544691b2f4b757e..1c77f2607b69fc8f37a2c746098813b7d74df6d6 100644
--- a/src/metabase/models/collection_revision.clj
+++ b/src/metabase/models/collection_revision.clj
@@ -23,5 +23,5 @@
   "Return the ID of the newest `CollectionRevision`, or zero if none have been made yet.
    (This is used by the collection graph update logic that checks for changes since the original graph was fetched)."
   []
-  (or (db/select-one-id CollectionRevision {:order-by [[:id :desc]]})
+  (or (:id (db/select-one [CollectionRevision [:%max.id :id]]))
       0))
diff --git a/src/metabase/models/dashboard.clj b/src/metabase/models/dashboard.clj
index 1cf324587f970b0b1d0adab5b6ba1076fe7a483a..05e1da017018c55c9a883804004e86c6b83702c2 100644
--- a/src/metabase/models/dashboard.clj
+++ b/src/metabase/models/dashboard.clj
@@ -55,9 +55,14 @@
   (db/delete! DashboardCard :dashboard_id (u/get-id dashboard)))
 
 (defn- pre-insert [dashboard]
-  (let [defaults {:parameters []}]
-    (merge defaults dashboard)))
+  (let [defaults  {:parameters []}
+        dashboard (merge defaults dashboard)]
+    (u/prog1 dashboard
+      (collection/check-collection-namespace (map->DashboardInstance dashboard)))))
 
+(defn- pre-update [dashboard]
+  (u/prog1 dashboard
+    (collection/check-collection-namespace dashboard)))
 
 (u/strict-extend (class Dashboard)
   models/IModel
@@ -66,6 +71,7 @@
           :types       (constantly {:parameters :json, :embedding_params :json})
           :pre-delete  pre-delete
           :pre-insert  pre-insert
+          :pre-update  pre-update
           :post-select public-settings/remove-public-uuid-if-public-sharing-is-disabled})
 
   ;; You can read/write a Dashboard if you can read/write its parent Collection
diff --git a/src/metabase/models/native_query_snippet.clj b/src/metabase/models/native_query_snippet.clj
index b86414274c2cf5e4365720d048344c4815cbd639..bc4f9a1006e275e26cd1a0e779db8b7693cb901f 100644
--- a/src/metabase/models/native_query_snippet.clj
+++ b/src/metabase/models/native_query_snippet.clj
@@ -1,5 +1,9 @@
 (ns metabase.models.native-query-snippet
-  (:require [metabase.models.interface :as i]
+  (:require [metabase.models
+             [collection :as collection]
+             [interface :as i]]
+            [metabase.models.native-query-snippet.permissions :as snippet.perms]
+            [metabase.plugins.classloader :as classloader]
             [metabase.util :as u]
             [metabase.util.i18n :refer [tru]]
             [schema.core :as s]
@@ -7,32 +11,44 @@
              [db :as db]
              [models :as models]]))
 
+;; Load the EE implementation of snippet permissions, if they exist (if we're running with EE code available).
+(u/ignore-exceptions
+  (classloader/require 'metabase-enterprise.enhancements.models.native-query-snippet.permissions))
+
 ;;; ----------------------------------------------- Entity & Lifecycle -----------------------------------------------
 
 (models/defmodel NativeQuerySnippet :native_query_snippet)
 
+(defmethod collection/allowed-namespaces (class NativeQuerySnippet)
+  [_]
+  #{:snippets})
+
+(defn- pre-insert [snippet]
+  (u/prog1 snippet
+    (collection/check-collection-namespace snippet)))
+
 (defn- pre-update [{:keys [creator_id id], :as updates}]
   (u/prog1 updates
     ;; throw an Exception if someone tries to update creator_id
     (when (contains? updates :creator_id)
       (when (not= creator_id (db/select-one-field :creator_id NativeQuerySnippet :id id))
-        (throw (UnsupportedOperationException. (tru "You cannot update the creator_id of a NativeQuerySnippet.")))))))
+        (throw (UnsupportedOperationException. (tru "You cannot update the creator_id of a NativeQuerySnippet.")))))
+    (collection/check-collection-namespace updates)))
 
 (u/strict-extend (class NativeQuerySnippet)
   models/IModel
   (merge
    models/IModelDefaults
    {:properties (constantly {:timestamped? true})
+    :pre-insert pre-insert
     :pre-update pre-update})
 
   i/IObjectPermissions
   (merge
    i/IObjectPermissionsDefaults
-   {;; In Metabase CE, anyone can read/edit/create NativeQuerySnippets. In EE permissions are dictated by a 'folder'
-    ;; system similar to Collections (not yet implemented).
-    :can-read?   (constantly true)
-    :can-write?  (constantly true)
-    :can-create? (constantly true)}))
+   {:can-read?   snippet.perms/can-read?
+    :can-write?  snippet.perms/can-write?
+    :can-create? snippet.perms/can-create?}))
 
 
 ;;; ---------------------------------------------------- Schemas -----------------------------------------------------
diff --git a/src/metabase/models/native_query_snippet/permissions.clj b/src/metabase/models/native_query_snippet/permissions.clj
new file mode 100644
index 0000000000000000000000000000000000000000..8d29ecb562b78c227b9a5df8b4ef24e0fe3af7d2
--- /dev/null
+++ b/src/metabase/models/native_query_snippet/permissions.clj
@@ -0,0 +1,64 @@
+(ns metabase.models.native-query-snippet.permissions
+  "NativeQuerySnippets have different permissions implementations. In Metabase CE, anyone can read/edit/create all
+  NativeQuerySnippets. EE has a more advanced implementation.
+
+  The code in this namespace provides sort of a strategy pattern interface to the underlying permissions operations.
+  The default implementation is defined below and can be swapped out at runtime with the more advanced EE
+  implementation."
+  (:require [potemkin.types :as p.types]
+            [pretty.core :refer [PrettyPrintable]]))
+
+(p.types/defprotocol+ PermissionsImpl
+  "Protocol for implementing the permissions logic for NativeQuerySnippets."
+  (can-read?* [this snippet] [this model id]
+    "Can the current User read this `snippet`?")
+  (can-write?*  [this snippet] [this model id]
+    "Can the current User edit this `snippet`?")
+  (can-create?* [this snippet] [this model id]
+    "Can the current User save a new `snippet` with these values?"))
+
+(defonce ^:private impl (atom nil))
+
+(defn set-impl!
+  "Change the implementation used for NativeQuerySnippet permissions. `new-impl` must satisfy the `PermissionsImpl`
+  protocol defined above."
+  [new-impl]
+  (reset! impl new-impl))
+
+(def default-impl
+  "Default 'simple' permissions implementation for NativeQuerySnippets for Metabase CE."
+  (reify
+    PrettyPrintable
+    (pretty [_]
+      `default-impl)
+    PermissionsImpl
+    (can-read?* [_ _] true)
+    (can-read?* [_ _ _] true)
+    (can-write?* [_ _] true)
+    (can-write?* [_ _ _] true)
+    (can-create?* [_ _] true)
+    (can-create?* [_ _ _] true)))
+
+(when-not @impl
+  (set-impl! default-impl))
+
+(defn can-read?
+  "Can the current User read this `snippet`?"
+  ([snippet]
+   (can-read?* @impl snippet))
+  ([model id]
+   (can-read?* @impl model id)))
+
+(defn can-write?
+  "Can the current User edit this `snippet`?"
+  ([snippet]
+   (can-write?* @impl snippet))
+  ([model id]
+   (can-write?* @impl model id)))
+
+(defn can-create?
+  "Can the current User save a new `snippet` with these values?"
+  ([snippet]
+   (can-create?* @impl snippet))
+  ([model id]
+   (can-create?* @impl model id)))
diff --git a/src/metabase/models/permissions.clj b/src/metabase/models/permissions.clj
index 75b0b98c74498430129aec681f339650db4bfd32..862e1832ffde139da37f31d9cf6c81d92819fb87 100644
--- a/src/metabase/models/permissions.clj
+++ b/src/metabase/models/permissions.clj
@@ -148,7 +148,7 @@
   "Return the permissions path for *readwrite* access for a `collection-or-id`."
   [collection-or-id :- MapOrID]
   (str "/collection/"
-       (if (get collection-or-id :metabase.models.collection/is-root?)
+       (if (get collection-or-id :metabase.models.collection.root/is-root?)
          "root"
          (u/get-id collection-or-id))
        "/"))
@@ -245,7 +245,7 @@
     ;; now pass that function our collection_id if we have one, or if not, pass it an object representing the Root
     ;; Collection
     #{(path-fn (or (:collection_id this)
-                   {:metabase.models.collection/is-root? true}))}))
+                   {:metabase.models.collection.root/is-root? true}))}))
 
 (def IObjectPermissionsForParentCollection
   "Implementation of `IObjectPermissions` for objects that have a `collection_id`, and thus, a parent Collection.
@@ -500,7 +500,7 @@
   be given some sort of access."
   [collection-or-id :- MapOrID]
   ;; don't apply this check to the Root Collection, because it's never personal
-  (when-not (:metabase.models.collection/is-root? collection-or-id)
+  (when-not (:metabase.models.collection.root/is-root? collection-or-id)
     ;; ok, once we've confirmed this isn't the Root Collection, see if it's in the DB with a personal_owner_id
     (let [collection (if (map? collection-or-id)
                        collection-or-id
diff --git a/src/metabase/models/pulse.clj b/src/metabase/models/pulse.clj
index 7ac77342cb4ab9036fbf3f98185966e5cde0a5e6..a4452c9535db0fb2e3b352ef6be7aab5163c2300 100644
--- a/src/metabase/models/pulse.clj
+++ b/src/metabase/models/pulse.clj
@@ -22,6 +22,7 @@
              [util :as u]]
             [metabase.models
              [card :refer [Card]]
+             [collection :as collection]
              [interface :as i]
              [permissions :as perms]
              [pulse-card :refer [PulseCard]]
@@ -44,6 +45,14 @@
   (doseq [model [PulseCard PulseChannel]]
     (db/delete! model :pulse_id (u/get-id notification))))
 
+(defn- pre-insert [notification]
+  (u/prog1 notification
+    (collection/check-collection-namespace notification)))
+
+(defn- pre-update [updates]
+  (u/prog1 updates
+    (collection/check-collection-namespace updates)))
+
 (defn- alert->card
   "Return the Card associated with an Alert, fetching it if needed, for permissions-checking purposes."
   [alert]
@@ -73,7 +82,9 @@
    models/IModelDefaults
    {:hydration-keys (constantly [:pulse])
     :properties     (constantly {:timestamped? true})
-    :pre-delete     pre-delete})
+    :pre-delete     pre-delete
+    :pre-insert     pre-insert
+    :pre-update     pre-update})
   i/IObjectPermissions
   (merge
    i/IObjectPermissionsDefaults
@@ -413,7 +424,7 @@
 
 ;; TODO - why do we make sure to strictly validate everything when we create a PULSE but not when we create an ALERT?
 (defn update-alert!
-  "Updates the given `ALERT` and returns it"
+  "Updates the given `alert` and returns it"
   [alert]
   (update-notification! (alert->notification alert))
   ;; fetch the fully updated pulse and return it (and fire off an event)
@@ -439,5 +450,4 @@
                                                                 [:= :pcr.user_id user-id]]} "r"]]}]})]
     (when (zero? result)
       (log/warnf "Failed to remove user-id '%s' from alert-id '%s'" user-id alert-id))
-
     result))
diff --git a/test/expectations.clj b/test/expectations.clj
index 3e7f112aa92e3ca890d4bd194e2c6368a3fc4684..67fbe27f95a12f5a68811ef1853ffc02874155f3 100644
--- a/test/expectations.clj
+++ b/test/expectations.clj
@@ -137,8 +137,8 @@
     ;; expecting it.
     (when-not (env/env :drivers)
       (t/testing "Don't write any new tests using expect!"
-        (t/is (<= total-expect-forms 1716))
-        (t/is (<= total-namespaces-using-expect 108))))))
+        (t/is (<= total-expect-forms 1555))
+        (t/is (<= total-namespaces-using-expect 106))))))
 
 (defmacro ^:deprecated expect
   "Simple macro that simulates converts an Expectations-style `expect` form into a `clojure.test` `deftest` form."
diff --git a/test/metabase/api/collection_test.clj b/test/metabase/api/collection_test.clj
index ea29433f2a4a3d38bc0984dcaac1a225437947d0..b511d2c61cab9bdb49d432916c0ce03c90e454e1 100644
--- a/test/metabase/api/collection_test.clj
+++ b/test/metabase/api/collection_test.clj
@@ -3,28 +3,21 @@
   (:require [clojure
              [string :as str]
              [test :refer :all]]
-            [expectations :refer [expect]]
             [metabase
-             [email-test :as et]
+             [models :refer [Card Collection Dashboard NativeQuerySnippet PermissionsGroup PermissionsGroupMembership Pulse PulseCard PulseChannel PulseChannelRecipient]]
+             [test :as mt]
              [util :as u]]
             [metabase.models
-             [card :refer [Card]]
-             [collection :as collection :refer [Collection]]
+             [collection :as collection]
              [collection-test :as collection-test]
-             [dashboard :refer [Dashboard]]
              [permissions :as perms]
-             [permissions-group :as group :refer [PermissionsGroup]]
-             [permissions-group-membership :refer [PermissionsGroupMembership]]
-             [pulse :refer [Pulse]]
-             [pulse-card :refer [PulseCard]]
-             [pulse-channel :refer [PulseChannel]]
-             [pulse-channel-recipient :refer [PulseChannelRecipient]]]
-            [metabase.test
-             [fixtures :as fixtures]
-             [util :as tu]]
-            [metabase.test.data.users :refer [user->client user->id]]
-            [toucan.db :as db]
-            [toucan.util.test :as tt]))
+             [permissions-group :as group]]
+            [metabase.models.collection
+             [graph :as graph]
+             [graph-test :as graph.test]]
+            [metabase.test.fixtures :as fixtures]
+            [schema.core :as s]
+            [toucan.db :as db]))
 
 (use-fixtures :once (fixtures/initialize :test-users-personal-collections))
 
@@ -32,108 +25,130 @@
 ;;; |                                                GET /collection                                                 |
 ;;; +----------------------------------------------------------------------------------------------------------------+
 
-;; check that we can get a basic list of collections
-;; (for the purposes of test purposes remove the personal collections)
-(tt/expect-with-temp [Collection [collection]]
-  [{:parent_id nil, :effective_location nil, :effective_ancestors (), :can_write true, :name "Our analytics", :id "root"}
-   (assoc (into {} collection) :can_write true)]
-  (for [collection ((user->client :crowberto) :get 200 "collection")
-        :when (not (:personal_owner_id collection))]
-    collection))
-
-;; We should only see our own Personal Collections!
-(expect
-  ["Our analytics"
-   "Lucky Pigeon's Personal Collection"]
-  (map :name ((user->client :lucky) :get 200 "collection")))
-
-;; ...unless we are *admins*
-(expect
-  ["Our analytics"
-   "Crowberto Corv's Personal Collection"
-   "Lucky Pigeon's Personal Collection"
-   "Rasta Toucan's Personal Collection"
-   "Trash Bird's Personal Collection"]
-  (map :name ((user->client :crowberto) :get 200 "collection")))
-
-;; check that we don't see collections if we don't have permissions for them
-(expect
-  ["Our analytics"
-   "Collection 1"
-   "Rasta Toucan's Personal Collection"]
-  (tu/with-non-admin-groups-no-root-collection-perms
-    (tt/with-temp* [Collection [collection-1 {:name "Collection 1"}]
-                    Collection [collection-2 {:name "Collection 2"}]]
-      (perms/grant-collection-read-permissions! (group/all-users) collection-1)
-      (map :name ((user->client :rasta) :get 200 "collection")))))
-
-;; check that we don't see collections if they're archived
-(expect
-  ["Our analytics"
-   "Rasta Toucan's Personal Collection"
-   "Regular Collection"]
-  (tt/with-temp* [Collection [collection-1 {:name "Archived Collection", :archived true}]
-                  Collection [collection-2 {:name "Regular Collection"}]]
-    (perms/grant-collection-read-permissions! (group/all-users) collection-1)
-    (perms/grant-collection-read-permissions! (group/all-users) collection-2)
-    (map :name ((user->client :rasta) :get 200 "collection"))))
-
-;; Check that if we pass `?archived=true` we instead see archived cards
-(expect
-  ["Archived Collection"]
-  (tt/with-temp* [Collection [collection-1 {:name "Archived Collection", :archived true}]
-                  Collection [collection-2 {:name "Regular Collection"}]]
-    (perms/grant-collection-read-permissions! (group/all-users) collection-1)
-    (perms/grant-collection-read-permissions! (group/all-users) collection-2)
-    (map :name ((user->client :rasta) :get 200 "collection" :archived :true))))
+(deftest list-collections-test
+  (testing "GET /api/collection"
+    (testing "check that we can get a basic list of collections"
+      ;; (for the purposes of test purposes remove the personal collections)
+      (mt/with-temp Collection [collection]
+        (is (= [{:parent_id           nil
+                 :effective_location  nil
+                 :effective_ancestors []
+                 :can_write           true
+                 :name                "Our analytics"
+                 :id                  "root"}
+                (assoc (into {} collection) :can_write true)]
+               (for [collection ((mt/user->client :crowberto) :get 200 "collection")
+                     :when      (not (:personal_owner_id collection))]
+                 collection)))))
+
+    (testing "We should only see our own Personal Collections!"
+      (is (= ["Lucky Pigeon's Personal Collection"]
+             (->> ((mt/user->client :lucky) :get 200 "collection")
+                  (filter :personal_owner_id)
+                  (map :name))))
+
+      (testing "...unless we are *admins*"
+        (is (= ["Crowberto Corv's Personal Collection"
+                "Lucky Pigeon's Personal Collection"
+                "Rasta Toucan's Personal Collection"
+                "Trash Bird's Personal Collection"]
+               (->> ((mt/user->client :crowberto) :get 200 "collection")
+                    (filter #((set (map mt/user->id [:crowberto :lucky :rasta :trashbird])) (:personal_owner_id %)))
+                    (map :name)
+                    sort)))))
+
+    (testing "check that we don't see collections if we don't have permissions for them"
+      (mt/with-non-admin-groups-no-root-collection-perms
+        (mt/with-temp* [Collection [collection-1 {:name "Collection 1"}]
+                        Collection [collection-2 {:name "Collection 2"}]]
+          (perms/grant-collection-read-permissions! (group/all-users) collection-1)
+          (is (= ["Our analytics"
+                  "Collection 1"
+                  "Rasta Toucan's Personal Collection"]
+                 (map :name ((mt/user->client :rasta) :get 200 "collection")))))))
+
+    (testing "check that we don't see collections if they're archived"
+      (mt/with-temp* [Collection [collection-1 {:name "Archived Collection", :archived true}]
+                      Collection [collection-2 {:name "Regular Collection"}]]
+        (is (= ["Our analytics"
+                "Rasta Toucan's Personal Collection"
+                "Regular Collection"]
+               (map :name ((mt/user->client :rasta) :get 200 "collection"))))))
+
+    (testing "Check that if we pass `?archived=true` we instead see archived Collections"
+      (mt/with-temp* [Collection [collection-1 {:name "Archived Collection", :archived true}]
+                      Collection [collection-2 {:name "Regular Collection"}]]
+        (is (= ["Archived Collection"]
+               (map :name ((mt/user->client :rasta) :get 200 "collection" :archived :true))))))
+
+    (testing "?namespace= parameter"
+      (mt/with-temp* [Collection [{normal-id :id} {:name "Normal Collection"}]
+                      Collection [{coins-id  :id} {:name "Coin Collection", :namespace "currency"}]]
+        (letfn [(collection-names [collections]
+                  (->> collections
+                       (filter #(#{normal-id coins-id} (:id %)))
+                       (map :name)))]
+          (testing "shouldn't show Collections of a different `:namespace` by default"
+            (is (= ["Normal Collection"]
+                   (collection-names ((mt/user->client :rasta) :get 200 "collection")))))
+
+          (testing "By passing `:namespace` we should be able to see Collections of that `:namespace`"
+            (testing "?namespace=currency"
+              (is (= ["Coin Collection"]
+                     (collection-names ((mt/user->client :rasta) :get 200 "collection?namespace=currency")))))
+
+            (testing "?namespace=stamps"
+              (is (= []
+                     (collection-names ((mt/user->client :rasta) :get 200 "collection?namespace=stamps")))))))))))
 
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
 ;;; |                                              GET /collection/:id                                               |
 ;;; +----------------------------------------------------------------------------------------------------------------+
 
-;; check that we can see collection details (GET /api/collection/:id)
-(expect
-  "Coin Collection"
-  (tt/with-temp Collection [collection {:name "Coin Collection"}]
-    (perms/grant-collection-read-permissions! (group/all-users) collection)
-    (:name ((user->client :rasta) :get 200 (str "collection/" (u/get-id collection))))))
+(deftest fetch-collection-test
+  (testing "GET /api/collection/:id"
+    (testing "check that we can see collection details"
+      (mt/with-temp Collection [collection {:name "Coin Collection"}]
+        (perms/grant-collection-read-permissions! (group/all-users) collection)
+        (is (= "Coin Collection"
+               (:name ((mt/user->client :rasta) :get 200 (str "collection/" (u/get-id collection))))))))
 
-;; check that collections detail properly checks permissions
-(expect
-  "You don't have permissions to do that."
-  (tu/with-non-admin-groups-no-root-collection-perms
-    (tt/with-temp Collection [collection]
-      ((user->client :rasta) :get 403 (str "collection/" (u/get-id collection))))))
+    (testing "check that collections detail properly checks permissions"
+      (mt/with-non-admin-groups-no-root-collection-perms
+        (mt/with-temp Collection [collection]
+          (is (= "You don't have permissions to do that."
+                 ((mt/user->client :rasta) :get 403 (str "collection/" (u/get-id collection))))))))))
 
 
-;;; ----------------------------------------- Cards, Dashboards, and Pulses ------------------------------------------
-
-;; check that cards are returned with the collection/items endpoint
-(tt/expect-with-temp [Collection [collection]
-                      Card       [card        {:collection_id (u/get-id collection)}]]
-  (tu/obj->json->obj
-    [{:id                  (u/get-id card)
-      :name                (:name card)
-      :collection_position nil
-      :display            "table"
-      :description         nil
-      :favorite            false
-      :model               "card"}])
-  (tu/obj->json->obj
-   ((user->client :crowberto) :get 200 (str "collection/" (u/get-id collection) "/items"))))
+;;; ------------------------------------------------ Collection Items ------------------------------------------------
 
 (defn- do-with-some-children-of-collection [collection-or-id-or-nil f]
-  (tu/with-non-admin-groups-no-root-collection-perms
+  (mt/with-non-admin-groups-no-root-collection-perms
     (let [collection-id-or-nil (when collection-or-id-or-nil
                                  (u/get-id collection-or-id-or-nil))]
-      (tt/with-temp* [Card       [_ {:name "Birthday Card", :collection_id collection-id-or-nil}]
-                      Dashboard  [_ {:name "Dine & Dashboard", :collection_id collection-id-or-nil}]
-                      Pulse      [_ {:name "Electro-Magnetic Pulse", :collection_id collection-id-or-nil}]]
-        (f)))))
+      (mt/with-temp* [Card       [{card-id :id}      {:name "Birthday Card", :collection_id collection-id-or-nil}]
+                      Dashboard  [{dashboard-id :id} {:name "Dine & Dashboard", :collection_id collection-id-or-nil}]
+                      Pulse      [{pulse-id :id}     {:name "Electro-Magnetic Pulse", :collection_id collection-id-or-nil}]]
+        (f {:card-id card-id, :dashboard-id dashboard-id, :pulse-id pulse-id})))))
 
 (defmacro ^:private with-some-children-of-collection {:style/indent 1} [collection-or-id-or-nil & body]
-  `(do-with-some-children-of-collection ~collection-or-id-or-nil (fn [] ~@body)))
+  `(do-with-some-children-of-collection
+    ~collection-or-id-or-nil
+    (fn [~'&ids]
+      ~@body)))
+
+(defn- remove-non-test-items
+  "Remove Cards, Dashboards, and Pulses that aren't the 'Birthday Card'/'Dine & Dashboard'/'Electro-Magnetic Pulse'
+  created by `with-some-children-of-collection`."
+  [items {:keys [card-id dashboard-id pulse-id]}]
+  (filter (fn [{:keys [id model]}]
+            (case model
+              "card"      (= id card-id)
+              "dashboard" (= id dashboard-id)
+              "pulse"     (= id pulse-id)
+              true))
+          items))
 
 (defn- default-item [item-map]
   (merge {:id true, :collection_position nil} item-map))
@@ -146,95 +161,131 @@
           :name        collection-name}
          extra-keypairs))
 
-;; check that you get to see the children as appropriate
-(expect
-  (map default-item [{:name "Birthday Card", :description nil, :favorite false, :model "card", :display "table"}
-                     {:name "Dine & Dashboard", :description nil, :model "dashboard"}
-                     {:name "Electro-Magnetic Pulse", :model "pulse"}])
-  (tt/with-temp Collection [collection {:name "Debt Collection"}]
-    (perms/grant-collection-read-permissions! (group/all-users) collection)
-    (with-some-children-of-collection collection
-      (tu/boolean-ids-and-timestamps
-       ((user->client :rasta) :get 200 (str "collection/" (u/get-id collection) "/items"))))))
-
-;; ...and that you can also filter so that you only see the children you want to see
-(expect
-  [(default-item {:name "Dine & Dashboard", :description nil, :model "dashboard"})]
-  (tt/with-temp Collection [collection {:name "Art Collection"}]
-    (perms/grant-collection-read-permissions! (group/all-users) collection)
-    (with-some-children-of-collection collection
-      (tu/boolean-ids-and-timestamps
-       ((user->client :rasta) :get 200 (str "collection/" (u/get-id collection) "/items?model=dashboard"))))))
-
-;; Let's make sure the `archived` option works.
-(expect
-  [(default-item {:name "Dine & Dashboard", :description nil, :model "dashboard"})]
-  (tt/with-temp Collection [collection {:name "Art Collection"}]
-    (perms/grant-collection-read-permissions! (group/all-users) collection)
-    (with-some-children-of-collection collection
-      (db/update-where! Dashboard {:collection_id (u/get-id collection)} :archived true)
-      (tu/boolean-ids-and-timestamps
-       ((user->client :rasta) :get 200 (str "collection/" (u/get-id collection) "/items?archived=true"))))))
+(deftest collection-items-test
+  (testing "GET /api/collection/:id/items"
+    (testing "check that cards are returned with the collection/items endpoint"
+      (mt/with-temp* [Collection [collection]
+                      Card       [card        {:collection_id (u/get-id collection)}]]
+        (is (= (mt/obj->json->obj
+                 [{:id                  (u/get-id card)
+                   :name                (:name card)
+                   :collection_position nil
+                   :display             "table"
+                   :description         nil
+                   :favorite            false
+                   :model               "card"}])
+               (mt/obj->json->obj
+                 ((mt/user->client :crowberto) :get 200 (str "collection/" (u/get-id collection) "/items")))))))
+
+    (testing "check that you get to see the children as appropriate"
+      (mt/with-temp Collection [collection {:name "Debt Collection"}]
+        (perms/grant-collection-read-permissions! (group/all-users) collection)
+        (with-some-children-of-collection collection
+          (is (= (map default-item [{:name "Birthday Card", :description nil, :favorite false, :model "card", :display "table"}
+                                    {:name "Dine & Dashboard", :description nil, :model "dashboard"}
+                                    {:name "Electro-Magnetic Pulse", :model "pulse"}])
+                 (mt/boolean-ids-and-timestamps
+                  ((mt/user->client :rasta) :get 200 (str "collection/" (u/get-id collection) "/items")))))))
+
+      (testing "...and that you can also filter so that you only see the children you want to see"
+        (mt/with-temp Collection [collection {:name "Art Collection"}]
+          (perms/grant-collection-read-permissions! (group/all-users) collection)
+          (with-some-children-of-collection collection
+            (is (= [(default-item {:name "Dine & Dashboard", :description nil, :model "dashboard"})]
+                   (mt/boolean-ids-and-timestamps
+                    ((mt/user->client :rasta) :get 200 (str "collection/" (u/get-id collection) "/items?model=dashboard")))))))))
+
+    (testing "Let's make sure the `archived` option works."
+      (mt/with-temp Collection [collection {:name "Art Collection"}]
+        (perms/grant-collection-read-permissions! (group/all-users) collection)
+        (with-some-children-of-collection collection
+          (db/update-where! Dashboard {:collection_id (u/get-id collection)} :archived true)
+          (is (= [(default-item {:name "Dine & Dashboard", :description nil, :model "dashboard"})]
+                 (mt/boolean-ids-and-timestamps
+                  ((mt/user->client :rasta) :get 200 (str "collection/" (u/get-id collection) "/items?archived=true"))))))))))
+
+(deftest snippet-collection-items-test
+  (testing "GET /api/collection/:id/items"
+    (testing "Native query snippets should come back when fetching the items in a Collection in the `:snippets` namespace"
+      (mt/with-temp* [Collection         [{collection-id :id} {:namespace "snippets", :name "My Snippet Collection"}]
+                      NativeQuerySnippet [{snippet-id :id}    {:collection_id collection-id, :name "My Snippet"}]
+                      NativeQuerySnippet [{archived-id :id}   {:collection_id collection-id, :name "Archived Snippet", :archived true}]]
+        (is (= [{:id    snippet-id
+                 :name  "My Snippet"
+                 :model "snippet"}]
+               ((mt/user->client :rasta) :get 200 (format "collection/%d/items" collection-id))))
+
+        (testing "\nShould be able to fetch archived Snippets"
+          (is (= [{:id    archived-id
+                   :name  "Archived Snippet"
+                   :model "snippet"}]
+                 ((mt/user->client :rasta) :get 200 (format "collection/%d/items?archived=true" collection-id)))))
+
+        (testing "\nShould be able to pass ?model=snippet, even though it makes no difference in this case"
+          (is (= [{:id    snippet-id
+                   :name  "My Snippet"
+                   :model "snippet"}]
+                 ((mt/user->client :rasta) :get 200 (format "collection/%d/items?model=snippet" collection-id)))))))))
+
 
 ;;; --------------------------------- Fetching Personal Collections (Ours & Others') ---------------------------------
 
 (defn- lucky-personal-collection []
-  {:description         nil
-   :archived            false
-   :slug                "lucky_pigeon_s_personal_collection"
-   :color               "#31698A"
-   :can_write           true
-   :name                "Lucky Pigeon's Personal Collection"
-   :personal_owner_id   (user->id :lucky)
-   :effective_ancestors [{:metabase.models.collection/is-root? true, :name "Our analytics", :id "root", :can_write true}]
-   :effective_location  "/"
-   :parent_id           nil
-   :id                  (u/get-id (collection/user->personal-collection (user->id :lucky)))
-   :location            "/"})
+  (merge
+   (mt/object-defaults Collection)
+   {:slug                "lucky_pigeon_s_personal_collection"
+    :color               "#31698A"
+    :can_write           true
+    :name                "Lucky Pigeon's Personal Collection"
+    :personal_owner_id   (mt/user->id :lucky)
+    :effective_ancestors [{:metabase.models.collection.root/is-root? true, :name "Our analytics", :id "root", :can_write true}]
+    :effective_location  "/"
+    :parent_id           nil
+    :id                  (u/get-id (collection/user->personal-collection (mt/user->id :lucky)))
+    :location            "/"}))
 
 (defn- lucky-personal-collection-id
   []
-  (u/get-id (collection/user->personal-collection (user->id :lucky))))
+  (u/get-id (collection/user->personal-collection (mt/user->id :lucky))))
 
 (defn- api-get-lucky-personal-collection [user-kw & {:keys [expected-status-code], :or {expected-status-code 200}}]
-  ((user->client user-kw) :get expected-status-code (str "collection/" (lucky-personal-collection-id))))
+  ((mt/user->client user-kw) :get expected-status-code (str "collection/" (lucky-personal-collection-id))))
 
 (defn- api-get-lucky-personal-collection-items [user-kw & {:keys [expected-status-code], :or {expected-status-code 200}}]
-  ((user->client user-kw) :get expected-status-code (str "collection/" (lucky-personal-collection-id) "/items")))
+  ((mt/user->client user-kw) :get expected-status-code (str "collection/" (lucky-personal-collection-id) "/items")))
 
-;; Can we use this endpoint to fetch our own Personal Collection?
-(expect
-  (lucky-personal-collection)
-  (api-get-lucky-personal-collection :lucky))
+(deftest fetch-personal-collection-test
+  (testing "GET /api/collection/:id"
+    (testing "Can we use this endpoint to fetch our own Personal Collection?"
+      (is (= (lucky-personal-collection)
+             (api-get-lucky-personal-collection :lucky))))
 
-;; Can and admin use this endpoint to fetch someone else's Personal Collection?
-(expect
-  (lucky-personal-collection)
-  (api-get-lucky-personal-collection :crowberto))
+    (testing "Can and admin use this endpoint to fetch someone else's Personal Collection?"
+      (is (= (lucky-personal-collection)
+             (api-get-lucky-personal-collection :crowberto))))
 
-;; Other, non-admin Users should not be allowed to fetch others' Personal Collections!
-(expect
-  "You don't have permissions to do that."
-  (api-get-lucky-personal-collection :rasta, :expected-status-code 403))
+    (testing "Other, non-admin Users should not be allowed to fetch others' Personal Collections!"
+      (is (= "You don't have permissions to do that."
+             (api-get-lucky-personal-collection :rasta, :expected-status-code 403))))))
 
 (def ^:private lucky-personal-subcollection-item
   [(collection-item "Lucky's Personal Sub-Collection" :can_write true)])
 
 (defn- api-get-lucky-personal-collection-with-subcollection [user-kw]
-  (tt/with-temp Collection [_ {:name     "Lucky's Personal Sub-Collection"
+  (mt/with-temp Collection [_ {:name     "Lucky's Personal Sub-Collection"
                                :location (collection/children-location
-                                          (collection/user->personal-collection (user->id :lucky)))}]
-    (tu/boolean-ids-and-timestamps (api-get-lucky-personal-collection-items user-kw))))
+                                          (collection/user->personal-collection (mt/user->id :lucky)))}]
+    (mt/boolean-ids-and-timestamps (api-get-lucky-personal-collection-items user-kw))))
 
-;; If we have a sub-Collection of our Personal Collection, that should show up
-(expect
-  lucky-personal-subcollection-item
-  (api-get-lucky-personal-collection-with-subcollection :lucky))
+(deftest fetch-personal-collection-items-test
+  (testing "GET /api/collection/:id/items"
+    (testing "If we have a sub-Collection of our Personal Collection, that should show up"
+      (is (= lucky-personal-subcollection-item
+             (api-get-lucky-personal-collection-with-subcollection :lucky))))
 
-;; sub-Collections of other's Personal Collections should show up for admins as well
-(expect
-  lucky-personal-subcollection-item
-  (api-get-lucky-personal-collection-with-subcollection :crowberto))
+    (testing "sub-Collections of other's Personal Collections should show up for admins as well"
+      (is (= lucky-personal-subcollection-item
+             (api-get-lucky-personal-collection-with-subcollection :crowberto))))))
 
 
 ;;; ------------------------------------ Effective Ancestors & Effective Children ------------------------------------
@@ -251,7 +302,7 @@
          `(perms/grant-collection-read-permissions! (group/all-users) ~collection-symb))
      ~@body))
 
-(defn- format-ancestors-and-children
+(defn- format-ancestors
   "Nicely format the `:effective_` results from an API call."
   [results]
   (-> results
@@ -259,420 +310,563 @@
       (update :effective_ancestors (partial map #(update % :id integer?)))
       (update :effective_location collection-test/location-path-ids->names)))
 
-(defn- api-get-collection-ancestors-and-children
+(defn- api-get-collection-ancestors
   "Call the API with Rasta to fetch `collection-or-id` and put the `:effective_` results in a nice format for the tests
   below."
   [collection-or-id & additional-get-params]
-  [(format-ancestors-and-children ((user->client :rasta) :get 200 (str "collection/" (u/get-id collection-or-id))))
-   (tu/boolean-ids-and-timestamps (apply (user->client :rasta) :get 200 (str "collection/" (u/get-id collection-or-id) "/items")
-                                         additional-get-params))])
-
-;; does a top-level Collection like A have the correct Children?
-(expect
-  [{:effective_ancestors []
-    :effective_location  "/"}
-   (map collection-item ["B" "C"])]
-  (with-collection-hierarchy [a b c d g]
-    (api-get-collection-ancestors-and-children a)))
-
-;; ok, does a second-level Collection have its parent and its children?
-(expect
-  [{:effective_ancestors [{:name "A", :id true, :can_write false}]
-    :effective_location  "/A/"}
-   (map collection-item ["D" "G"])]
-  (with-collection-hierarchy [a b c d g]
-    (api-get-collection-ancestors-and-children c)))
-
-;; what about a third-level Collection?
-(expect
-  [{:effective_ancestors [{:name "A", :id true, :can_write false}
-                          {:name "C", :id true, :can_write false}]
-    :effective_location  "/A/C/"}
-   []]
-  (with-collection-hierarchy [a b c d g]
-    (api-get-collection-ancestors-and-children d)))
-
-;; for D: if we remove perms for C we should only have A as an ancestor; effective_location should lie and say we are
-;; a child of A
-(expect
-  [{:effective_ancestors [{:name "A", :id true, :can_write false}]
-    :effective_location  "/A/"}
-   []]
-  (with-collection-hierarchy [a b d g]
-    (api-get-collection-ancestors-and-children d)))
-
-;; for D: If, on the other hand, we remove A, we should see C as the only ancestor and as a root-level Collection.
-(expect
-  [{:effective_ancestors [{:name "C", :id true, :can_write false}]
-    :effective_location  "/C/"}
-   []]
-  (with-collection-hierarchy [b c d g]
-    (api-get-collection-ancestors-and-children d)))
-
-;; for C: if we remove D we should get E and F as effective children
-(expect
-  [{:effective_ancestors [{:name "A", :id true, :can_write false}]
-    :effective_location  "/A/"}
-   (map collection-item ["E" "F"])]
-  (with-collection-hierarchy [a b c e f g]
-    (api-get-collection-ancestors-and-children c)))
-
-;; Make sure we can collapse multiple generations. For A: removing C and D should move up E and F
-(expect
-  [{:effective_ancestors []
-    :effective_location  "/"}
-   (map collection-item ["B" "E" "F"])]
-  (with-collection-hierarchy [a b e f g]
-    (api-get-collection-ancestors-and-children a)))
-
-;; Let's make sure the 'archived` option works on Collections, nested or not
-(expect
-  [{:effective_ancestors []
-    :effective_location  "/"}
-   [(collection-item "B")]]
-  (with-collection-hierarchy [a b c]
-    (db/update! Collection (u/get-id b) :archived true)
-    (api-get-collection-ancestors-and-children a :archived true)))
+  (format-ancestors ((mt/user->client :rasta) :get 200 (str "collection/" (u/get-id collection-or-id)))))
+
+(defn- api-get-collection-children
+  [collection-or-id & additional-get-params]
+  (mt/boolean-ids-and-timestamps (apply (mt/user->client :rasta) :get 200 (str "collection/" (u/get-id collection-or-id) "/items")
+                                        additional-get-params)))
+
+(deftest effective-ancestors-and-children-test
+  (testing "does a top-level Collection like A have the correct Children?"
+    (with-collection-hierarchy [a b c d g]
+      (testing "ancestors"
+        (is(= {:effective_ancestors []
+               :effective_location  "/"}
+              (api-get-collection-ancestors a))))
+      (testing "children"
+        (is (= (map collection-item ["B" "C"])
+               (api-get-collection-children a))))))
+
+  (testing "ok, does a second-level Collection have its parent and its children?"
+    (with-collection-hierarchy [a b c d g]
+      (testing "ancestors"
+        (is (= {:effective_ancestors [{:name "A", :id true, :can_write false}]
+                :effective_location  "/A/"}
+               (api-get-collection-ancestors c))))
+      (testing "children"
+        (is (= (map collection-item ["D" "G"])
+               (api-get-collection-children c))))))
+
+  (testing "what about a third-level Collection?"
+    (with-collection-hierarchy [a b c d g]
+      (testing "ancestors"
+        (is (= {:effective_ancestors [{:name "A", :id true, :can_write false}
+                                      {:name "C", :id true, :can_write false}]
+                :effective_location  "/A/C/"}
+               (api-get-collection-ancestors d))))
+      (testing "children"
+        (is (= []
+               (api-get-collection-children d))))))
+
+  (testing (str "for D: if we remove perms for C we should only have A as an ancestor; effective_location should lie "
+                "and say we are a child of A")
+    (with-collection-hierarchy [a b d g]
+      (testing "ancestors"
+        (is (= {:effective_ancestors [{:name "A", :id true, :can_write false}]
+                :effective_location  "/A/"}
+               (api-get-collection-ancestors d))))
+      (testing "children"
+        (is (= []
+               (api-get-collection-children d))))))
+
+  (testing "for D: If, on the other hand, we remove A, we should see C as the only ancestor and as a root-level Collection."
+    (with-collection-hierarchy [b c d g]
+      (testing "ancestors"
+        (is (= {:effective_ancestors [{:name "C", :id true, :can_write false}]
+                :effective_location  "/C/"}
+               (api-get-collection-ancestors d))))
+      (testing "children"
+        (is (= []
+               (api-get-collection-children d)))))
+
+    (testing "for C: if we remove D we should get E and F as effective children"
+      (with-collection-hierarchy [a b c e f g]
+        (testing "ancestors"
+          (is (= {:effective_ancestors [{:name "A", :id true, :can_write false}]
+                  :effective_location  "/A/"}
+                 (api-get-collection-ancestors c))))
+        (testing "children"
+          (is (= (map collection-item ["E" "F"])
+                 (api-get-collection-children c)))))))
+
+  (testing "Make sure we can collapse multiple generations. For A: removing C and D should move up E and F"
+    (with-collection-hierarchy [a b e f g]
+      (testing "ancestors"
+        (is (= {:effective_ancestors []
+                :effective_location  "/"}
+               (api-get-collection-ancestors a))))
+      (testing "children"
+        (is (= (map collection-item ["B" "E" "F"])
+               (api-get-collection-children a))))))
+
+  (testing "Let's make sure the 'archived` option works on Collections, nested or not"
+    (with-collection-hierarchy [a b c]
+      (db/update! Collection (u/get-id b) :archived true)
+      (testing "ancestors"
+        (is (= {:effective_ancestors []
+                :effective_location  "/"}
+               (api-get-collection-ancestors a :archived true))))
+      (testing "children"
+        (is (= [(collection-item "B")]
+               (api-get-collection-children a :archived true)))))))
 
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
 ;;; |                                              GET /collection/root                                              |
 ;;; +----------------------------------------------------------------------------------------------------------------+
 
-;; Check that we can see stuff that isn't in any Collection -- meaning they're in the so-called "Root" Collection
-(expect
-  {:name                "Our analytics"
-   :id                  "root"
-   :can_write           true
-   :effective_location  nil
-   :effective_ancestors []
-   :parent_id           nil}
-  (with-some-children-of-collection nil
-    ((user->client :crowberto) :get 200 "collection/root")))
-
-;; Make sure you can see everything for Users that can see everything
-(expect
-  [(default-item {:name "Birthday Card", :description nil, :favorite false, :model "card", :display "table"})
-   (collection-item "Crowberto Corv's Personal Collection")
-   (default-item {:name "Dine & Dashboard", :description nil, :model "dashboard"})
-   (default-item {:name "Electro-Magnetic Pulse", :model "pulse"})]
-  (with-some-children-of-collection nil
-    (tu/boolean-ids-and-timestamps ((user->client :crowberto) :get 200 "collection/root/items"))))
-
-;; ...but we don't let you see stuff you wouldn't otherwise be allowed to see
-(expect
-  [(collection-item "Rasta Toucan's Personal Collection")]
-  ;; if a User doesn't have perms for the Root Collection then they don't get to see things with no collection_id
-  (with-some-children-of-collection nil
-    (tu/boolean-ids-and-timestamps ((user->client :rasta) :get 200 "collection/root/items"))))
-
-;; ...but if they have read perms for the Root Collection they should get to see them
-(expect
-  [(default-item {:name "Birthday Card", :description nil, :favorite false, :model "card", :display "table"})
-   (default-item {:name "Dine & Dashboard", :description nil, :model "dashboard"})
-   (default-item {:name "Electro-Magnetic Pulse", :model "pulse"})
-   (collection-item "Rasta Toucan's Personal Collection")]
-  (with-some-children-of-collection nil
-    (tt/with-temp* [PermissionsGroup           [group]
-                    PermissionsGroupMembership [_ {:user_id (user->id :rasta), :group_id (u/get-id group)}]]
-      (perms/grant-permissions! group (perms/collection-read-path {:metabase.models.collection/is-root? true}))
-      (tu/boolean-ids-and-timestamps ((user->client :rasta) :get 200 "collection/root/items")))))
-
-;; So I suppose my Personal Collection should show up when I fetch the Root Collection, shouldn't it...
-(expect
-  [{:name        "Rasta Toucan's Personal Collection"
-    :id          (u/get-id (collection/user->personal-collection (user->id :rasta)))
-    :description nil
-    :model       "collection"
-    :can_write   true}]
-  ((user->client :rasta) :get 200 "collection/root/items"))
-
-;; And for admins, only return our own Personal Collection (!)
-(expect
-  [{:name        "Crowberto Corv's Personal Collection"
-    :id          (u/get-id (collection/user->personal-collection (user->id :crowberto)))
-    :description nil
-    :model       "collection"
-    :can_write   true}]
-  ((user->client :crowberto) :get 200 "collection/root/items"))
-
-;; That includes sub-collections of Personal Collections! I shouldn't see them!
-(expect
-  [{:name        "Crowberto Corv's Personal Collection"
-    :id          (u/get-id (collection/user->personal-collection (user->id :crowberto)))
-    :description nil
-    :model       "collection"
-    :can_write   true}]
-  (tt/with-temp Collection [_ {:name     "Lucky's Sub-Collection"
-                               :location (collection/children-location
-                                          (collection/user->personal-collection (user->id :lucky)))}]
-    ((user->client :crowberto) :get 200 "collection/root/items")))
-
-;; Can we look for `archived` stuff with this endpoint?
-(expect
-  [{:name                "Business Card"
-    :description         nil
-    :collection_position nil
-    :display             "table"
-    :favorite            false
-    :model               "card"}]
-  (tt/with-temp Card [card {:name "Business Card", :archived true}]
-    (for [item ((user->client :crowberto) :get 200 "collection/root/items?archived=true")]
-      (dissoc item :id))))
+(deftest fetch-root-collection-test
+  (testing "GET /api/collection/root"
+    (testing "Check that we can see stuff that isn't in any Collection -- meaning they're in the so-called \"Root\" Collection"
+      (is (= {:name                "Our analytics"
+              :id                  "root"
+              :can_write           true
+              :effective_location  nil
+              :effective_ancestors []
+              :parent_id           nil}
+             (with-some-children-of-collection nil
+               ((mt/user->client :crowberto) :get 200 "collection/root")))))
+
+    (testing "Make sure you can see everything for Users that can see everything"
+      (is (= [(default-item {:name "Birthday Card", :description nil, :favorite false, :model "card", :display "table"})
+              (collection-item "Crowberto Corv's Personal Collection")
+              (default-item {:name "Dine & Dashboard", :description nil, :model "dashboard"})
+              (default-item {:name "Electro-Magnetic Pulse", :model "pulse"})]
+             (with-some-children-of-collection nil
+               (-> ((mt/user->client :crowberto) :get 200 "collection/root/items")
+                   (remove-non-test-items &ids)
+                   mt/boolean-ids-and-timestamps))))
+
+      (testing "...but we don't let you see stuff you wouldn't otherwise be allowed to see"
+        (is (= [(collection-item "Rasta Toucan's Personal Collection")]
+               ;; if a User doesn't have perms for the Root Collection then they don't get to see things with no collection_id
+               (with-some-children-of-collection nil
+                 (mt/boolean-ids-and-timestamps ((mt/user->client :rasta) :get 200 "collection/root/items")))))
+
+        (testing "...but if they have read perms for the Root Collection they should get to see them"
+          (with-some-children-of-collection nil
+            (mt/with-temp* [PermissionsGroup           [group]
+                            PermissionsGroupMembership [_ {:user_id (mt/user->id :rasta), :group_id (u/get-id group)}]]
+              (perms/grant-permissions! group (perms/collection-read-path {:metabase.models.collection.root/is-root? true}))
+              (is (= [(default-item {:name "Birthday Card", :description nil, :favorite false, :model "card", :display "table"})
+                      (default-item {:name "Dine & Dashboard", :description nil, :model "dashboard"})
+                      (default-item {:name "Electro-Magnetic Pulse", :model "pulse"})
+                      (collection-item "Rasta Toucan's Personal Collection")]
+                     (-> ((mt/user->client :rasta) :get 200 "collection/root/items")
+                         (remove-non-test-items &ids)
+                         mt/boolean-ids-and-timestamps ))))))))
+
+    (testing "So I suppose my Personal Collection should show up when I fetch the Root Collection, shouldn't it..."
+      (is (= [{:name        "Rasta Toucan's Personal Collection"
+               :id          (u/get-id (collection/user->personal-collection (mt/user->id :rasta)))
+               :description nil
+               :model       "collection"
+               :can_write   true}]
+             (->> ((mt/user->client :rasta) :get 200 "collection/root/items")
+                  (filter #(str/includes? (:name %) "Personal Collection"))))))
+
+    (testing "For admins, only return our own Personal Collection (!)"
+      (is (= [{:name        "Crowberto Corv's Personal Collection"
+               :id          (u/get-id (collection/user->personal-collection (mt/user->id :crowberto)))
+               :description nil
+               :model       "collection"
+               :can_write   true}]
+             (->> ((mt/user->client :crowberto) :get 200 "collection/root/items")
+                  (filter #(str/includes? (:name %) "Personal Collection")))))
+
+      (testing "That includes sub-collections of Personal Collections! I shouldn't see them!"
+        (mt/with-temp Collection [_ {:name     "Lucky's Sub-Collection"
+                                     :location (collection/children-location
+                                                (collection/user->personal-collection (mt/user->id :lucky)))}]
+          (is (= [{:name        "Crowberto Corv's Personal Collection"
+                   :id          (u/get-id (collection/user->personal-collection (mt/user->id :crowberto)))
+                   :description nil
+                   :model       "collection"
+                   :can_write   true}]
+                 (->> ((mt/user->client :crowberto) :get 200 "collection/root/items")
+                      (filter #(str/includes? (:name %) "Personal Collection"))))))))
+
+    (testing "Can we look for `archived` stuff with this endpoint?"
+      (mt/with-temp Card [card {:name "Business Card", :archived true}]
+        (is (= [{:name                "Business Card"
+                 :description         nil
+                 :collection_position nil
+                 :display             "table"
+                 :favorite            false
+                 :model               "card"}]
+               (for [item ((mt/user->client :crowberto) :get 200 "collection/root/items?archived=true")]
+                 (dissoc item :id))))))))
 
 
 ;;; ----------------------------------- Effective Children, Ancestors, & Location ------------------------------------
 
-(defn- api-get-root-collection-ancestors-and-children
+(defn- api-get-root-collection-ancestors
   "Call the API with Rasta to fetch the 'Root' Collection and put the `:effective_` results in a nice format for the
   tests below."
   [& additional-get-params]
-  [(format-ancestors-and-children ((user->client :rasta) :get 200 "collection/root"))
-   (tu/boolean-ids-and-timestamps (apply (user->client :rasta) :get 200 "collection/root/items" additional-get-params))])
-
-;; Do top-level collections show up as children of the Root Collection?
-(expect
-  [{:effective_ancestors []
-    :effective_location  nil}
-   (map collection-item ["A" "Rasta Toucan's Personal Collection"])]
-  (with-collection-hierarchy [a b c d e f g]
-    (api-get-root-collection-ancestors-and-children)))
-
-;; ...and collapsing children should work for the Root Collection as well
-(expect
-  [{:effective_ancestors []
-    :effective_location  nil}
-   (map collection-item ["B" "D" "F" "Rasta Toucan's Personal Collection"])]
-  (with-collection-hierarchy [b d e f g]
-    (api-get-root-collection-ancestors-and-children)))
-
-;; does `archived` work on Collections as well?
-(expect
-  [{:effective_ancestors []
-    :effective_location  nil}
-   [(collection-item "A")]]
-  (with-collection-hierarchy [a b d e f g]
-    (db/update! Collection (u/get-id a) :archived true)
-    (api-get-root-collection-ancestors-and-children :archived true)))
+  (format-ancestors ((mt/user->client :rasta) :get 200 "collection/root")))
+
+(defn- api-get-root-collection-children
+  [& additional-get-params]
+  (mt/boolean-ids-and-timestamps (apply (mt/user->client :rasta) :get 200 "collection/root/items" additional-get-params)))
+
+(deftest fetch-root-collection-items-test
+  (testing "GET /api/collection/root/items"
+    (testing "Do top-level collections show up as children of the Root Collection?"
+      (with-collection-hierarchy [a b c d e f g]
+        (testing "ancestors"
+          (is (= {:effective_ancestors []
+                  :effective_location  nil}
+                 (api-get-root-collection-ancestors))))
+        (testing "children"
+          (is (= (map collection-item ["A" "Rasta Toucan's Personal Collection"])
+                 (api-get-root-collection-children))))))
+
+    (testing "...and collapsing children should work for the Root Collection as well"
+      (with-collection-hierarchy [b d e f g]
+        (testing "ancestors"
+          (is (= {:effective_ancestors []
+                  :effective_location  nil}
+                 (api-get-root-collection-ancestors))))
+        (testing "children"
+          (is (= (map collection-item ["B" "D" "F" "Rasta Toucan's Personal Collection"])
+                 (api-get-root-collection-children))))))
+
+    (testing "does `archived` work on Collections as well?"
+      (with-collection-hierarchy [a b d e f g]
+        (db/update! Collection (u/get-id a) :archived true)
+        (testing "ancestors"
+          (is (= {:effective_ancestors []
+                  :effective_location  nil}
+                 (api-get-root-collection-ancestors :archived true))))
+        (testing "children"
+          (is (= [(collection-item "A")]
+                 (api-get-root-collection-children :archived true))))))
+
+    (testing "\n?namespace= parameter"
+      (mt/with-temp* [Collection [{normal-id :id} {:name "Normal Collection"}]
+                      Collection [{coins-id :id}  {:name "Coin Collection", :namespace "currency"}]]
+        (letfn [(collection-names [items]
+                  (->> items
+                       (filter #(and (= (:model %) "collection")
+                                     (#{normal-id coins-id} (:id %))))
+                       (map :name)))]
+          (testing "should only show Collections in the 'default' namespace by default"
+            (is (= ["Normal Collection"]
+                   (collection-names ((mt/user->client :rasta) :get 200 "collection/root/items")))))
+
+          (testing "By passing `:namespace` we should be able to see Collections in that `:namespace`"
+            (testing "?namespace=currency"
+              (is (= ["Coin Collection"]
+                     (collection-names ((mt/user->client :rasta) :get 200 "collection/root/items?namespace=currency")))))
+            (testing "?namespace=stamps"
+              (is (= []
+                     (collection-names ((mt/user->client :rasta) :get 200 "collection/root/items?namespace=stamps")))))))))))
+
+(deftest root-collection-snippets-test
+  (testing "GET /api/collection/root/items?namespace=snippets"
+    (testing "\nNative query snippets should come back when fetching the items in the root Collection of the `:snippets` namespace"
+      (mt/with-temp* [NativeQuerySnippet [{snippet-id :id}   {:name "My Snippet"}]
+                      NativeQuerySnippet [{archived-id :id}  {:name "Archived Snippet", :archived true}]
+                      Dashboard          [{dashboard-id :id} {:name "My Dashboard"}]]
+        (letfn [(only-test-items [results]
+                  (if (sequential? results)
+                    (filter #(#{snippet-id archived-id dashboard-id} (:id %)) results)
+                    results))
+                (only-test-item-names [results]
+                  (let [items (only-test-items results)]
+                    (if (sequential? items)
+                      (map :name items)
+                      items)))]
+          (is (= [{:id    snippet-id
+                   :name  "My Snippet"
+                   :model "snippet"}]
+                 (only-test-items ((mt/user->client :rasta) :get 200 "collection/root/items?namespace=snippets"))))
+
+          (testing "\nSnippets should not come back for the default namespace"
+            (is (= ["My Dashboard"]
+                   (only-test-item-names ((mt/user->client :rasta) :get 200 "collection/root/items")))))
+
+          (testing "\nShould be able to fetch archived Snippets"
+            (is (= ["Archived Snippet"]
+                   (only-test-item-names ((mt/user->client :rasta) :get 200
+                                                 "collection/root/items?namespace=snippets&archived=true")))))
+
+          (testing "\nShould be able to pass ?model=snippet, even though it makes no difference in this case"
+            (is (= ["My Snippet"]
+                   (only-test-item-names ((mt/user->client :rasta) :get 200
+                                          "collection/root/items?namespace=snippets&model=snippet"))))))))))
 
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
 ;;; |                                              POST /api/collection                                              |
 ;;; +----------------------------------------------------------------------------------------------------------------+
 
-;; test that we can create a new collection (POST /api/collection)
-(expect
-  {:name              "Stamp Collection"
-   :slug              "stamp_collection"
-   :description       nil
-   :color             "#123456"
-   :archived          false
-   :location          "/"
-   :personal_owner_id nil}
-  (tu/with-model-cleanup [Collection]
-    (-> ((user->client :crowberto) :post 200 "collection"
-         {:name "Stamp Collection", :color "#123456"})
-        (dissoc :id))))
-
-;; test that non-admins aren't allowed to create a collection in the root collection
-(expect
-  "You don't have permissions to do that."
-  (tu/with-non-admin-groups-no-root-collection-perms
-    ((user->client :rasta) :post 403 "collection"
-     {:name "Stamp Collection", :color "#123456"})))
-
-;; Can a non-admin user with Root Collection perms add a new collection to the Root Collection? (#8949)
-(expect
-  {:name              "Stamp Collection"
-   :description       nil
-   :color             "#123456"
-   :archived          false
-   :location          "/"
-   :personal_owner_id nil
-   :slug              "stamp_collection"}
-  (tu/with-model-cleanup [Collection]
-    (tu/with-non-admin-groups-no-root-collection-perms
-      (-> (tt/with-temp* [PermissionsGroup           [group]
-                          PermissionsGroupMembership [_ {:user_id (user->id :rasta), :group_id (u/get-id group)}]]
+(deftest create-collection-test
+  (testing "POST /api/collection"
+    (testing "\ntest that we can create a new collection"
+      (mt/with-model-cleanup [Collection]
+        (is (= (merge
+                (mt/object-defaults Collection)
+                {:name              "Stamp Collection"
+                 :slug              "stamp_collection"
+                 :color             "#123456"
+                 :archived          false
+                 :location          "/"
+                 :personal_owner_id nil})
+               (-> ((mt/user->client :crowberto) :post 200 "collection"
+                    {:name "Stamp Collection", :color "#123456"})
+                   (dissoc :id))))))
+
+    (testing "\ntest that non-admins aren't allowed to create a collection in the root collection"
+      (mt/with-non-admin-groups-no-root-collection-perms
+        (is (= "You don't have permissions to do that."
+               ((mt/user->client :rasta) :post 403 "collection"
+                {:name "Stamp Collection", :color "#123456"})))))
+
+    (testing "\nCan a non-admin user with Root Collection perms add a new collection to the Root Collection? (#8949)"
+      (mt/with-model-cleanup [Collection]
+        (mt/with-non-admin-groups-no-root-collection-perms
+          (mt/with-temp* [PermissionsGroup           [group]
+                          PermissionsGroupMembership [_ {:user_id (mt/user->id :rasta), :group_id (u/get-id group)}]]
             (perms/grant-collection-readwrite-permissions! group collection/root-collection)
-            ((user->client :rasta) :post 200 "collection"
-             {:name "Stamp Collection", :color "#123456"}))
-          (dissoc :id)))))
-
-;; Can I create a Collection as a child of an existing collection?
-(expect
-  {:id                true
-   :name              "Trading Card Collection"
-   :slug              "trading_card_collection"
-   :description       "Collection of basketball cards including limited-edition holographic Draymond Green"
-   :color             "#ABCDEF"
-   :archived          false
-   :location          "/A/C/D/"
-   :personal_owner_id nil}
-  (tu/with-model-cleanup [Collection]
-    (with-collection-hierarchy [a c d]
-      (-> ((user->client :crowberto) :post 200 "collection"
-           {:name        "Trading Card Collection"
-            :color       "#ABCDEF"
-            :description "Collection of basketball cards including limited-edition holographic Draymond Green"
-            :parent_id   (u/get-id d)})
-          (update :location collection-test/location-path-ids->names)
-          (update :id integer?)))))
-
+            (is (= (merge
+                    (mt/object-defaults Collection)
+                    {:name     "Stamp Collection"
+                     :color    "#123456"
+                     :location "/"
+                     :slug     "stamp_collection"})
+                   (dissoc ((mt/user->client :rasta) :post 200 "collection"
+                            {:name "Stamp Collection", :color "#123456"})
+                           :id)))))))
+
+    (testing "\nCan I create a Collection as a child of an existing collection?"
+      (mt/with-model-cleanup [Collection]
+        (with-collection-hierarchy [a c d]
+          (is (= (merge
+                  (mt/object-defaults Collection)
+                  {:id          true
+                   :name        "Trading Card Collection"
+                   :slug        "trading_card_collection"
+                   :description "Collection of basketball cards including limited-edition holographic Draymond Green"
+                   :color       "#ABCDEF"
+                   :location    "/A/C/D/"})
+                 (-> ((mt/user->client :crowberto) :post 200 "collection"
+                      {:name        "Trading Card Collection"
+                       :color       "#ABCDEF"
+                       :description "Collection of basketball cards including limited-edition holographic Draymond Green"
+                       :parent_id   (u/get-id d)})
+                     (update :location collection-test/location-path-ids->names)
+                     (update :id integer?)))))))
+
+    (testing "\nShould be able to create a Collection in a different namespace"
+      (let [collection-name (mt/random-name)]
+        (try
+          (is (schema= {:name      (s/eq collection-name)
+                        :namespace (s/eq "snippets")
+                        s/Keyword  s/Any}
+                       ((mt/user->client :crowberto) :post 200 "collection"
+                        {:name       collection-name
+                         :color      "#f38630"
+                         :descrption "My SQL Snippets"
+                         :namespace  "snippets"})))
+          (finally
+            (db/delete! Collection :name collection-name)))))))
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
 ;;; |                                            PUT /api/collection/:id                                             |
 ;;; +----------------------------------------------------------------------------------------------------------------+
 
-;; test that we can update a collection (PUT /api/collection/:id)
-(tt/expect-with-temp [Collection [collection]]
-  {:id                (u/get-id collection)
-   :name              "My Beautiful Collection"
-   :slug              "my_beautiful_collection"
-   :description       nil
-   :color             "#ABCDEF"
-   :archived          false
-   :location          "/"
-   :personal_owner_id nil}
-  ((user->client :crowberto) :put 200 (str "collection/" (u/get-id collection))
-   {:name "My Beautiful Collection", :color "#ABCDEF"}))
-
-;; check that users without write perms aren't allowed to update a Collection
-(expect
-  "You don't have permissions to do that."
-  (tu/with-non-admin-groups-no-root-collection-perms
-    (tt/with-temp Collection [collection]
-      ((user->client :rasta) :put 403 (str "collection/" (u/get-id collection))
-       {:name "My Beautiful Collection", :color "#ABCDEF"}))))
-
-;; Archiving a collection should delete any alerts associated with questions in the collection
-(expect
-  {:emails (merge (et/email-to :crowberto {:subject "One of your alerts has stopped working",
-                                           :body    {"the question was archived by Crowberto Corv" true}})
-                  (et/email-to :rasta {:subject "One of your alerts has stopped working",
-                                       :body    {"the question was archived by Crowberto Corv" true}}))
-   :pulse  nil}
-  (tt/with-temp* [Collection            [{collection-id :id}]
-                  Card                  [{card-id :id :as card} {:collection_id collection-id}]
-                  Pulse                 [{pulse-id :id} {:alert_condition  "rows"
-                                                         :alert_first_only false
-                                                         :creator_id       (user->id :rasta)
-                                                         :name             "Original Alert Name"}]
-
-                  PulseCard             [_              {:pulse_id pulse-id
-                                                         :card_id  card-id
-                                                         :position 0}]
-                  PulseChannel          [{pc-id :id}    {:pulse_id pulse-id}]
-                  PulseChannelRecipient [{pcr-id-1 :id} {:user_id          (user->id :crowberto)
-                                                         :pulse_channel_id pc-id}]
-                  PulseChannelRecipient [{pcr-id-2 :id} {:user_id          (user->id :rasta)
-                                                         :pulse_channel_id pc-id}]]
-    (et/with-fake-inbox
-      (et/with-expected-messages 2
-        ((user->client :crowberto) :put 200 (str "collection/" collection-id)
-         {:name "My Beautiful Collection", :color "#ABCDEF", :archived true}))
-      (array-map
-       :emails (et/regex-email-bodies #"the question was archived by Crowberto Corv")
-       :pulse  (Pulse pulse-id)))))
-
-;; I shouldn't be allowed to archive a Collection without proper perms
-(expect
-  "You don't have permissions to do that."
-  (tu/with-non-admin-groups-no-root-collection-perms
-    (tt/with-temp Collection [collection]
-      ((user->client :rasta) :put 403 (str "collection/" (u/get-id collection))
-       {:archived true}))))
-
-;; Perms checking should be recursive as well...
-;;
-;; Create Collections A > B, and grant permissions for A. You should not be allowed to archive A because you would
-;; also need perms for B
-(expect
-  "You don't have permissions to do that."
-  (tu/with-non-admin-groups-no-root-collection-perms
-    (tt/with-temp* [Collection [collection-a]
-                    Collection [collection-b {:location (collection/children-location collection-a)}]]
-      (perms/grant-collection-readwrite-permissions! (group/all-users) collection-a)
-      ((user->client :rasta) :put 403 (str "collection/" (u/get-id collection-a))
-       {:archived true}))))
-
-;; Can I *change* the `location` of a Collection? (i.e. move it into a different parent Colleciton)
-(expect
-  {:id                true
-   :name              "E"
-   :slug              "e"
-   :description       nil
-   :color             "#ABCDEF"
-   :archived          false
-   :location          "/A/B/"
-   :personal_owner_id nil}
-  (with-collection-hierarchy [a b e]
-    (-> ((user->client :crowberto) :put 200 (str "collection/" (u/get-id e))
-         {:parent_id (u/get-id b)})
-        (update :location collection-test/location-path-ids->names)
-        (update :id integer?))))
-
-;; I shouldn't be allowed to move the Collection without proper perms.
-;; If I want to move A into B, I should need permissions for both A and B
-(expect
-  "You don't have permissions to do that."
-  (tu/with-non-admin-groups-no-root-collection-perms
-    (tt/with-temp* [Collection [collection-a]
-                    Collection [collection-b]]
-      (perms/grant-collection-readwrite-permissions! (group/all-users) collection-a)
-      ((user->client :rasta) :put 403 (str "collection/" (u/get-id collection-a))
-       {:parent_id (u/get-id collection-b)}))))
-
-;; Perms checking should be recursive as well...
-;;
-;; Create A, B, and C; B is a child of A. Grant perms for A and B. Moving A into C should fail because we need perms
-;; for C:
-;; (collections with readwrite perms marked below with a `*`)
-;;
-;; A* -> B*  ==>  C -> A -> B
-;; C
-(expect
-  "You don't have permissions to do that."
-  (tu/with-non-admin-groups-no-root-collection-perms
-    (tt/with-temp* [Collection [collection-a]
-                    Collection [collection-b {:location (collection/children-location collection-a)}]
-                    Collection [collection-c]]
-      (doseq [collection [collection-a collection-b]]
-        (perms/grant-collection-readwrite-permissions! (group/all-users) collection))
-      ((user->client :rasta) :put 403 (str "collection/" (u/get-id collection-a))
-       {:parent_id (u/get-id collection-c)}))))
-
-
-;; Create A, B, and C; B is a child of A. Grant perms for A and C. Moving A into C should fail because we need perms
-;; for B:
-;; (collections with readwrite perms marked below with a `*`)
-;;
-;; A* -> B  ==>  C -> A -> B
-;; C*
-(expect
-  "You don't have permissions to do that."
-  (tu/with-non-admin-groups-no-root-collection-perms
-    (tt/with-temp* [Collection [collection-a]
-                    Collection [collection-b {:location (collection/children-location collection-a)}]
-                    Collection [collection-c]]
-      (doseq [collection [collection-a collection-c]]
-        (perms/grant-collection-readwrite-permissions! (group/all-users) collection))
-      ((user->client :rasta) :put 403 (str "collection/" (u/get-id collection-a))
-       {:parent_id (u/get-id collection-c)}))))
-
-;; Create A, B, and C; B is a child of A. Grant perms for B and C. Moving A into C should fail because we need perms
-;; for A:
-;; (collections with readwrite perms marked below with a `*`)
-;;
-;; A -> B*  ==>  C -> A -> B
-;; C*
-(expect
-  "You don't have permissions to do that."
-  (tu/with-non-admin-groups-no-root-collection-perms
-    (tt/with-temp* [Collection [collection-a]
-                    Collection [collection-b {:location (collection/children-location collection-a)}]
-                    Collection [collection-c]]
-      (doseq [collection [collection-b collection-c]]
-        (perms/grant-collection-readwrite-permissions! (group/all-users) collection))
-      ((user->client :rasta) :put 403 (str "collection/" (u/get-id collection-a))
-       {:parent_id (u/get-id collection-c)}))))
+(deftest update-collection-test
+  (testing "PUT /api/collection/:id"
+    (testing "test that we can update a collection"
+      (mt/with-temp Collection [collection]
+        (is (= (merge
+                (mt/object-defaults Collection)
+                {:id       (u/get-id collection)
+                 :name     "My Beautiful Collection"
+                 :slug     "my_beautiful_collection"
+                 :color    "#ABCDEF"
+                 :location "/"})
+               ((mt/user->client :crowberto) :put 200 (str "collection/" (u/get-id collection))
+                {:name "My Beautiful Collection", :color "#ABCDEF"})))))
+
+    (testing "check that users without write perms aren't allowed to update a Collection"
+      (mt/with-non-admin-groups-no-root-collection-perms
+        (mt/with-temp Collection [collection]
+          (is (= "You don't have permissions to do that."
+                 ((mt/user->client :rasta) :put 403 (str "collection/" (u/get-id collection))
+                  {:name "My Beautiful Collection", :color "#ABCDEF"}))))))))
+
+(deftest archive-collection-test
+  (testing "PUT /api/collection/:id"
+    (testing "Archiving a collection should delete any alerts associated with questions in the collection"
+      (mt/with-temp* [Collection            [{collection-id :id}]
+                      Card                  [{card-id :id :as card} {:collection_id collection-id}]
+                      Pulse                 [{pulse-id :id} {:alert_condition  "rows"
+                                                             :alert_first_only false
+                                                             :creator_id       (mt/user->id :rasta)
+                                                             :name             "Original Alert Name"}]
+
+                      PulseCard             [_              {:pulse_id pulse-id
+                                                             :card_id  card-id
+                                                             :position 0}]
+                      PulseChannel          [{pc-id :id}    {:pulse_id pulse-id}]
+                      PulseChannelRecipient [{pcr-id-1 :id} {:user_id          (mt/user->id :crowberto)
+                                                             :pulse_channel_id pc-id}]
+                      PulseChannelRecipient [{pcr-id-2 :id} {:user_id          (mt/user->id :rasta)
+                                                             :pulse_channel_id pc-id}]]
+        (mt/with-fake-inbox
+          (mt/with-expected-messages 2
+            ((mt/user->client :crowberto) :put 200 (str "collection/" collection-id)
+             {:name "My Beautiful Collection", :color "#ABCDEF", :archived true}))
+          (testing "emails"
+            (is (= (merge (mt/email-to :crowberto {:subject "One of your alerts has stopped working",
+                                                   :body    {"the question was archived by Crowberto Corv" true}})
+                          (mt/email-to :rasta {:subject "One of your alerts has stopped working",
+                                               :body    {"the question was archived by Crowberto Corv" true}}))
+                   (mt/regex-email-bodies #"the question was archived by Crowberto Corv"))))
+          (testing "Pulse"
+            (is (= nil
+                   (Pulse pulse-id)))))))
+
+    (testing "I shouldn't be allowed to archive a Collection without proper perms"
+      (mt/with-non-admin-groups-no-root-collection-perms
+        (mt/with-temp Collection [collection]
+          (is (= "You don't have permissions to do that."
+                 ((mt/user->client :rasta) :put 403 (str "collection/" (u/get-id collection))
+                  {:archived true})))))
+
+      (testing "Perms checking should be recursive as well..."
+        ;; Create Collections A > B, and grant permissions for A. You should not be allowed to archive A because you
+        ;; would also need perms for B
+        (mt/with-non-admin-groups-no-root-collection-perms
+          (mt/with-temp* [Collection [collection-a]
+                          Collection [collection-b {:location (collection/children-location collection-a)}]]
+            (perms/grant-collection-readwrite-permissions! (group/all-users) collection-a)
+            (is (= "You don't have permissions to do that."
+                   ((mt/user->client :rasta) :put 403 (str "collection/" (u/get-id collection-a))
+                    {:archived true})))))))))
+
+(deftest move-collection-test
+  (testing "PUT /api/collection/:id"
+    (testing "Can I *change* the `location` of a Collection? (i.e. move it into a different parent Collection)"
+      (with-collection-hierarchy [a b e]
+        (is (= (merge
+                (mt/object-defaults Collection)
+                {:id       true
+                 :name     "E"
+                 :slug     "e"
+                 :color    "#ABCDEF"
+                 :location "/A/B/"})
+               (-> ((mt/user->client :crowberto) :put 200 (str "collection/" (u/get-id e))
+                    {:parent_id (u/get-id b)})
+                   (update :location collection-test/location-path-ids->names)
+                   (update :id integer?))))))
+
+    (testing "I shouldn't be allowed to move the Collection without proper perms."
+      (testing "If I want to move A into B, I should need permissions for both A and B"
+        (mt/with-non-admin-groups-no-root-collection-perms
+          (mt/with-temp* [Collection [collection-a]
+                          Collection [collection-b]]
+            (perms/grant-collection-readwrite-permissions! (group/all-users) collection-a)
+            (is (= "You don't have permissions to do that."
+                   ((mt/user->client :rasta) :put 403 (str "collection/" (u/get-id collection-a))
+                    {:parent_id (u/get-id collection-b)}))))))
+
+      (testing "Perms checking should be recursive as well..."
+        (testing "Create A, B, and C; B is a child of A."
+          (testing "Grant perms for A and B. Moving A into C should fail because we need perms for C"
+            ;; (collections with readwrite perms marked below with a `*`)
+            ;; A* -> B* ==> C -> A -> B
+            (mt/with-non-admin-groups-no-root-collection-perms
+              (mt/with-temp* [Collection [collection-a]
+                              Collection [collection-b {:location (collection/children-location collection-a)}]
+                              Collection [collection-c]]
+                (doseq [collection [collection-a collection-b]]
+                  (perms/grant-collection-readwrite-permissions! (group/all-users) collection))
+                (is (= "You don't have permissions to do that."
+                       ((mt/user->client :rasta) :put 403 (str "collection/" (u/get-id collection-a))
+                        {:parent_id (u/get-id collection-c)}))))))
+
+          (testing "Grant perms for A and C. Moving A into C should fail because we need perms for B."
+            ;; A* -> B  ==>  C -> A -> B
+            ;; C*
+            (mt/with-non-admin-groups-no-root-collection-perms
+              (mt/with-temp* [Collection [collection-a]
+                              Collection [collection-b {:location (collection/children-location collection-a)}]
+                              Collection [collection-c]]
+                (doseq [collection [collection-a collection-c]]
+                  (perms/grant-collection-readwrite-permissions! (group/all-users) collection))
+                (is (= "You don't have permissions to do that."
+                       ((mt/user->client :rasta) :put 403 (str "collection/" (u/get-id collection-a))
+                        {:parent_id (u/get-id collection-c)}))))))
+
+          (testing "Grant perms for B and C. Moving A into C should fail because we need perms for A"
+            ;; A -> B*  ==>  C -> A -> B
+            ;; C*
+            (mt/with-non-admin-groups-no-root-collection-perms
+              (mt/with-temp* [Collection [collection-a]
+                              Collection [collection-b {:location (collection/children-location collection-a)}]
+                              Collection [collection-c]]
+                (doseq [collection [collection-b collection-c]]
+                  (perms/grant-collection-readwrite-permissions! (group/all-users) collection))
+                (is (= "You don't have permissions to do that."
+                       ((mt/user->client :rasta) :put 403 (str "collection/" (u/get-id collection-a))
+                        {:parent_id (u/get-id collection-c)})))))))))))
+
+
+;;; +----------------------------------------------------------------------------------------------------------------+
+;;; |                            GET /api/collection/graph and PUT /api/collection/graph                             |
+;;; +----------------------------------------------------------------------------------------------------------------+
+
+(deftest graph-test
+  (mt/with-temp* [Collection [{default-a :id}   {:location "/"}]
+                  Collection [{default-ab :id}  {:location (format "/%d/" default-a)}]
+                  Collection [{currency-a :id}  {:namespace "currency", :location "/"}]
+                  Collection [{currency-ab :id} {:namespace "currency", :location (format "/%d/" currency-a)}]
+                  PermissionsGroup [{group-id :id}]]
+    (letfn [(nice-graph [graph]
+              (let [id->alias {default-a   "Default A"
+                               default-ab  "Default A -> B"
+                               currency-a  "Currency A"
+                               currency-ab "Currency A -> B"}]
+                (transduce
+                 identity
+                 (fn
+                   ([graph]
+                    (-> (get-in graph [:groups (keyword (str group-id))])
+                        (select-keys (vals id->alias))))
+                   ([graph [collection-id k]]
+                    (graph.test/replace-collection-ids collection-id graph k)))
+                 graph
+                 id->alias)))]
+      (doseq [collection [default-a default-ab currency-a currency-ab]]
+        (perms/grant-collection-read-permissions! group-id collection))
+      (testing "GET /api/collection/graph\n"
+        (testing "Should be able to fetch the permissions graph for the default namespace"
+          (is (= {"Default A" "read", "Default A -> B" "read"}
+                 (nice-graph ((mt/user->client :crowberto) :get 200 "collection/graph")))))
+
+        (testing "Should be able to fetch the permissions graph for a non-default namespace"
+          (is (= {"Currency A" "read", "Currency A -> B" "read"}
+                 (nice-graph ((mt/user->client :crowberto) :get 200 "collection/graph?namespace=currency")))))
+
+        (testing "have to be a superuser"
+          (is (= "You don't have permissions to do that."
+                 ((mt/user->client :rasta) :get 403 "collection/graph")))))
+
+      (testing "PUT /api/collection/graph\n"
+        (testing "Should be able to update the graph for the default namespace.\n"
+          (testing "Should ignore updates to Collections outside of the namespace"
+            (let [response ((mt/user->client :crowberto) :put 200 "collection/graph"
+                            (assoc (graph/graph) :groups {group-id {default-ab :write, currency-ab :write}}))]
+              (is (= {"Default A" "read", "Default A -> B" "write"}
+                     (nice-graph response))))))
+
+        (testing "Should be able to update the graph for a non-default namespace.\n"
+          (testing "Should ignore updates to Collections outside of the namespace"
+            (let [response ((mt/user->client :crowberto) :put 200 "collection/graph?namespace=currency"
+                            (assoc (graph/graph) :groups {group-id {default-a :write, currency-a :write}}))]
+              (is (= {"Currency A" "write", "Currency A -> B" "read"}
+                     (nice-graph response))))))
+
+        (testing "have to be a superuser"
+          (is (= "You don't have permissions to do that."
+                 ((mt/user->client :rasta) :put 403 "collection/graph?namespace=currency"
+                  (assoc (graph/graph) :groups {group-id {default-a :write, currency-a :write}})))))))))
diff --git a/test/metabase/api/search_test.clj b/test/metabase/api/search_test.clj
index 24cb63b3c920211cfd316a49bcfe2a012a083c2a..eada0b6f1f9ff53eb735d207999dc43f103e3da3 100644
--- a/test/metabase/api/search_test.clj
+++ b/test/metabase/api/search_test.clj
@@ -154,7 +154,7 @@
       (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-permissions! group (perms/collection-read-path {:metabase.models.collection.root/is-root? true}))
           (is (= (remove (comp #{"collection"} :model) (default-search-results))
                  (search-request :rasta :q "test")))))))
 
@@ -180,7 +180,7 @@
         (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-permissions! group (perms/collection-read-path {:metabase.models.collection.root/is-root? true}))
             (perms/grant-collection-read-permissions! group collection)
             (is (= (sorted-results
                     (into
@@ -312,3 +312,13 @@
         (perms/revoke-permissions! (group/all-users) db-id)
         (is (= []
                (search-request :rasta :q (:name table))))))))
+
+(deftest collection-namespaces-test
+  (testing "Search should only return Collections in the 'default' namespace"
+    (mt/with-temp* [Collection [c1 {:name "Normal Collection"}]
+                    Collection [c2 {:name "Coin Collection", :namespace "currency"}]]
+      (is (= ["Normal Collection"]
+             (->> (search-request :crowberto :q "Collection")
+                  (filter #(and (= (:model %) "collection")
+                                (#{"Normal Collection" "Coin Collection"} (:name %))))
+                  (map :name)))))))
diff --git a/test/metabase/models/card_test.clj b/test/metabase/models/card_test.clj
index 6f99eae4ed5974518824b41d471e4fef8e7a3b43..3f3198a06350282300ddd27fb717e57fd145340d 100644
--- a/test/metabase/models/card_test.clj
+++ b/test/metabase/models/card_test.clj
@@ -1,103 +1,105 @@
 (ns metabase.models.card-test
   (:require [cheshire.core :as json]
-            [expectations :refer :all]
-            [metabase.models
-             [card :as card :refer :all]
-             [dashboard :refer [Dashboard]]
-             [dashboard-card :refer [DashboardCard]]]
-            [metabase.test
-             [data :as data]
-             [util :as tu]]
-            [metabase.util :as u]
+            [clojure.test :refer :all]
+            [metabase
+             [models :refer [Card Collection Dashboard DashboardCard]]
+             [test :as mt]
+             [util :as u]]
+            [metabase.models.card :as card]
+            [metabase.test.util :as tu]
             [toucan.db :as db]
             [toucan.util.test :as tt]))
 
-;; Check that the :dashboard_count delay returns the correct count of Dashboards a Card is in
-(expect
-  [0 1 2]
-  (tt/with-temp* [Card      [{card-id :id}]
-                  Dashboard [dash-1]
-                  Dashboard [dash-2]]
-    (let [add-card-to-dash!   (fn [dash] (db/insert! DashboardCard :card_id card-id, :dashboard_id (u/get-id dash)))
-          get-dashboard-count (fn [] (dashboard-count (Card card-id)))]
-
-      [(get-dashboard-count)
-       (do (add-card-to-dash! dash-1) (get-dashboard-count))
-       (do (add-card-to-dash! dash-2) (get-dashboard-count))])))
-
-
-;; card-dependencies
-
-(expect
-  {:Segment #{2 3}
-   :Metric  #{}}
-  (card-dependencies
-   {:dataset_query {:type  :query
-                    :query {:filter [:and
-                                     [:> [:field-id 4] "2014-10-19"]
-                                     [:= [:field-id 5] "yes"]
-                                     [:segment 2]
-                                     [:segment 3]]}}}))
-
-(expect
-  {:Segment #{1}
-   :Metric  #{7}}
-  (card-dependencies
-   {:dataset_query {:type  :query
-                    :query {:aggregation [:metric 7]
-                            :filter      [:and
-                                          [:> [:field-id 4] "2014-10-19"]
-                                          [:= [:field-id 5] "yes"]
-                                          [:or [:segment 1] [:!= [:field-id 5] "5"]]]}}}))
-
-(expect
-  {:Segment #{}
-   :Metric  #{}}
-  (card-dependencies
-   {:dataset_query {:type  :query
-                    :query {:aggregation nil
-                            :filter      nil}}}))
-
-
-;; Test that when somebody archives a Card, it is removed from any Dashboards it belongs to
-(expect
-  0
-  (tt/with-temp* [Dashboard     [dashboard]
-                  Card          [card]
-                  DashboardCard [dashcard  {:dashboard_id (u/get-id dashboard), :card_id (u/get-id card)}]]
-    (db/update! Card (u/get-id card) :archived true)
-    (db/count DashboardCard :dashboard_id (u/get-id dashboard))))
-
-
-;; test that a Card's :public_uuid comes back if public sharing is enabled...
-(expect
-  (tu/with-temporary-setting-values [enable-public-sharing true]
-    (tt/with-temp Card [card {:public_uuid (str (java.util.UUID/randomUUID))}]
-      (boolean (:public_uuid card)))))
-
-;; ...but if public sharing is *disabled* it should come back as `nil`
-(expect
-  nil
-  (tu/with-temporary-setting-values [enable-public-sharing false]
-    (tt/with-temp Card [card {:public_uuid (str (java.util.UUID/randomUUID))}]
-      (:public_uuid card))))
+(deftest dashboard-count-test
+  (testing "Check that the :dashboard_count delay returns the correct count of Dashboards a Card is in"
+    (tt/with-temp* [Card      [{card-id :id}]
+                    Dashboard [dash-1]
+                    Dashboard [dash-2]]
+      (letfn [(add-card-to-dash! [dash]
+                (db/insert! DashboardCard :card_id card-id, :dashboard_id (u/get-id dash)))
+              (get-dashboard-count []
+                (card/dashboard-count (Card card-id)))]
+        (is (= 0
+               (get-dashboard-count)))
+        (testing "add to a Dashboard"
+          (add-card-to-dash! dash-1)
+          (is (= 1
+                 (get-dashboard-count))))
+        (testing "add to a second Dashboard"
+          (add-card-to-dash! dash-2)
+          (is (= 2
+                 (get-dashboard-count))))))))
+
+(deftest card-dependencies-test
+  (testing "Segment dependencies"
+    (is (= {:Segment #{2 3}
+            :Metric  #{}}
+           (card/card-dependencies
+            {:dataset_query {:type  :query
+                             :query {:filter [:and
+                                              [:> [:field-id 4] "2014-10-19"]
+                                              [:= [:field-id 5] "yes"]
+                                              [:segment 2]
+                                              [:segment 3]]}}}))))
+
+  (testing "Segment and Metric dependencies"
+    (is (= {:Segment #{1}
+            :Metric  #{7}}
+           (card/card-dependencies
+            {:dataset_query {:type  :query
+                             :query {:aggregation [:metric 7]
+                                     :filter      [:and
+                                                   [:> [:field-id 4] "2014-10-19"]
+                                                   [:= [:field-id 5] "yes"]
+                                                   [:or [:segment 1] [:!= [:field-id 5] "5"]]]}}}))))
+
+  (testing "no dependencies"
+    (is (= {:Segment #{}
+            :Metric  #{}}
+           (card/card-dependencies
+            {:dataset_query {:type  :query
+                             :query {:aggregation nil
+                                     :filter      nil}}})))))
+
+(deftest remove-from-dashboards-when-archiving-test
+  (testing "Test that when somebody archives a Card, it is removed from any Dashboards it belongs to"
+    (tt/with-temp* [Dashboard     [dashboard]
+                    Card          [card]
+                    DashboardCard [dashcard  {:dashboard_id (u/get-id dashboard), :card_id (u/get-id card)}]]
+      (db/update! Card (u/get-id card) :archived true)
+      (is (= 0
+             (db/count DashboardCard :dashboard_id (u/get-id dashboard)))))))
+
+(deftest public-sharing-test
+  (testing "test that a Card's :public_uuid comes back if public sharing is enabled..."
+    (tu/with-temporary-setting-values [enable-public-sharing true]
+      (tt/with-temp Card [card {:public_uuid (str (java.util.UUID/randomUUID))}]
+        (is (schema= u/uuid-regex
+                     (:public_uuid card)))))
+
+    (testing "...but if public sharing is *disabled* it should come back as `nil`"
+      (tu/with-temporary-setting-values [enable-public-sharing false]
+        (tt/with-temp Card [card {:public_uuid (str (java.util.UUID/randomUUID))}]
+          (is (= nil
+                 (:public_uuid card))))))))
 
 (defn- dummy-dataset-query [database-id]
-  {:database (data/id)
+  {:database (mt/id)
    :type     :native
    :native   {:query "SELECT count(*) FROM toucan_sightings;"}})
 
-(expect
-  [{:name "some name"    :database_id (data/id)}
-   {:name "another name" :database_id (data/id)}]
+(deftest database-id-test
   (tt/with-temp Card [{:keys [id] :as card} {:name          "some name"
-                                             :dataset_query (dummy-dataset-query (data/id))
-                                             :database_id   (data/id)}]
-    [(into {} (db/select-one [Card :name :database_id] :id id))
-     (do
-       (db/update! Card id {:name          "another name"
-                            :dataset_query (dummy-dataset-query (data/id))})
-       (into {} (db/select-one [Card :name :database_id] :id id)))]))
+                                             :dataset_query (dummy-dataset-query (mt/id))
+                                             :database_id   (mt/id)}]
+    (testing "before update"
+      (is (= {:name "some name", :database_id (mt/id)}
+             (into {} (db/select-one [Card :name :database_id] :id id)))))
+    (db/update! Card id {:name          "another name"
+                         :dataset_query (dummy-dataset-query (mt/id))})
+    (testing "after update"
+      (is (= {:name "another name" :database_id (mt/id)}
+             (into {} (db/select-one [Card :name :database_id] :id id)))))))
 
 
 ;;; ------------------------------------------ Circular Reference Detection ------------------------------------------
@@ -106,7 +108,7 @@
   "Generate values for a Card with `source-table` for use with `with-temp`."
   {:style/indent 1}
   [source-table & {:as kvs}]
-  (merge {:dataset_query {:database (data/id)
+  (merge {:dataset_query {:database (mt/id)
                           :type     :query
                           :query    {:source-table source-table}}}
          kvs))
@@ -119,39 +121,55 @@
                                ;; we have to manually JSON-encode since we're skipping normal pre-update stuff
                                (update :dataset_query json/generate-string))}))
 
-;; If a Card uses itself as a source, perms calculations should fallback to the 'only admins can see it' perms of
-;; #{"/db/0"} (DB 0 will never exist, so regular users will never get to see it, but because admins have root perms,
-;; they will still get to see it and perhaps fix it.)
-(expect
-  Exception
-  (tt/with-temp Card [card (card-with-source-table (data/id :venues))]
-    ;; now try to make the Card reference itself. Should throw Exception
-    (db/update! Card (u/get-id card)
-      (card-with-source-table (str "card__" (u/get-id card))))))
-
-;; Do the same stuff with circular reference between two Cards... (A -> B -> A)
-(expect
-  Exception
-  (tt/with-temp* [Card [card-a (card-with-source-table (data/id :venues))]
-                  Card [card-b (card-with-source-table (str "card__" (u/get-id card-a)))]]
-    (db/update! Card (u/get-id card-a)
-      (card-with-source-table (str "card__" (u/get-id card-b))))))
-
-;; ok now try it with A -> C -> B -> A
-(expect
-  Exception
-  (tt/with-temp* [Card [card-a (card-with-source-table (data/id :venues))]
-                  Card [card-b (card-with-source-table (str "card__" (u/get-id card-a)))]
-                  Card [card-c (card-with-source-table (str "card__" (u/get-id card-b)))]]
-    (db/update! Card (u/get-id card-a)
-                (card-with-source-table (str "card__" (u/get-id card-c))))))
-
-(expect
-  #{1}
-  (#'card/extract-ids :segment {:query {:fields [[:segment 1]
-                                                 [:metric 2]]}}))
-
-(expect
-  #{2}
-  (#'card/extract-ids :metric {:query {:fields [[:segment 1]
-                                                [:metric 2]]}}))
+(deftest circular-reference-test
+  (testing "Should throw an Exception if saving a Card that references itself"
+    (tt/with-temp Card [card (card-with-source-table (mt/id :venues))]
+      ;; now try to make the Card reference itself. Should throw Exception
+      (is (thrown?
+           Exception
+           (db/update! Card (u/get-id card)
+             (card-with-source-table (str "card__" (u/get-id card))))))))
+
+  (testing "Do the same stuff with circular reference between two Cards... (A -> B -> A)"
+    (tt/with-temp* [Card [card-a (card-with-source-table (mt/id :venues))]
+                    Card [card-b (card-with-source-table (str "card__" (u/get-id card-a)))]]
+      (is (thrown?
+           Exception
+           (db/update! Card (u/get-id card-a)
+             (card-with-source-table (str "card__" (u/get-id card-b))))))))
+
+  (testing "ok now try it with A -> C -> B -> A"
+    (tt/with-temp* [Card [card-a (card-with-source-table (mt/id :venues))]
+                    Card [card-b (card-with-source-table (str "card__" (u/get-id card-a)))]
+                    Card [card-c (card-with-source-table (str "card__" (u/get-id card-b)))]]
+      (is (thrown?
+           Exception
+           (db/update! Card (u/get-id card-a)
+             (card-with-source-table (str "card__" (u/get-id card-c)))))))))
+
+(deftest extract-ids-test
+  (doseq [[ids-type expected] {:segment #{1}
+                               :metric  #{2}}]
+    (testing (pr-str (list 'extract-ids ids-type 'card))
+      (is (= expected
+             (#'card/extract-ids ids-type {:query {:fields [[:segment 1]
+                                                            [:metric 2]]}}))))))
+
+(deftest validate-collection-namespace-test
+  (mt/with-temp Collection [{collection-id :id} {:namespace "currency"}]
+    (testing "Shouldn't be able to create a Card in a non-normal Collection"
+      (let [card-name (mt/random-name)]
+        (try
+          (is (thrown-with-msg?
+               clojure.lang.ExceptionInfo
+               #"A Card can only go in Collections in the \"default\" namespace"
+               (db/insert! Card (assoc (tt/with-temp-defaults Card) :collection_id collection-id, :name card-name))))
+          (finally
+            (db/delete! Card :name card-name)))))
+
+    (testing "Shouldn't be able to move a Card to a non-normal Collection"
+      (mt/with-temp Card [{card-id :id}]
+        (is (thrown-with-msg?
+             clojure.lang.ExceptionInfo
+             #"A Card can only go in Collections in the \"default\" namespace"
+             (db/update! Card card-id {:collection_id collection-id})))))))
diff --git a/test/metabase/models/collection/graph_test.clj b/test/metabase/models/collection/graph_test.clj
new file mode 100644
index 0000000000000000000000000000000000000000..3f3fe161ed76d4d16d8dcd6cc5ecc673150185cd
--- /dev/null
+++ b/test/metabase/models/collection/graph_test.clj
@@ -0,0 +1,383 @@
+(ns metabase.models.collection.graph-test
+  (:require [clojure.test :refer :all]
+            [medley.core :as m]
+            [metabase
+             [test :as mt]
+             [util :as u]]
+            [metabase.api.common :refer [*current-user-id*]]
+            [metabase.models
+             [collection :as collection :refer [Collection]]
+             [collection-revision :as collection-revision :refer [CollectionRevision]]
+             [permissions :as perms]
+             [permissions-group :as group :refer [PermissionsGroup]]]
+            [metabase.models.collection.graph :as graph]
+            [metabase.test.fixtures :as fixtures]
+            [metabase.util.schema :as su]
+            [schema.core :as s]
+            [toucan.db :as db]))
+
+(use-fixtures :once (fixtures/initialize :db :test-users :test-users-personal-collections))
+
+(defn- lucky-collection-children-location []
+  (collection/children-location (collection/user->personal-collection (mt/user->id :lucky))))
+
+(defn replace-collection-ids
+  "In Collection perms `graph`, replace instances of the ID of `collection-or-id` with `:COLLECTION`, making it possible
+  to write tests that don't need to know its actual numeric ID."
+  ([collection-or-id graph]
+   (replace-collection-ids collection-or-id graph :COLLECTION))
+
+  ([collection-or-id graph replacement-key]
+   (let [id      (u/get-id collection-or-id)
+         ;; match variations that pop up depending on whether the map was serialized to JSON. 100, :100, or "100"
+         id-keys #{id (str id) (keyword (str id))}]
+     (update graph :groups (partial m/map-vals (partial m/map-keys (fn [collection-id]
+                                                                     (if (id-keys collection-id)
+                                                                       replacement-key
+                                                                       collection-id))))))))
+
+(defn- clear-graph-revisions! []
+  (db/delete! CollectionRevision))
+
+(defn- only-groups
+  "Remove entries for non-'magic' groups from a fetched perms `graph`."
+  [graph groups-or-ids]
+  (update graph :groups select-keys (map u/get-id groups-or-ids)))
+
+(defn- only-collections
+  "Remove entries for Collections whose ID is not in `collection-ids` from a fetched perms `graph`."
+  [graph collections-or-ids]
+  (let [ids (for [collection-or-id collections-or-ids]
+              (if (= :root collection-or-id)
+                collection-or-id
+                (u/get-id collection-or-id)))]
+    (update graph :groups (fn [groups]
+                            (m/map-vals #(select-keys % ids) groups)))))
+
+(defn- graph
+  "Fetch collection graph.
+
+  * `:clear-revisions?` = delete any previously existing collection revision entries so we get revision = 0
+  * `:collections`      = IDs of Collections to keep. `:root` is always kept.
+  * `:groups`           = IDs of Groups to keep. 'Magic' groups are always kept."
+  [& {:keys [clear-revisions? collections groups]}]
+  (when clear-revisions?
+    (clear-graph-revisions!))
+  ;; force lazy creation of the three magic groups as needed
+  (group/all-users)
+  (group/admin)
+  (group/metabot)
+  ;; now fetch the graph
+  (cond-> (-> (graph/graph)
+              (only-groups (concat [(group/all-users) (group/metabot) (group/admin)] groups))
+              (only-collections (cons :root collections)))))
+
+(deftest basic-test
+  (testing "Check that the basic graph works"
+    (mt/with-non-admin-groups-no-root-collection-perms
+      (is (= {:revision 0
+              :groups   {(u/get-id (group/all-users)) {:root :none}
+                         (u/get-id (group/metabot))   {:root :none}
+                         (u/get-id (group/admin))     {:root :write}}}
+             (graph :clear-revisions? true))))))
+
+(deftest new-collection-perms-test
+  (testing "Creating a new Collection shouldn't give perms to anyone but admins"
+    (mt/with-non-admin-groups-no-root-collection-perms
+      (mt/with-temp Collection [collection]
+        (is (= {:revision 0
+                :groups   {(u/get-id (group/all-users)) {:root :none,  :COLLECTION :none}
+                           (u/get-id (group/metabot))   {:root :none,  :COLLECTION :none}
+                           (u/get-id (group/admin))     {:root :write, :COLLECTION :write}}}
+               (replace-collection-ids collection (graph :clear-revisions? true, :collections [collection]))))))))
+
+(deftest read-perms-test
+  (testing "make sure read perms show up correctly"
+    (mt/with-non-admin-groups-no-root-collection-perms
+      (mt/with-temp Collection [collection]
+        (perms/grant-collection-read-permissions! (group/all-users) collection)
+        (is (= {:revision 0
+                :groups   {(u/get-id (group/all-users)) {:root :none,  :COLLECTION :read}
+                           (u/get-id (group/metabot))   {:root :none,  :COLLECTION :none}
+                           (u/get-id (group/admin))     {:root :write, :COLLECTION :write}}}
+               (replace-collection-ids collection (graph :clear-revisions? true, :collections [collection]))))))))
+
+(deftest grant-write-perms-for-new-collections-test
+  (testing "make sure we can grant write perms for new collections (!)"
+    (mt/with-non-admin-groups-no-root-collection-perms
+      (mt/with-temp Collection [collection]
+        (perms/grant-collection-readwrite-permissions! (group/all-users) collection)
+        (is (=  {:revision 0
+                 :groups   {(u/get-id (group/all-users)) {:root :none,  :COLLECTION :write}
+                            (u/get-id (group/metabot))   {:root :none,  :COLLECTION :none}
+                            (u/get-id (group/admin))     {:root :write, :COLLECTION :write}}}
+                (replace-collection-ids collection (graph :clear-revisions? true, :collections [collection]))))))))
+
+(deftest non-magical-groups-test
+  (testing "make sure a non-magical group will show up"
+    (mt/with-temp PermissionsGroup [new-group]
+      (mt/with-non-admin-groups-no-root-collection-perms
+        (is (=   {:revision 0
+                  :groups   {(u/get-id (group/all-users)) {:root :none}
+                             (u/get-id (group/metabot))   {:root :none}
+                             (u/get-id (group/admin))     {:root :write}
+                             (u/get-id new-group)         {:root :none}}}
+                 (graph :clear-revisions? true, :groups [new-group])))))))
+
+(deftest root-collection-read-perms-test
+  (testing "How abut *read* permissions for the Root Collection?"
+    (mt/with-temp PermissionsGroup [new-group]
+      (mt/with-non-admin-groups-no-root-collection-perms
+        (perms/grant-collection-read-permissions! new-group collection/root-collection)
+        (is (= {:revision 0
+                :groups   {(u/get-id (group/all-users)) {:root :none}
+                           (u/get-id (group/metabot))   {:root :none}
+                           (u/get-id (group/admin))     {:root :write}
+                           (u/get-id new-group)         {:root :read}}}
+               (graph :clear-revisions? true, :groups [new-group])))))))
+
+(deftest root-collection-write-perms-test
+  (testing "How about granting *write* permissions for the Root Collection?"
+    (mt/with-temp PermissionsGroup [new-group]
+      (mt/with-non-admin-groups-no-root-collection-perms
+        (perms/grant-collection-readwrite-permissions! new-group collection/root-collection)
+        (is (= {:revision 0
+                :groups   {(u/get-id (group/all-users)) {:root :none}
+                           (u/get-id (group/metabot))   {:root :none}
+                           (u/get-id (group/admin))     {:root :write}
+                           (u/get-id new-group)         {:root :write}}}
+               (graph :clear-revisions? true, :groups [new-group])))))))
+
+(deftest no-op-test
+  (testing "Can we do a no-op update?"
+    ;; need to bind *current-user-id* or the Revision won't get updated
+    (clear-graph-revisions!)
+    (mt/with-non-admin-groups-no-root-collection-perms
+      (binding [*current-user-id* (mt/user->id :crowberto)]
+        (graph/update-graph! (graph :clear-revisions? true))
+        (is (= {:revision 0
+                :groups   {(u/get-id (group/all-users)) {:root :none}
+                           (u/get-id (group/metabot))   {:root :none}
+                           (u/get-id (group/admin))     {:root :write}}}
+               (graph))
+            "revision should not have changed, because there was nothing to do...")))))
+
+(deftest grant-perms-test
+  (testing "Can we give someone read perms via the graph?"
+    (clear-graph-revisions!)
+    (mt/with-non-admin-groups-no-root-collection-perms
+      (mt/with-temp Collection [collection]
+        (binding [*current-user-id* (mt/user->id :crowberto)]
+          (graph/update-graph! (assoc-in (graph :clear-revisions? true)
+                                         [:groups (u/get-id (group/all-users)) (u/get-id collection)]
+                                         :read))
+          (is (= {:revision 1
+                  :groups   {(u/get-id (group/all-users)) {:root :none,  :COLLECTION :read}
+                             (u/get-id (group/metabot))   {:root :none,  :COLLECTION :none}
+                             (u/get-id (group/admin))     {:root :write, :COLLECTION :write}}}
+                 (replace-collection-ids collection (graph :collections [collection]))))))))
+
+  (testing "can we give them *write* perms?"
+    (mt/with-non-admin-groups-no-root-collection-perms
+      (mt/with-temp Collection [collection]
+        (binding [*current-user-id* (mt/user->id :crowberto)]
+          (graph/update-graph! (assoc-in (graph :clear-revisions? true)
+                                         [:groups (u/get-id (group/all-users)) (u/get-id collection)]
+                                         :write))
+          (is (= {:revision 1
+                  :groups   {(u/get-id (group/all-users)) {:root :none,  :COLLECTION :write}
+                             (u/get-id (group/metabot))   {:root :none,  :COLLECTION :none}
+                             (u/get-id (group/admin))     {:root :write, :COLLECTION :write}}}
+                 (replace-collection-ids collection (graph :collections [collection])))))))))
+
+(deftest revoke-perms-test
+  (testing "can we *revoke* perms?"
+    (clear-graph-revisions!)
+    (mt/with-non-admin-groups-no-root-collection-perms
+      (mt/with-temp Collection [collection]
+        (binding [*current-user-id* (mt/user->id :crowberto)]
+          (perms/grant-collection-read-permissions! (group/all-users) collection)
+          (graph/update-graph! (assoc-in (graph :clear-revisions? true)
+                                         [:groups (u/get-id (group/all-users)) (u/get-id collection)]
+                                         :none))
+          (is (= {:revision 1
+                  :groups   {(u/get-id (group/all-users)) {:root :none,  :COLLECTION :none}
+                             (u/get-id (group/metabot))   {:root :none,  :COLLECTION :none}
+                             (u/get-id (group/admin))     {:root :write, :COLLECTION :write}}}
+                 (replace-collection-ids collection (graph :collections [collection])))))))))
+
+(deftest grant-root-permissions-test
+  (testing "Can we grant *read* permissions for the Root Collection?"
+    (mt/with-temp PermissionsGroup [new-group]
+      (clear-graph-revisions!)
+      (mt/with-non-admin-groups-no-root-collection-perms
+        (binding [*current-user-id* (mt/user->id :crowberto)]
+          (graph/update-graph! (assoc-in (graph :clear-revisions? true)
+                                         [:groups (u/get-id new-group) :root]
+                                         :read))
+          (is (= {:revision 1
+                  :groups   {(u/get-id (group/all-users)) {:root :none}
+                             (u/get-id (group/metabot))   {:root :none}
+                             (u/get-id (group/admin))     {:root :write}
+                             (u/get-id new-group)         {:root :read}}}
+                 (graph :groups [new-group])))))))
+
+  (testing "How about granting *write* permissions for the Root Collection?"
+    (mt/with-temp PermissionsGroup [new-group]
+      (clear-graph-revisions!)
+      (mt/with-non-admin-groups-no-root-collection-perms
+        (binding [*current-user-id* (mt/user->id :crowberto)]
+          (graph/update-graph! (assoc-in (graph :clear-revisions? true)
+                                         [:groups (u/get-id new-group) :root]
+                                         :write))
+          (is (= {:revision 1
+                  :groups   {(u/get-id (group/all-users)) {:root :none}
+                             (u/get-id (group/metabot))   {:root :none}
+                             (u/get-id (group/admin))     {:root :write}
+                             (u/get-id new-group)         {:root :write}}}
+                 (graph :groups [new-group]))))))))
+
+(deftest revoke-root-permissions-test
+  (testing "can we *revoke* RootCollection perms?"
+    (mt/with-temp PermissionsGroup [new-group]
+      (clear-graph-revisions!)
+      (mt/with-non-admin-groups-no-root-collection-perms
+        (binding [*current-user-id* (mt/user->id :crowberto)]
+          (perms/grant-collection-readwrite-permissions! new-group collection/root-collection)
+          (graph/update-graph! (assoc-in (graph :clear-revisions? true)
+                                         [:groups (u/get-id new-group) :root]
+                                         :none))
+          (is (= {:revision 1
+                  :groups   {(u/get-id (group/all-users)) {:root :none}
+                             (u/get-id (group/metabot))   {:root :none}
+                             (u/get-id (group/admin))     {:root :write}
+                             (u/get-id new-group)         {:root :none}}}
+                 (graph :groups [new-group]))))))))
+
+(deftest personal-collections-should-not-appear-test
+  (testing "Make sure that personal Collections *do not* appear in the Collections graph"
+    (mt/with-non-admin-groups-no-root-collection-perms
+      (is (= {:revision 0
+              :groups   {(u/get-id (group/all-users)) {:root :none}
+                         (u/get-id (group/metabot))   {:root :none}
+                         (u/get-id (group/admin))     {:root :write}}}
+             (graph :clear-revisions? true)))))
+
+  (testing "Make sure descendants of Personal Collections do not come back as part of the graph either..."
+    (clear-graph-revisions!)
+    (mt/with-non-admin-groups-no-root-collection-perms
+      (mt/with-temp Collection [_ {:location (lucky-collection-children-location)}]
+        (is (= {:revision 0
+                :groups   {(u/get-id (group/all-users)) {:root :none}
+                           (u/get-id (group/metabot))   {:root :none}
+                           (u/get-id (group/admin))     {:root :write}}}
+               (graph)))))))
+
+(deftest disallow-editing-personal-collections-test
+  (testing "Make sure that if we try to be sneaky and edit a Personal Collection via the graph, changes are ignored"
+    (mt/with-non-admin-groups-no-root-collection-perms
+      (let [lucky-personal-collection-id (u/get-id (collection/user->personal-collection (mt/user->id :lucky)))
+            path                         [:groups (u/get-id (group/all-users)) lucky-personal-collection-id]]
+        (mt/throw-if-called graph/update-group-permissions!
+          (graph/update-graph! (assoc-in (graph :clear-revisions? true) path :read)))
+
+        (testing "double-check that the graph is unchanged"
+          (is (= {:revision 0
+                  :groups   {(u/get-id (group/all-users)) {:root :none}
+                             (u/get-id (group/metabot))   {:root :none}
+                             (u/get-id (group/admin))     {:root :write}}}
+                 (graph))))
+
+        (testing "No revision should have been saved"
+          (is (= 0
+                 (collection-revision/latest-id)))))))
+
+  (testing "Make sure you can't be sneaky and edit descendants of Personal Collections either."
+    (mt/with-temp Collection [collection {:location (lucky-collection-children-location)}]
+      (let [lucky-personal-collection-id (u/get-id (collection/user->personal-collection (mt/user->id :lucky)))]
+        (is (thrown?
+             Exception
+             (graph/update-graph! (assoc-in (graph :clear-revisions? true)
+                                            [:groups
+                                             (u/get-id (group/all-users))
+                                             lucky-personal-collection-id
+                                             (u/get-id collection)]
+                                            :read))))))))
+
+(deftest collection-namespace-test
+  (testing "The permissions graph should be namespace-aware.\n"
+    (mt/with-non-admin-groups-no-root-collection-perms
+      (mt/with-temp* [Collection [{default-a :id}   {:location "/"}]
+                      Collection [{default-ab :id}  {:location (format "/%d/" default-a)}]
+                      Collection [{currency-a :id}  {:namespace "currency", :location "/"}]
+                      Collection [{currency-ab :id} {:namespace "currency", :location (format "/%d/" currency-a)}]
+                      PermissionsGroup [{group-id :id}]]
+        (letfn [(nice-graph [graph]
+                  (let [id->alias {default-a   "Default A"
+                                   default-ab  "Default A -> B"
+                                   currency-a  "Currency A"
+                                   currency-ab "Currency A -> B"}]
+                    (transduce
+                     identity
+                     (fn
+                       ([graph]
+                        (-> (get-in graph [:groups group-id])
+                            (select-keys (vals id->alias))))
+                       ([graph [collection-id k]]
+                        (replace-collection-ids collection-id graph k)))
+                     graph
+                     id->alias)))]
+          (doseq [collection [default-a default-ab currency-a currency-ab]]
+            (perms/grant-collection-read-permissions! group-id collection))
+          (testing "Calling (graph) with no args should only show Collections in the default namespace"
+            (is (= {"Default A" :read, "Default A -> B" :read}
+                   (nice-graph (graph/graph))
+                   (nice-graph (graph/graph nil)))))
+
+          (testing "You should be able to pass an different namespace to (graph) to see Collections in that namespace"
+            (is (= {"Currency A" :read, "Currency A -> B" :read}
+                   (nice-graph (graph/graph :currency)))))
+
+          (testing "Should be able to pass `::graph/all` to get a combined graph of all namespaces (used for saving revisions)"
+            (is (= {"Default A" :read, "Default A -> B" :read "Currency A" :read, "Currency A -> B" :read}
+                   (nice-graph (graph/graph ::graph/all)))))
+
+          ;; bind a current user so CollectionRevisions get saved.
+          (mt/with-test-user :crowberto
+            (testing "Should be able to update the graph for the default namespace.\n"
+              (let [before (graph/graph ::graph/all)]
+                (graph/update-graph! (assoc (graph/graph) :groups {group-id {default-ab :write, currency-ab :write}}))
+                (is (= {"Default A" :read, "Default A -> B" :write}
+                       (nice-graph (graph/graph))))
+
+                (testing "Updates to Collections in other namespaces should be ignored"
+                  (is (= {"Currency A" :read, "Currency A -> B" :read}
+                         (nice-graph (graph/graph :currency)))))
+
+                (testing "A CollectionRevision recording the *changes* to the perms graph should be saved."
+                  (is (schema= {:id         su/IntGreaterThanZero
+                                :before     (s/eq (mt/obj->json->obj before))
+                                :after      (s/eq {(keyword (str group-id)) {(keyword (str default-ab)) "write"}})
+                                :user_id    (s/eq (mt/user->id :crowberto))
+                                :created_at java.time.temporal.Temporal
+                                s/Keyword   s/Any}
+                               (db/select-one CollectionRevision {:order-by [[:id :desc]]}))))))
+
+            (testing "Should be able to update the graph for a non-default namespace.\n"
+              (let [before (graph/graph ::graph/all)]
+                (graph/update-graph! :currency (assoc (graph/graph) :groups {group-id {default-a :write, currency-a :write}}))
+                (is (= {"Currency A" :write, "Currency A -> B" :read}
+                       (nice-graph (graph/graph :currency))))
+
+                (testing "Updates to Collections in other namespaces should be ignored"
+                  (is (= {"Default A" :read, "Default A -> B" :write}
+                         (nice-graph (graph/graph)))))
+
+                (testing "A CollectionRevision recording the *changes* to the perms graph should be saved."
+                  (is (schema= {:id         su/IntGreaterThanZero
+                                :before     (s/eq (mt/obj->json->obj before))
+                                :after      (s/eq {(keyword (str group-id)) {(keyword (str currency-a)) "write"}})
+                                :user_id    (s/eq (mt/user->id :crowberto))
+                                :created_at java.time.temporal.Temporal
+                                s/Keyword   s/Any}
+                               (db/select-one CollectionRevision {:order-by [[:id :desc]]}))))))))))))
diff --git a/test/metabase/models/collection_test.clj b/test/metabase/models/collection_test.clj
index dc3a140bc5e876d95493328a59b0f3bebc42010b..6e356b7f19267afbf315dd0bbb766bc65fc0a188 100644
--- a/test/metabase/models/collection_test.clj
+++ b/test/metabase/models/collection_test.clj
@@ -3,400 +3,125 @@
   (:require [clojure
              [string :as str]
              [test :refer :all]]
-            [expectations :refer :all]
-            [medley.core :as m]
-            [metabase.api.common :refer [*current-user-id* *current-user-permissions-set*]]
+            [expectations :refer [expect]]
+            [metabase
+             [models :refer [Card Collection Dashboard NativeQuerySnippet Permissions PermissionsGroup Pulse User]]
+             [test :as mt]
+             [util :as u]]
+            [metabase.api.common :refer [*current-user-permissions-set*]]
             [metabase.models
-             [card :refer [Card]]
-             [collection :as collection :refer [Collection]]
-             [collection-revision :refer [CollectionRevision]]
-             [dashboard :refer [Dashboard]]
-             [permissions :as perms :refer [Permissions]]
-             [permissions-group :as group :refer [PermissionsGroup]]
-             [pulse :refer [Pulse]]
-             [user :refer [User]]]
-            [metabase.test
-             [fixtures :as fixtures]
-             [util :as tu]]
-            [metabase.test.data.users :as test-users]
-            [metabase.util :as u]
+             [collection :as collection]
+             [permissions :as perms]]
+            [metabase.test.fixtures :as fixtures]
+            [metabase.util.schema :as su]
+            [schema.core :as s]
             [toucan
              [db :as db]
-             [hydrate :refer [hydrate]]]
-            [toucan.util.test :as tt]))
+             [hydrate :refer [hydrate]]]))
 
 (use-fixtures :once (fixtures/initialize :db :test-users :test-users-personal-collections))
 
 (defn- lucky-collection-children-location []
-  (collection/children-location (collection/user->personal-collection (test-users/user->id :lucky))))
-
-(defn- replace-collection-ids
-  "In Collection perms `graph`, replace instances of the ID of `collection-or-id` with `:COLLECTION`, making it possible
-  to write tests that don't need to know its actual numeric ID."
-  [collection-or-id graph]
-  (update graph :groups (partial m/map-vals (partial m/map-keys (fn [collection-id]
-                                                                  (if (= collection-id (u/get-id collection-or-id))
-                                                                    :COLLECTION
-                                                                    collection-id))))))
-
-;; test that we can create a new Collection with valid inputs
-(expect
-  {:name              "My Favorite Cards"
-   :slug              "my_favorite_cards"
-   :description       nil
-   :color             "#ABCDEF"
-   :archived          false
-   :location          "/"
-   :personal_owner_id nil}
-  (tt/with-temp Collection [collection {:name "My Favorite Cards", :color "#ABCDEF"}]
-    (dissoc collection :id)))
+  (collection/children-location (collection/user->personal-collection (mt/user->id :lucky))))
+
+(deftest create-collection-test
+  (testing "test that we can create a new Collection with valid inputs"
+    (mt/with-temp Collection [collection {:name "My Favorite Cards", :color "#ABCDEF"}]
+      (is (= (merge
+              (mt/object-defaults Collection)
+              {:name              "My Favorite Cards"
+               :slug              "my_favorite_cards"
+               :description       nil
+               :color             "#ABCDEF"
+               :archived          false
+               :location          "/"
+               :personal_owner_id nil})
+             (mt/derecordize (dissoc collection :id)))))))
 
 (deftest color-validation-test
   (testing "Collection colors should be validated when inserted into the DB"
-    (are [test-msg input] (is (thrown? Exception
-                                       (db/insert! Collection {:name "My Favorite Cards", :color input}))
-                              test-msg)
-      "Missing color"       nil
-      "Too short"           #"ABC"
-      "Invalid chars"       "#BCDEFG"
-      "Too long"            #"ABCDEFF"
-      "Missing hash prefix" "ABCDEF")))
-
-;; double-check that `with-temp-defaults` are working correctly for Collection
-(expect
-  :ok
-  (tt/with-temp* [Collection [_]]
-    :ok))
-
-;; test that duplicate names ARE allowed
-(expect
-  :ok
-  (tt/with-temp* [Collection [_ {:name "My Favorite Cards"}]
-                  Collection [_ {:name "My Favorite Cards"}]]
-    :ok))
-
-;; Duplicate names should result in duplicate slugs...
-(expect
-  ["my_favorite_cards"
-   "my_favorite_cards"]
-  (tt/with-temp* [Collection [collection-1 {:name "My Favorite Cards"}]
-                  Collection [collection-2 {:name "My Favorite Cards"}]]
-    (map :slug [collection-1 collection-2])))
-
-
-;; things with different names that would cause the same slug SHOULD be allowed
-(expect
-  :ok
-  (tt/with-temp* [Collection [_ {:name "My Favorite Cards"}]
-                  Collection [_ {:name "my_favorite Cards"}]]
-    :ok))
-
-;; check that archiving a collection archives its cards as well
-(expect
-  true
-  (tt/with-temp* [Collection [collection]
-                  Card       [card       {:collection_id (u/get-id collection)}]]
-    (db/update! Collection (u/get-id collection)
-      :archived true)
-    (db/select-one-field :archived Card :id (u/get-id card))))
-
-;; check that unarchiving a collection unarchives its cards as well
-(expect
-  false
-  (tt/with-temp* [Collection [collection {:archived true}]
-                  Card       [card       {:collection_id (u/get-id collection), :archived true}]]
-    (db/update! Collection (u/get-id collection)
-      :archived false)
-    (db/select-one-field :archived Card :id (u/get-id card))))
-
-;; check that collections' names cannot be blank
-(expect
-  Exception
-  (tt/with-temp Collection [collection {:name ""}]
-    collection))
-
-;; check we can't change the name of a Collection to a blank string
-(expect
-  Exception
-  (tt/with-temp Collection [collection]
-    (db/update! Collection (u/get-id collection)
-      :name "")))
-
-;;; +----------------------------------------------------------------------------------------------------------------+
-;;; |                                                  Graph Tests                                                   |
-;;; +----------------------------------------------------------------------------------------------------------------+
-
-(defn- clear-graph-revisions! []
-  (db/delete! CollectionRevision))
-
-(defn- graph
-  "Fetch collection graph. `:clear-revisions?` = delete any previously existing collection revision entries so we get
-  revision = 0."
-  [& {:keys [clear-revisions?]}]
-  (when clear-revisions?
-    (clear-graph-revisions!))
-  ;; force lazy creation of the three magic groups as needed
-  (group/all-users)
-  (group/admin)
-  (group/metabot)
-  ;; now fetch the graph
-  (collection/graph))
-
-;; Check that the basic graph works
-(expect
-  {:revision 0
-   :groups   {(u/get-id (group/all-users)) {:root :none}
-              (u/get-id (group/metabot))   {:root :none}
-              (u/get-id (group/admin))     {:root :write}}}
-  (tu/with-non-admin-groups-no-root-collection-perms
-    (graph :clear-revisions? true)))
-
-;; Creating a new Collection shouldn't give perms to anyone but admins
-(expect
-  {:revision 0
-   :groups   {(u/get-id (group/all-users)) {:root :none,  :COLLECTION :none}
-              (u/get-id (group/metabot))   {:root :none,  :COLLECTION :none}
-              (u/get-id (group/admin))     {:root :write, :COLLECTION :write}}}
-  (tu/with-non-admin-groups-no-root-collection-perms
-    (tt/with-temp Collection [collection]
-      (replace-collection-ids collection (graph :clear-revisions? true)))))
-
-;; make sure read perms show up correctly
-(expect
-  {:revision 0
-   :groups   {(u/get-id (group/all-users)) {:root :none,  :COLLECTION :read}
-              (u/get-id (group/metabot))   {:root :none,  :COLLECTION :none}
-              (u/get-id (group/admin))     {:root :write, :COLLECTION :write}}}
-  (tu/with-non-admin-groups-no-root-collection-perms
-    (tt/with-temp Collection [collection]
-      (perms/grant-collection-read-permissions! (group/all-users) collection)
-      (replace-collection-ids collection (graph :clear-revisions? true)))))
-
-;; make sure we can grant write perms for new collections (!)
-(expect
-  {:revision 0
-   :groups   {(u/get-id (group/all-users)) {:root :none,  :COLLECTION :write}
-              (u/get-id (group/metabot))   {:root :none,  :COLLECTION :none}
-              (u/get-id (group/admin))     {:root :write, :COLLECTION :write}}}
-  (tu/with-non-admin-groups-no-root-collection-perms
-    (tt/with-temp Collection [collection]
-      (perms/grant-collection-readwrite-permissions! (group/all-users) collection)
-      (replace-collection-ids collection (graph :clear-revisions? true)))))
-
-;; make sure a non-magical group will show up
-(tt/expect-with-temp [PermissionsGroup [new-group]]
-  {:revision 0
-   :groups   {(u/get-id (group/all-users)) {:root :none}
-              (u/get-id (group/metabot))   {:root :none}
-              (u/get-id (group/admin))     {:root :write}
-              (u/get-id new-group)         {:root :none}}}
-  (tu/with-non-admin-groups-no-root-collection-perms
-    (graph :clear-revisions? true)))
-
-;; How abut *read* permissions for the Root Collection?
-(tt/expect-with-temp [PermissionsGroup [new-group]]
-  {:revision 0
-   :groups   {(u/get-id (group/all-users)) {:root :none}
-              (u/get-id (group/metabot))   {:root :none}
-              (u/get-id (group/admin))     {:root :write}
-              (u/get-id new-group)         {:root :read}}}
-  (tu/with-non-admin-groups-no-root-collection-perms
-    (perms/grant-collection-read-permissions! new-group collection/root-collection)
-    (graph :clear-revisions? true)))
-
-;; How about granting *write* permissions for the Root Collection?
-(tt/expect-with-temp [PermissionsGroup [new-group]]
-  {:revision 0
-   :groups   {(u/get-id (group/all-users)) {:root :none}
-              (u/get-id (group/metabot))   {:root :none}
-              (u/get-id (group/admin))     {:root :write}
-              (u/get-id new-group)         {:root :write}}}
-  (tu/with-non-admin-groups-no-root-collection-perms
-    (perms/grant-collection-readwrite-permissions! new-group collection/root-collection)
-    (graph :clear-revisions? true)))
-
-;; Can we do a no-op update?
-(expect
-  ;; revision should not have changed, because there was nothing to do...
-  {:revision 0
-   :groups   {(u/get-id (group/all-users)) {:root :none}
-              (u/get-id (group/metabot))   {:root :none}
-              (u/get-id (group/admin))     {:root :write}}}
-  ;; need to bind *current-user-id* or the Revision won't get updated
-  (do
-    (clear-graph-revisions!)
-    (tu/with-non-admin-groups-no-root-collection-perms
-      (binding [*current-user-id* (test-users/user->id :crowberto)]
-        (collection/update-graph! (graph :clear-revisions? true))
-        (graph)))))
-
-;; Can we give someone read perms via the graph?
-(expect
-  {:revision 1
-   :groups   {(u/get-id (group/all-users)) {:root :none,  :COLLECTION :read}
-              (u/get-id (group/metabot))   {:root :none,  :COLLECTION :none}
-              (u/get-id (group/admin))     {:root :write, :COLLECTION :write}}}
-  (do
-    (clear-graph-revisions!)
-    (tu/with-non-admin-groups-no-root-collection-perms
-      (tt/with-temp Collection [collection]
-        (binding [*current-user-id* (test-users/user->id :crowberto)]
-          (collection/update-graph! (assoc-in (graph :clear-revisions? true)
-                                              [:groups (u/get-id (group/all-users)) (u/get-id collection)]
-                                              :read))
-          (replace-collection-ids collection (graph)))))))
-
-;; can we give them *write* perms?
-(expect
- {:revision 1
-  :groups   {(u/get-id (group/all-users)) {:root :none,  :COLLECTION :write}
-             (u/get-id (group/metabot))   {:root :none,  :COLLECTION :none}
-             (u/get-id (group/admin))     {:root :write, :COLLECTION :write}}}
- (tu/with-non-admin-groups-no-root-collection-perms
-   (tt/with-temp Collection [collection]
-     (binding [*current-user-id* (test-users/user->id :crowberto)]
-       (collection/update-graph! (assoc-in (graph :clear-revisions? true)
-                                           [:groups (u/get-id (group/all-users)) (u/get-id collection)]
-                                           :write))
-       (replace-collection-ids collection (graph))))))
-
-;; can we *revoke* perms?
-(expect
-  {:revision 1
-   :groups   {(u/get-id (group/all-users)) {:root :none,  :COLLECTION :none}
-              (u/get-id (group/metabot))   {:root :none,  :COLLECTION :none}
-              (u/get-id (group/admin))     {:root :write, :COLLECTION :write}}}
-  (do
-    (clear-graph-revisions!)
-    (tu/with-non-admin-groups-no-root-collection-perms
-      (tt/with-temp Collection [collection]
-        (binding [*current-user-id* (test-users/user->id :crowberto)]
-          (perms/grant-collection-read-permissions! (group/all-users) collection)
-          (collection/update-graph! (assoc-in (graph :clear-revisions? true)
-                                              [:groups (u/get-id (group/all-users)) (u/get-id collection)]
-                                              :none))
-          (replace-collection-ids collection (graph)))))))
-
-;; How abut *read* permissions for the Root Collection?
-(tt/expect-with-temp [PermissionsGroup [new-group]]
-  {:revision 1
-   :groups   {(u/get-id (group/all-users)) {:root :none}
-              (u/get-id (group/metabot))   {:root :none}
-              (u/get-id (group/admin))     {:root :write}
-              (u/get-id new-group)         {:root :read}}}
-  (do
-    (clear-graph-revisions!)
-    (tu/with-non-admin-groups-no-root-collection-perms
-      (binding [*current-user-id* (test-users/user->id :crowberto)]
-        (collection/update-graph! (assoc-in (graph :clear-revisions? true)
-                                            [:groups (u/get-id new-group) :root]
-                                            :read))
-        (graph)))))
-
-;; How about granting *write* permissions for the Root Collection?
-(tt/expect-with-temp [PermissionsGroup [new-group]]
-  {:revision 1
-   :groups   {(u/get-id (group/all-users)) {:root :none}
-              (u/get-id (group/metabot))   {:root :none}
-              (u/get-id (group/admin))     {:root :write}
-              (u/get-id new-group)         {:root :write}}}
-  (do
-    (clear-graph-revisions!)
-    (tu/with-non-admin-groups-no-root-collection-perms
-      (binding [*current-user-id* (test-users/user->id :crowberto)]
-        (collection/update-graph! (assoc-in (graph :clear-revisions? true)
-                                            [:groups (u/get-id new-group) :root]
-                                            :write))
-        (graph)))))
-
-;; can we *revoke* RootCollection perms?
-(tt/expect-with-temp [PermissionsGroup [new-group]]
-  {:revision 1
-   :groups   {(u/get-id (group/all-users)) {:root :none}
-              (u/get-id (group/metabot))   {:root :none}
-              (u/get-id (group/admin))     {:root :write}
-              (u/get-id new-group)         {:root :none}}}
-  (do
-    (clear-graph-revisions!)
-    (tu/with-non-admin-groups-no-root-collection-perms
-      (binding [*current-user-id* (test-users/user->id :crowberto)]
-        (perms/grant-collection-readwrite-permissions! new-group collection/root-collection)
-        (collection/update-graph! (assoc-in (graph :clear-revisions? true)
-                                            [:groups (u/get-id new-group) :root]
-                                            :none))
-        (graph)))))
-
-;; Make sure that personal Collections *do not* appear in the Collections graph
-(expect
-  {:revision 0
-   :groups   {(u/get-id (group/all-users)) {:root :none}
-              (u/get-id (group/metabot))   {:root :none}
-              (u/get-id (group/admin))     {:root :write}}}
-  (tu/with-non-admin-groups-no-root-collection-perms
-    (graph :clear-revisions? true)))
-
-;; Make sure that if we try to be sneaky and edit a Personal Collection via the graph, an Exception is thrown
-(expect
-  Exception
-  (let [lucky-personal-collection-id (u/get-id (collection/user->personal-collection (test-users/user->id :lucky)))]
-    (collection/update-graph! (assoc-in (graph :clear-revisions? true)
-                                        [:groups (u/get-id (group/all-users)) lucky-personal-collection-id]
-                                        :read))))
-
-;; double-check that the graph is unchanged
-(expect
-  {:revision 0
-   :groups   {(u/get-id (group/all-users)) {:root :none}
-              (u/get-id (group/metabot))   {:root :none}
-              (u/get-id (group/admin))     {:root :write}}}
-  (do
-    (clear-graph-revisions!)
-    (tu/with-non-admin-groups-no-root-collection-perms
-      (u/ignore-exceptions
-        (let [lucky-personal-collection-id (u/get-id (collection/user->personal-collection (test-users/user->id :lucky)))]
-          (collection/update-graph! (assoc-in (graph :clear-revisions? true)
-                                              [:groups (u/get-id (group/all-users)) lucky-personal-collection-id]
-                                              :read))))
-      (graph))))
-
-;; Make sure descendants of Personal Collections do not come back as part of the graph either...
-(expect
-  {:revision 0
-   :groups   {(u/get-id (group/all-users)) {:root :none}
-              (u/get-id (group/metabot))   {:root :none}
-              (u/get-id (group/admin))     {:root :write}}}
-  (do
-    (clear-graph-revisions!)
-    (tu/with-non-admin-groups-no-root-collection-perms
-      (tt/with-temp Collection [_ {:location (lucky-collection-children-location)}]
-        (graph)))))
-
-;; ...and that you can't be sneaky and try to edit them either...
-(expect
-  Exception
-  (tt/with-temp Collection [collection {:location (lucky-collection-children-location)}]
-    (let [lucky-personal-collection-id (u/get-id (collection/user->personal-collection (test-users/user->id :lucky)))]
-      (collection/update-graph! (assoc-in (graph :clear-revisions? true)
-                                          [:groups
-                                           (u/get-id (group/all-users))
-                                           lucky-personal-collection-id
-                                           (u/get-id collection)]
-                                          :read)))))
+    (doseq [[input msg] {nil        "Missing color"
+                         "#ABC"     "Too short"
+                         "#BCDEFG"  "Invalid chars"
+                         "#ABCDEFF" "Too long"
+                         "ABCDEF"   "Missing hash prefix"}]
+      (testing msg
+        (is (thrown?
+             Exception
+             (db/insert! Collection {:name "My Favorite Cards", :color input})))))))
+
+(deftest with-temp-defaults-test
+  (testing "double-check that `with-temp-defaults` are working correctly for Collection"
+    (mt/with-temp Collection [collection]
+      (is (some? collection)))))
+
+(deftest duplicate-names-test
+  (testing "test that duplicate names ARE allowed"
+    (mt/with-temp* [Collection [c1 {:name "My Favorite Cards"}]
+                    Collection [c2 {:name "My Favorite Cards"}]]
+      (is (some? c1))
+      (is (some? c2))
+
+      (testing "Duplicate names should result in duplicate slugs..."
+        (testing "Collection 1"
+          (is (= "my_favorite_cards"
+                 (:slug c1))))
+        (testing "Collection 2"
+          (is (= "my_favorite_cards"
+                 (:slug c2)))))))
+
+  (testing "things with different names that would cause the same slug SHOULD be allowed"
+    (mt/with-temp* [Collection [c1 {:name "My Favorite Cards"}]
+                    Collection [c2 {:name "my_favorite Cards"}]]
+      (is (some? c1))
+      (is (some? c2))
+      (is (= (:slug c1) (:slug c2))))))
+
+(deftest archive-cards-test
+  (testing "check that archiving a Collection archives its Cards as well"
+    (mt/with-temp* [Collection [collection]
+                    Card       [card       {:collection_id (u/get-id collection)}]]
+      (db/update! Collection (u/get-id collection)
+        :archived true)
+      (is (= true
+             (db/select-one-field :archived Card :id (u/get-id card))))))
+
+  (testing "check that unarchiving a Collection unarchives its Cards as well"
+    (mt/with-temp* [Collection [collection {:archived true}]
+                    Card       [card       {:collection_id (u/get-id collection), :archived true}]]
+      (db/update! Collection (u/get-id collection)
+        :archived false)
+      (is (= false
+             (db/select-one-field :archived Card :id (u/get-id card)))))))
+
+(deftest validate-name-test
+  (testing "check that collections' names cannot be blank"
+    (is (thrown?
+         Exception
+         (mt/with-temp Collection [collection {:name ""}]
+           collection))))
+
+  (testing "check we can't change the name of a Collection to a blank string"
+    (mt/with-temp Collection [collection]
+      (is (thrown?
+           Exception
+           (db/update! Collection (u/get-id collection)
+             :name ""))))))
 
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
 ;;; |                                     Nested Collections Helper Fns & Macros                                     |
 ;;; +----------------------------------------------------------------------------------------------------------------+
 
-(defn do-with-collection-hierarchy [a-fn]
-  (tu/with-non-admin-groups-no-root-collection-perms
-    (tt/with-temp* [Collection [a {:name "A"}]
-                    Collection [b {:name "B", :location (collection/location-path a)}]
-                    Collection [c {:name "C", :location (collection/location-path a)}]
-                    Collection [d {:name "D", :location (collection/location-path a c)}]
-                    Collection [e {:name "E", :location (collection/location-path a c d)}]
-                    Collection [f {:name "F", :location (collection/location-path a c)}]
-                    Collection [g {:name "G", :location (collection/location-path a c f)}]]
+(defn do-with-collection-hierarchy [options a-fn]
+  (mt/with-non-admin-groups-no-root-collection-perms
+    (mt/with-temp* [Collection [a (merge options {:name "A"})]
+                    Collection [b (merge options {:name "B", :location (collection/location-path a)})]
+                    Collection [c (merge options {:name "C", :location (collection/location-path a)})]
+                    Collection [d (merge options {:name "D", :location (collection/location-path a c)})]
+                    Collection [e (merge options {:name "E", :location (collection/location-path a c d)})]
+                    Collection [f (merge options {:name "F", :location (collection/location-path a c)})]
+                    Collection [g (merge options {:name "G", :location (collection/location-path a c f)})]]
       (a-fn {:a a, :b b, :c c, :d d, :e e, :f f, :g g}))))
 
 (defmacro with-collection-hierarchy
@@ -413,8 +138,8 @@
      (with-collection-hierarchy [{:keys [a b c]}]
        ...)"
   {:style/indent 1}
-  [[collections-binding] & body]
-  `(do-with-collection-hierarchy (fn [~collections-binding] ~@body)))
+  [[collections-binding options] & body]
+  `(do-with-collection-hierarchy ~options (fn [~collections-binding] ~@body)))
 
 (defmacro with-current-user-perms-for-collections
   "Run `body` with the current User permissions for `collections-or-ids`.
@@ -453,120 +178,153 @@
 (deftest location-path-test
   (testing "Does our handy utility function for working with `location` paths work as expected?"
     (testing "valid input"
-      (are [expected args] (is (= expected
-                                  (apply collection/location-path args)))
-        "/1/2/3/" [1 2 3]
-        "/"       nil
-        "/1/"     [{:id 1}]
-        "/1/2/3/" [{:id 1} {:id 2} {:id 3}]
-        "/1/337/" [1 {:id 337}]))
-    (testing "invalid input"
-      (are [args] (thrown? Exception (apply collection/location-path args))
-        ["1"]
-        [nil]
-        [-1]
-        ;; shouldn't allow duplicates
-        [1 2 1]))))
-
-(expect [1 2 3]   (collection/location-path->ids "/1/2/3/"))
-(expect []        (collection/location-path->ids "/"))
-(expect [1]       (collection/location-path->ids "/1/"))
-(expect [1 337]   (collection/location-path->ids "/1/337/"))
-(expect Exception (collection/location-path->ids "/a/"))
-(expect Exception (collection/location-path->ids nil))
-(expect Exception (collection/location-path->ids "/-1/"))
-(expect Exception (collection/location-path->ids "/1/2/1/"))
-
-(expect 3         (collection/location-path->parent-id "/1/2/3/"))
-(expect nil       (collection/location-path->parent-id "/"))
-(expect 1         (collection/location-path->parent-id "/1/"))
-(expect 337       (collection/location-path->parent-id "/1/337/"))
-(expect Exception (collection/location-path->parent-id "/a/"))
-(expect Exception (collection/location-path->parent-id nil))
-(expect Exception (collection/location-path->parent-id "/-1/"))
-(expect Exception (collection/location-path->parent-id "/1/2/1/"))
-
-(expect "/1/2/3/1000/" (collection/children-location {:id 1000, :location "/1/2/3/"}))
-(expect "/1000/"       (collection/children-location {:id 1000, :location "/"}))
-(expect "/1/1000/"     (collection/children-location {:id 1000, :location "/1/"}))
-(expect "/1/337/1000/" (collection/children-location {:id 1000, :location "/1/337/"}))
-(expect Exception      (collection/children-location {:id 1000, :location "/a/"}))
-(expect Exception      (collection/children-location {:id 1000, :location nil}))
-(expect Exception      (collection/children-location {:id 1000, :location "/-1/"}))
-(expect Exception      (collection/children-location {:id nil,  :location "/1/"}))
-(expect Exception      (collection/children-location {:id "a",  :location "/1/"}))
-(expect Exception      (collection/children-location {:id 1,    :location "/1/2/"}))
-
-;; Make sure we can look at the current user's permissions set and figure out which Collections they're allowed to see
-(expect
-  #{8 9}
-  (collection/permissions-set->visible-collection-ids
-   #{"/db/1/"
-     "/db/2/native/"
-     "/db/4/schema/"
-     "/db/5/schema/PUBLIC/"
-     "/db/6/schema/PUBLIC/table/7/"
-     "/collection/8/"
-     "/collection/9/read/"}))
-
-;; If the current user has root permissions then make sure the function returns `:all`, which signifies that they are
-;; able to see all Collections
-(expect
-  :all
-  (collection/permissions-set->visible-collection-ids
-   #{"/"
-     "/db/2/native/"
-     "/collection/9/read/"}))
-
-;; for the Root Collection we should return `"root"`
-(expect
-  #{8 9 "root"}
-  (collection/permissions-set->visible-collection-ids
-   #{"/collection/8/"
-     "/collection/9/read/"
-     "/collection/root/"}))
-
-(expect
-  #{"root"}
-  (collection/permissions-set->visible-collection-ids
-   #{"/collection/root/read/"}))
-
-;; Can we calculate `effective-location-path`?
-(expect "/10/20/"    (collection/effective-location-path "/10/20/30/" #{10 20}))
-(expect "/10/30/"    (collection/effective-location-path "/10/20/30/" #{10 30}))
-(expect "/"          (collection/effective-location-path "/10/20/30/" #{}))
-(expect "/10/20/30/" (collection/effective-location-path "/10/20/30/" #{10 20 30}))
-(expect "/10/20/30/" (collection/effective-location-path "/10/20/30/" :all))
-(expect Exception    (collection/effective-location-path "/10/20/30/" nil))
-(expect Exception    (collection/effective-location-path "/10/20/30/" [20]))
-(expect Exception    (collection/effective-location-path nil #{}))
-(expect Exception    (collection/effective-location-path [10 20] #{}))
-
-;; Does the function also work if we call the single-arity version that powers hydration?
-(expect
-  "/10/20/"
-  (binding [*current-user-permissions-set* (atom #{"/collection/10/" "/collection/20/read/"})]
-    (collection/effective-location-path {:location "/10/20/30/"})))
-
-(expect
-  "/10/30/"
-  (binding [*current-user-permissions-set* (atom #{"/collection/10/read/" "/collection/30/read/"})]
-    (collection/effective-location-path {:location "/10/20/30/"})))
-
-(expect
-  "/"
-  (binding [*current-user-permissions-set* (atom #{})]
-    (collection/effective-location-path {:location "/10/20/30/"})))
-
-(expect
-  "/10/20/30/"
-  (binding [*current-user-permissions-set* (atom #{"/collection/10/" "/collection/20/read/" "/collection/30/read/"})]
-    (collection/effective-location-path {:location "/10/20/30/"})))
+      (doseq [[args expected] {[1 2 3]                   "/1/2/3/"
+                               nil                       "/"
+                               [{:id 1}]                 "/1/"
+                               [{:id 1} {:id 2} {:id 3}] "/1/2/3/"
+                               [1 {:id 337}]             "/1/337/"}]
+        (testing (pr-str (cons 'location-path args))
+          (is (= expected
+                 (apply collection/location-path args))))))
 
-(expect
-  "/10/20/30/"
-  (binding [*current-user-permissions-set* (atom #{"/"})]
-    (collection/effective-location-path {:location "/10/20/30/"})))
+    (testing "invalid input"
+      (doseq [args [["1"]
+                    [nil]
+                    [-1]
+                    ;; shouldn't allow duplicates
+                    [1 2 1]]]
+        (testing (pr-str (cons 'location-path args))
+          (is (thrown?
+               Exception
+               (apply collection/location-path args))))))))
+
+(deftest location-path-ids-test
+  (testing "valid input"
+    (doseq [[path expected] {"/1/2/3/" [1 2 3]
+                             "/"       []
+                             "/1/"     [1]
+                             "/1/337/" [1 337]}]
+      (testing (pr-str (list 'location-path->ids path))
+        (is (= expected
+               (collection/location-path->ids path))))
+
+      (testing (pr-str (list 'location-path->parent-id path))
+        (is (= (last expected)
+               (collection/location-path->parent-id path))))))
+
+  (testing "invalid input"
+    (doseq [path ["/a/"
+                  nil
+                  "/-1/"
+                  "/1/2/1/"]]
+      (testing (pr-str (list 'location-path->ids path))
+        (is (thrown?
+             Exception
+             (collection/location-path->ids path))))
+
+      (testing (pr-str (list 'location-path->parent-id path))
+        (is (thrown?
+             Exception
+             (collection/location-path->parent-id path)))))))
+
+(deftest children-location-test
+  (testing "valid input"
+    (doseq [[collection expected] {{:id 1000, :location "/1/2/3/"} "/1/2/3/1000/"
+                                   {:id 1000, :location "/"}       "/1000/"
+                                   {:id 1000, :location "/1/"}     "/1/1000/"
+                                   {:id 1000, :location "/1/337/"} "/1/337/1000/"}]
+      (testing (pr-str (list 'children-location collection))
+        (is (= expected
+               (collection/children-location collection))))))
+
+  (testing "invalid input"
+    (doseq [collection [{:id 1000, :location "/a/"}
+                        {:id 1000, :location nil}
+                        {:id 1000, :location "/-1/"}
+                        {:id nil, :location "/1/"}
+                        {:id "a", :location "/1/"}
+                        {:id 1, :location "/1/2/"}]]
+      (testing (pr-str (list 'children-location collection))
+        (is (thrown?
+             Exception
+             (collection/children-location collection)))))))
+
+(deftest permissions-set->visible-collection-ids-test
+  (testing "Make sure we can look at the current user's permissions set and figure out which Collections they're allowed to see"
+    (is (= #{8 9}
+           (collection/permissions-set->visible-collection-ids
+            #{"/db/1/"
+              "/db/2/native/"
+              "/db/4/schema/"
+              "/db/5/schema/PUBLIC/"
+              "/db/6/schema/PUBLIC/table/7/"
+              "/collection/8/"
+              "/collection/9/read/"}))))
+
+  (testing "If the current user has root permissions then make sure the function returns `:all`, which signifies that they are able to see all Collections"
+    (is (= :all
+           (collection/permissions-set->visible-collection-ids
+            #{"/"
+              "/db/2/native/"
+              "/collection/9/read/"}))))
+
+  (testing "for the Root Collection we should return `root`"
+    (is (= #{8 9 "root"}
+           (collection/permissions-set->visible-collection-ids
+            #{"/collection/8/"
+              "/collection/9/read/"
+              "/collection/root/"})))
+
+    (is (= #{"root"}
+           (collection/permissions-set->visible-collection-ids
+            #{"/collection/root/read/"})))))
+
+(deftest effective-location-path-test
+  (testing "valid input"
+    (doseq [[args expected] {["/10/20/30/" #{10 20}]    "/10/20/"
+                             ["/10/20/30/" #{10 30}]    "/10/30/"
+                             ["/10/20/30/" #{}]         "/"
+                             ["/10/20/30/" #{10 20 30}] "/10/20/30/"
+                             ["/10/20/30/" :all]        "/10/20/30/"}]
+      (testing (pr-str (cons 'effective-location-path args))
+        (is (= expected
+               (apply collection/effective-location-path args))))))
+
+  (testing "invalid input"
+    (doseq [args [["/10/20/30/" nil]
+                  ["/10/20/30/" [20]]
+                  [nil #{}]
+                  [[10 20] #{}]]]
+      (testing (pr-str (cons 'effective-location-path args))
+        (is (thrown?
+             Exception
+             (apply collection/effective-location-path args))))))
+
+  (testing "Does the function also work if we call the single-arity version that powers hydration?"
+    (testing "mix of full and read perms"
+      (binding [*current-user-permissions-set* (atom #{"/collection/10/" "/collection/20/read/"})]
+        (is (= "/10/20/"
+               (collection/effective-location-path {:location "/10/20/30/"})))))
+
+    (testing "missing some perms"
+      (binding [*current-user-permissions-set* (atom #{"/collection/10/read/" "/collection/30/read/"})]
+        (is (= "/10/30/"
+               (collection/effective-location-path {:location "/10/20/30/"})))))
+
+    (testing "no perms"
+      (binding [*current-user-permissions-set* (atom #{})]
+        (is (= "/"
+               (collection/effective-location-path {:location "/10/20/30/"})))))
+
+    (testing "read perms for all"
+      (binding [*current-user-permissions-set* (atom #{"/collection/10/" "/collection/20/read/" "/collection/30/read/"})]
+        (is (= "/10/20/30/"
+               (collection/effective-location-path {:location "/10/20/30/"})))))
+
+    (testing "root perms"
+      (binding [*current-user-permissions-set* (atom #{"/"})]
+        (is (= "/10/20/30/"
+               (collection/effective-location-path {:location "/10/20/30/"})))))))
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
 ;;; |                                Nested Collections: CRUD Constraints & Behavior                                 |
@@ -574,13 +332,13 @@
 
 ;; Can we INSERT a Collection with a valid path?
 (defn- insert-collection-with-location! [location]
-  (tu/with-model-cleanup [Collection]
-    (-> (db/insert! Collection :name (tu/random-name), :color "#ABCDEF", :location location)
+  (mt/with-model-cleanup [Collection]
+    (-> (db/insert! Collection :name (mt/random-name), :color "#ABCDEF", :location location)
         :location
         (= location))))
 
 (expect
-  (tt/with-temp Collection [parent]
+  (mt/with-temp Collection [parent]
     (insert-collection-with-location! (collection/location-path parent))))
 
 ;; Make sure we can't INSERT a Collection with an invalid path
@@ -599,20 +357,20 @@
 
 ;; MAae sure we can UPDATE a Collection and give it a new, *valid* location
 (expect
-  (tt/with-temp* [Collection [collection-1]
+  (mt/with-temp* [Collection [collection-1]
                   Collection [collection-2]]
     (db/update! Collection (u/get-id collection-1) :location (collection/location-path collection-2))))
 
 ;; Make sure we can't UPDATE a Collection to give it an valid path
 (expect
   Exception
-  (tt/with-temp Collection [collection]
+  (mt/with-temp Collection [collection]
     (db/update! Collection (u/get-id collection) :location "/a/")))
 
 ;; Make sure we can't UPDATE a Collection to give it a non-existent ancestors
 (expect
   Exception
-  (tt/with-temp Collection [collection]
+  (mt/with-temp Collection [collection]
     (db/update! Collection (u/get-id collection) :location (collection/location-path (nonexistent-collection-id)))))
 
 
@@ -804,12 +562,13 @@
   (with-collection-hierarchy [{:keys [a b c d e f g]}]
     (descendants collection/root-collection)))
 
-;; double-check that descendant-ids is working right too
-(tt/expect-with-temp [Collection [a]
-                      Collection [b {:location (collection/children-location a)}]
-                      Collection [c {:location (collection/children-location b)}]]
-  #{(u/get-id b) (u/get-id c)}
-  (#'collection/descendant-ids a))
+(deftest descendant-ids-test
+  (testing "double-check that descendant-ids is working right too"
+    (mt/with-temp* [Collection [a]
+                    Collection [b {:location (collection/children-location a)}]
+                    Collection [c {:location (collection/children-location b)}]]
+      (is (= #{(u/get-id b) (u/get-id c)}
+             (#'collection/descendant-ids a))))))
 
 
 ;;; ----------------------------------------------- Effective Children -----------------------------------------------
@@ -944,173 +703,166 @@
 
 ;;; ---------------------------------------------- Perms for Archiving -----------------------------------------------
 
-;; To Archive A, you should need *write* perms for A and all of its descendants, and also the Root Collection...
-(expect
-  #{"/collection/root/"
-    "/collection/A/"
-    "/collection/B/"
-    "/collection/C/"
-    "/collection/D/"
-    "/collection/E/"
-    "/collection/F/"
-    "/collection/G/"}
-  (with-collection-hierarchy [{:keys [a], :as collections}]
-    (->> (collection/perms-for-archiving a)
-         (perms-path-ids->names collections))))
-
-;; Now let's move down a level. To archive B, you should need permissions for A and B, since B doesn't
-;; have any descendants
-(expect
-  #{"/collection/A/"
-    "/collection/B/"}
-  (with-collection-hierarchy [{:keys [b], :as collections}]
-    (->> (collection/perms-for-archiving b)
-         (perms-path-ids->names collections))))
-
-;; but for C, you should need perms for A (parent); C; and D, E, F, and G (descendants)
-(expect
-  #{"/collection/A/"
-    "/collection/C/"
-    "/collection/D/"
-    "/collection/E/"
-    "/collection/F/"
-    "/collection/G/"}
-  (with-collection-hierarchy [{:keys [c], :as collections}]
-    (->> (collection/perms-for-archiving c)
-         (perms-path-ids->names collections))))
-
-;; For D you should need C (parent), D, and E (descendant)
-(expect
-  #{"/collection/C/"
-    "/collection/D/"
-    "/collection/E/"}
-  (with-collection-hierarchy [{:keys [d], :as collections}]
-    (->> (collection/perms-for-archiving d)
-         (perms-path-ids->names collections))))
-
-;; If you try to calculate permissions to archive the Root Collection, throw an Exception! Because you can't do
-;; that...
-(expect
-  Exception
-  (collection/perms-for-archiving collection/root-collection))
-
-;; Let's make sure we get an Exception when we try to archive a Personal Collection
-(expect
-  Exception
-  (collection/perms-for-archiving (collection/user->personal-collection (test-users/fetch-user :lucky))))
-
-;; also you should get an Exception if you try to pull a fast one on use and pass in some sort of invalid input...
-(expect Exception (collection/perms-for-archiving nil))
-(expect Exception (collection/perms-for-archiving {}))
-(expect Exception (collection/perms-for-archiving 1))
+(deftest perms-for-archiving-test
+  (with-collection-hierarchy [{:keys [a b c d], :as collections}]
+    (testing "To Archive A, you should need *write* perms for A and all of its descendants, and also the Root Collection..."
+      (is (= #{"/collection/root/"
+               "/collection/A/"
+               "/collection/B/"
+               "/collection/C/"
+               "/collection/D/"
+               "/collection/E/"
+               "/collection/F/"
+               "/collection/G/"}
+             (->> (collection/perms-for-archiving a)
+                  (perms-path-ids->names collections)))))
+
+    (testing (str "Now let's move down a level. To archive B, you should need permissions for A and B, since B doesn't "
+                  "have any descendants")
+      (is (= #{"/collection/A/"
+               "/collection/B/"}
+             (->> (collection/perms-for-archiving b)
+                  (perms-path-ids->names collections)))))
+
+    (testing "but for C, you should need perms for A (parent); C; and D, E, F, and G (descendants)"
+      (is (= #{"/collection/A/"
+               "/collection/C/"
+               "/collection/D/"
+               "/collection/E/"
+               "/collection/F/"
+               "/collection/G/"}
+             (->> (collection/perms-for-archiving c)
+                  (perms-path-ids->names collections)))))
+
+    (testing "For D you should need C (parent), D, and E (descendant)"
+      (is (= #{"/collection/C/"
+               "/collection/D/"
+               "/collection/E/"}
+             (->> (collection/perms-for-archiving d)
+                  (perms-path-ids->names collections)))))))
+
+(deftest perms-for-archiving-exceptions-test
+  (testing "If you try to calculate permissions to archive the Root Collection, throw an Exception!"
+    (is (thrown?
+         Exception
+         (collection/perms-for-archiving collection/root-collection))))
+
+  (testing "Let's make sure we get an Exception when we try to archive a Personal Collection"
+    (is (thrown?
+         Exception
+         (collection/perms-for-archiving (collection/user->personal-collection (mt/fetch-user :lucky))))))
+
+  (testing "invalid input"
+    (doseq [input [nil {} 1]]
+      (testing (format "input = %s" (pr-str input))
+        (is (thrown?
+             Exception
+             (collection/perms-for-archiving input)))))))
 
 
 ;;; ------------------------------------------------ Perms for Moving ------------------------------------------------
 
 ;; `*` marks the things that require permissions in charts below!
 
-;; If we want to move B into C, we should need perms for A, B, and C. B because it is being moved; C we are moving
-;; something into it, A because we are moving something out of it
-;;
-;;    +-> B                              +-> B*
-;;    |                                  |
-;; A -+-> C -+-> D -> E  ===>  A* -> C* -+-> D -> E
-;;           |                           |
-;;           +-> F -> G                  +-> F -> G
-
-(expect
-  #{"/collection/A/"
-    "/collection/B/"
-    "/collection/C/"}
-  (with-collection-hierarchy [{:keys [b c], :as collections}]
-    (->> (collection/perms-for-moving b c)
-         (perms-path-ids->names collections))))
-
-;; Ok, now let's try moving something with descendants. If we move C into B, we need perms for C and all its
-;; descendants, and B, since it's the new parent; and A, the old parent
-;;
-;;    +-> B
-;;    |
-;; A -+-> C -+-> D -> E  ===>  A* -> B* -> C* -+-> D* -> E*
-;;           |                                 |
-;;           +-> F -> G                        +-> F* -> G*
-(expect
-  #{"/collection/A/"
-    "/collection/B/"
-    "/collection/C/"
-    "/collection/D/"
-    "/collection/E/"
-    "/collection/F/"
-    "/collection/G/"}
+(deftest perms-for-moving-test
   (with-collection-hierarchy [{:keys [b c], :as collections}]
-    (->> (collection/perms-for-moving c b)
-         (perms-path-ids->names collections))))
-
-;; Ok, now how about moving B into the Root Collection?
-;;
-;;    +-> B                    B* [and Root*]
-;;    |
-;; A -+-> C -+-> D -> E  ===>  A* -> C -+-> D -> E
-;;           |                          |
-;;           +-> F -> G                 +-> F -> G
-(expect
-  #{"/collection/root/"
-    "/collection/A/"
-    "/collection/B/"}
-  (with-collection-hierarchy [{:keys [b], :as collections}]
-    (->> (collection/perms-for-moving b collection/root-collection)
-         (perms-path-ids->names collections))))
-
-;; How about moving C into the Root Collection?
-;;
-;;    +-> B                    A* -> B
-;;    |
-;; A -+-> C -+-> D -> E  ===>  C* -+-> D* -> E* [and Root*]
-;;           |                     |
-;;           +-> F -> G            +-> F* -> G*
-(expect
-  #{"/collection/root/"
-    "/collection/A/"
-    "/collection/C/"
-    "/collection/D/"
-    "/collection/E/"
-    "/collection/F/"
-    "/collection/G/"}
-  (with-collection-hierarchy [{:keys [c], :as collections}]
-    (->> (collection/perms-for-moving c collection/root-collection)
-         (perms-path-ids->names collections))))
-
-;; If you try to calculate permissions to move or archive the Root Collection, throw an Exception! Because you can't
-;; do that...
-(expect
-  Exception
-  (with-collection-hierarchy [{:keys [a]}]
-    (collection/perms-for-moving collection/root-collection a)))
-
-;; You should also see an Exception if you try to move a Collection into itself or into one its descendants...
-(expect
-  Exception
-  (with-collection-hierarchy [{:keys [b]}]
-    (collection/perms-for-moving b b)))
-
-(expect
-  Exception
-  (with-collection-hierarchy [{:keys [a b]}]
-    (collection/perms-for-moving a b)))
-
-;; Let's make sure we get an Exception when we try to *move* a Collection
-(expect
-  Exception
-  (with-collection-hierarchy [{:keys [a]}]
-    (collection/perms-for-moving (collection/user->personal-collection (test-users/fetch-user :lucky)) a)))
-
-;; also you should get an Exception if you try to pull a fast one on use and pass in some sort of invalid input...
-(expect Exception (collection/perms-for-moving {:location "/"} nil))
-(expect Exception (collection/perms-for-moving {:location "/"} {}))
-(expect Exception (collection/perms-for-moving {:location "/"} 1))
-(expect Exception (collection/perms-for-moving nil {:location "/"}))
-(expect Exception (collection/perms-for-moving {}  {:location "/"}))
-(expect Exception (collection/perms-for-moving 1   {:location "/"}))
+    (testing "If we want to move B into C, we should need perms for A, B, and C."
+      ;; B because it is being moved; C we are moving
+      ;; something into it, A because we are moving something out of it
+      ;;
+      ;;    +-> B                              +-> B*
+      ;;    |                                  |
+      ;; A -+-> C -+-> D -> E  ===>  A* -> C* -+-> D -> E
+      ;;           |                           |
+      ;;           +-> F -> G                  +-> F -> G
+
+      (is (= #{"/collection/A/"
+               "/collection/B/"
+               "/collection/C/"}
+             (->> (collection/perms-for-moving b c)
+                  (perms-path-ids->names collections)))))
+
+    (testing "Ok, now let's try moving something with descendants."
+      ;; If we move C into B, we need perms for C and all its
+      ;; descendants, and B, since it's the new parent; and A, the old parent
+      ;;
+      ;;    +-> B
+      ;;    |
+      ;; A -+-> C -+-> D -> E  ===>  A* -> B* -> C* -+-> D* -> E*
+      ;;           |                                 |
+      ;;           +-> F -> G                        +-> F* -> G*
+      (is (= #{"/collection/A/"
+               "/collection/B/"
+               "/collection/C/"
+               "/collection/D/"
+               "/collection/E/"
+               "/collection/F/"
+               "/collection/G/"}
+             (->> (collection/perms-for-moving c b)
+                  (perms-path-ids->names collections)))))
+
+    (testing "Ok, now how about moving B into the Root Collection?"
+      ;;    +-> B                    B* [and Root*]
+      ;;    |
+      ;; A -+-> C -+-> D -> E  ===>  A* -> C -+-> D -> E
+      ;;           |                          |
+      ;;           +-> F -> G                 +-> F -> G
+      (is (= #{"/collection/root/"
+               "/collection/A/"
+               "/collection/B/"}
+             (->> (collection/perms-for-moving b collection/root-collection)
+                  (perms-path-ids->names collections)))))
+
+    (testing "How about moving C into the Root Collection?"
+      ;;    +-> B                    A* -> B
+      ;;    |
+      ;; A -+-> C -+-> D -> E  ===>  C* -+-> D* -> E* [and Root*]
+      ;;           |                     |
+      ;;           +-> F -> G            +-> F* -> G*
+      (is (= #{"/collection/root/"
+               "/collection/A/"
+               "/collection/C/"
+               "/collection/D/"
+               "/collection/E/"
+               "/collection/F/"
+               "/collection/G/"}
+             (->> (collection/perms-for-moving c collection/root-collection)
+                  (perms-path-ids->names collections)))))))
+
+(deftest perms-for-moving-exceptions-test
+  (with-collection-hierarchy [{:keys [a b], :as collections}]
+    (testing "If you try to calculate permissions to move or archive the Root Collection, throw an Exception!"
+      (is (thrown?
+           Exception
+           (collection/perms-for-moving collection/root-collection a))))
+
+    (testing "You should also see an Exception if you try to move a Collection into itself or into one its descendants..."
+      (testing "B -> B"
+        (is (thrown?
+             Exception
+             (collection/perms-for-moving b b))))
+
+      (testing "A -> B"
+        (is (thrown?
+             Exception
+             (collection/perms-for-moving a b)))))
+
+    (testing "Let's make sure we get an Exception when we try to *move* a Personal Collection"
+      (is (thrown?
+           Exception
+           (collection/perms-for-moving (collection/user->personal-collection (mt/fetch-user :lucky)) a)))))
+
+  (testing "invalid input"
+    (doseq [[collection new-parent] [[{:location "/"} nil]
+                                     [{:location "/"} {}]
+                                     [{:location "/"} 1]
+                                     [nil {:location "/"}]
+                                     [{}  {:location "/"}]
+                                     [1   {:location "/"}]]]
+      (testing (pr-str (list 'perms-for-moving collection new-parent))
+        (is (thrown?
+             Exception
+             (collection/perms-for-moving collection new-parent)))))))
 
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
@@ -1226,179 +978,129 @@
   (with-collection-hierarchy [collections]
     (collection-locations (vals collections) :archived false)))
 
-;; Test that we can archive a Collection with no descendants!
-;;
-;;    +-> B                        +-> B
-;;    |                            |
-;; A -+-> C -+-> D -> E   ===>  A -+-> C -+-> D
-;;           |                            |
-;;           +-> F -> G                   +-> F -> G
-(expect
-  {"A" {"B" {}
-        "C" {"D" {}
-             "F" {"G" {}}}}}
-  (with-collection-hierarchy [{:keys [e], :as collections}]
-    (db/update! Collection (u/get-id e) :archived true)
-    (collection-locations (vals collections) :archived false)))
-
-;; Test that we can archive a Collection *with* descendants
-;;
-;;    +-> B                        +-> B
-;;    |                            |
-;; A -+-> C -+-> D -> E   ===>  A -+
-;;           |
-;;           +-> F -> G
-(expect
-  {"A" {"B" {}}}
-  (with-collection-hierarchy [{:keys [c], :as collections}]
-    (db/update! Collection (u/get-id c) :archived true)
-    (collection-locations (vals collections) :archived false)))
-
-;; Test that we can unarchive a Collection with no descendants
-;;
-;;    +-> B                        +-> B
-;;    |                            |
-;; A -+-> C -+-> D        ===>  A -+-> C -+-> D -> E
-;;           |                            |
-;;           +-> F -> G                   +-> F -> G
-(expect
-  {"A" {"B" {}
-        "C" {"D" {"E" {}}
-             "F" {"G" {}}}}}
-  (with-collection-hierarchy [{:keys [e], :as collections}]
-    (db/update! Collection (u/get-id e) :archived true)
-    (db/update! Collection (u/get-id e) :archived false)
-    (collection-locations (vals collections) :archived false)))
-
-
-;; Test that we can unarchive a Collection *with* descendants
-;;
-;;    +-> B                        +-> B
-;;    |                            |
-;; A -+                   ===>  A -+-> C -+-> D -> E
-;;                                        |
-;;                                        +-> F -> G
-(expect
-  {"A" {"B" {}
-        "C" {"D" {"E" {}}
-             "F" {"G" {}}}}}
-  (with-collection-hierarchy [{:keys [c], :as collections}]
-    (db/update! Collection (u/get-id c) :archived true)
-    (db/update! Collection (u/get-id c) :archived false)
-    (collection-locations (vals collections) :archived false)))
-
-;; Test that archiving applies to Cards
-;; Card is in E; archiving E should cause Card to be archived
-(expect
-  (with-collection-hierarchy [{:keys [e], :as collections}]
-    (tt/with-temp Card [card {:collection_id (u/get-id e)}]
+(deftest nested-collections-archiving-test
+  (testing "Test that we can archive a Collection with no descendants!"
+    ;;    +-> B                        +-> B
+    ;;    |                            |
+    ;; A -+-> C -+-> D -> E   ===>  A -+-> C -+-> D
+    ;;           |                            |
+    ;;           +-> F -> G                   +-> F -> G
+    (with-collection-hierarchy [{:keys [e], :as collections}]
       (db/update! Collection (u/get-id e) :archived true)
-      (db/select-one-field :archived Card :id (u/get-id card)))))
-
-;; Test that archiving applies to Cards belonging to descendant Collections
-;; Card is in E, a descendant of C; archiving C should cause Card to be archived
-(expect
-  (with-collection-hierarchy [{:keys [c e], :as collections}]
-    (tt/with-temp Card [card {:collection_id (u/get-id e)}]
+      (is (= {"A" {"B" {}
+                   "C" {"D" {}
+                        "F" {"G" {}}}}}
+             (collection-locations (vals collections) :archived false)))))
+
+  (testing "Test that we can archive a Collection *with* descendants"
+    ;;    +-> B                        +-> B
+    ;;    |                            |
+    ;; A -+-> C -+-> D -> E   ===>  A -+
+    ;;           |
+    ;;           +-> F -> G
+    (with-collection-hierarchy [{:keys [c], :as collections}]
       (db/update! Collection (u/get-id c) :archived true)
-      (db/select-one-field :archived Card :id (u/get-id card)))))
-
-;; Test that archiving applies to Dashboards
-;; Dashboard is in E; archiving E should cause Dashboard to be archived
-(expect
-  (with-collection-hierarchy [{:keys [e], :as collections}]
-    (tt/with-temp Dashboard [dashboard {:collection_id (u/get-id e)}]
+      (is (= {"A" {"B" {}}}
+             (collection-locations (vals collections) :archived false))))))
+
+(deftest nested-collection-unarchiving-test
+  (testing "Test that we can unarchive a Collection with no descendants"
+    ;;    +-> B                        +-> B
+    ;;    |                            |
+    ;; A -+-> C -+-> D        ===>  A -+-> C -+-> D -> E
+    ;;           |                            |
+    ;;           +-> F -> G                   +-> F -> G
+    (with-collection-hierarchy [{:keys [e], :as collections}]
       (db/update! Collection (u/get-id e) :archived true)
-      (db/select-one-field :archived Dashboard :id (u/get-id dashboard)))))
-
-;; Test that archiving applies to Dashboards belonging to descendant Collections
-;; Dashboard is in E, a descendant of C; archiving C should cause Dashboard to be archived
-(expect
-  (with-collection-hierarchy [{:keys [c e], :as collections}]
-    (tt/with-temp Dashboard [dashboard {:collection_id (u/get-id e)}]
-      (db/update! Collection (u/get-id c) :archived true)
-      (db/select-one-field :archived Dashboard :id (u/get-id dashboard)))))
-
-;; Test that archiving Collections applies to Pulses
-;; Pulse is in E; archiving E should cause Pulse to be archived
-(expect
-  (with-collection-hierarchy [{:keys [e], :as collections}]
-    (tt/with-temp Pulse [pulse {:collection_id (u/get-id e)}]
-      (db/update! Collection (u/get-id e) :archived true)
-      (db/select-one-field :archived Pulse :id (u/get-id pulse)))))
-
-;; Test that archiving works on Pulses belonging to descendant Collections
-;; Pulse is in E, a descendant of C; archiving C should cause Pulse to be archived
-(expect
-  (with-collection-hierarchy [{:keys [c e], :as collections}]
-    (tt/with-temp Pulse [pulse {:collection_id (u/get-id e)}]
-      (db/update! Collection (u/get-id c) :archived true)
-      (db/select-one-field :archived Pulse :id (u/get-id pulse)))))
-
-;; Test that unarchiving applies to Cards
-;; Card is in E; unarchiving E should cause Card to be unarchived
-(expect
-  false
-  (with-collection-hierarchy [{:keys [e], :as collections}]
-    (db/update! Collection (u/get-id e) :archived true)
-    (tt/with-temp Card [card {:collection_id (u/get-id e), :archived true}]
       (db/update! Collection (u/get-id e) :archived false)
-      (db/select-one-field :archived Card :id (u/get-id card)))))
-
-;; Test that unarchiving applies to Cards belonging to descendant Collections
-;; Card is in E, a descendant of C; unarchiving C should cause Card to be unarchived
-(expect
-  false
-  (with-collection-hierarchy [{:keys [c e], :as collections}]
-    (db/update! Collection (u/get-id c) :archived true)
-    (tt/with-temp Card [card {:collection_id (u/get-id e), :archived true}]
-      (db/update! Collection (u/get-id c) :archived false)
-      (db/select-one-field :archived Card :id (u/get-id card)))))
-
-;; Test that unarchiving applies to Dashboards
-;; Dashboard is in E; unarchiving E should cause Dashboard to be unarchived
-(expect
-  false
-  (with-collection-hierarchy [{:keys [e], :as collections}]
-    (db/update! Collection (u/get-id e) :archived true)
-    (tt/with-temp Dashboard [dashboard {:collection_id (u/get-id e), :archived true}]
-      (db/update! Collection (u/get-id e) :archived false)
-      (db/select-one-field :archived Dashboard :id (u/get-id dashboard)))))
-
-;; Test that unarchiving applies to Dashboards belonging to descendant Collections
-;; Dashboard is in E, a descendant of C; unarchiving C should cause Dashboard to be unarchived
-(expect
-  false
-  (with-collection-hierarchy [{:keys [c e], :as collections}]
-    (db/update! Collection (u/get-id c) :archived true)
-    (tt/with-temp Dashboard [dashboard {:collection_id (u/get-id e), :archived true}]
+      (is (= {"A" {"B" {}
+                   "C" {"D" {"E" {}}
+                        "F" {"G" {}}}}}
+             (collection-locations (vals collections) :archived false)))))
+
+  (testing "Test that we can unarchive a Collection *with* descendants"
+    ;;    +-> B                        +-> B
+    ;;    |                            |
+    ;; A -+                   ===>  A -+-> C -+-> D -> E
+    ;;                                        |
+    ;;                                        +-> F -> G
+    (with-collection-hierarchy [{:keys [c], :as collections}]
+      (db/update! Collection (u/get-id c) :archived true)
       (db/update! Collection (u/get-id c) :archived false)
-      (db/select-one-field :archived Dashboard :id (u/get-id dashboard)))))
-
-;; Test that we cannot archive a Collection at the same time we are moving it
-(expect
-  Exception
-  (with-collection-hierarchy [{:keys [c], :as collections}]
-    (db/update! Collection (u/get-id c), :archived true, :location "/")))
-
-;; Test that we cannot unarchive a Collection at the same time we are moving it
-(expect
-  Exception
-  (with-collection-hierarchy [{:keys [c], :as collections}]
-    (db/update! Collection (u/get-id c), :archived true)
-    (db/update! Collection (u/get-id c), :archived false, :location "/")))
-
-;; Passing in a value of archived that is the same as the value in the DB shouldn't affect anything however!
-(expect
-  (with-collection-hierarchy [{:keys [c], :as collections}]
-    (db/update! Collection (u/get-id c), :archived false, :location "/")))
-
-;; Check that attempting to unarchive a Card that's not archived doesn't affect arcived descendants
-(expect
-  (with-collection-hierarchy [{:keys [c e], :as collections}]
-    (db/update! Collection (u/get-id e), :archived true)
-    (db/update! Collection (u/get-id c), :archived false)
-    (db/select-one-field :archived Collection :id (u/get-id e))))
+      (is (= {"A" {"B" {}
+                   "C" {"D" {"E" {}}
+                        "F" {"G" {}}}}}
+             (collection-locations (vals collections) :archived false))))))
+
+(deftest nested-collection-archiving-objects-test
+  (doseq [model [Card Dashboard NativeQuerySnippet Pulse]]
+    (testing (format "Test that archiving applies to %ss" (name model))
+      ;; object is in E; archiving E should cause object to be archived
+      (with-collection-hierarchy [{:keys [e], :as collections} (when (= model NativeQuerySnippet)
+                                                                 {:namespace "snippets"})]
+        (mt/with-temp model [object {:collection_id (u/get-id e)}]
+          (db/update! Collection (u/get-id e) :archived true)
+          (is (= true
+                 (db/select-one-field :archived model :id (u/get-id object)))))))
+
+    (testing (format "Test that archiving applies to %ss belonging to descendant Collections" (name model))
+      ;; object is in E, a descendant of C; archiving C should cause object to be archived
+      (with-collection-hierarchy [{:keys [c e], :as collections} (when (= model NativeQuerySnippet)
+                                                                   {:namespace "snippets"})]
+        (mt/with-temp model [object {:collection_id (u/get-id e)}]
+          (db/update! Collection (u/get-id c) :archived true)
+          (is (= true
+                 (db/select-one-field :archived model :id (u/get-id object)))))))))
+
+(deftest nested-collection-unarchiving-objects-test
+  (doseq [model [Card Dashboard NativeQuerySnippet Pulse]]
+    (testing (format "Test that unarchiving applies to %ss" (name model))
+      ;; object is in E; unarchiving E should cause object to be unarchived
+      (with-collection-hierarchy [{:keys [e], :as collections} (when (= model NativeQuerySnippet)
+                                                                 {:namespace "snippets"})]
+        (db/update! Collection (u/get-id e) :archived true)
+        (mt/with-temp model [object {:collection_id (u/get-id e), :archived true}]
+          (db/update! Collection (u/get-id e) :archived false)
+          (is (= false
+                 (db/select-one-field :archived model :id (u/get-id object)))))))
+
+    (testing (format "Test that unarchiving applies to %ss belonging to descendant Collections" (name model))
+      ;; object is in E, a descendant of C; unarchiving C should cause object to be unarchived
+      (with-collection-hierarchy [{:keys [c e], :as collections} (when (= model NativeQuerySnippet)
+                                                                   {:namespace "snippets"})]
+        (db/update! Collection (u/get-id c) :archived true)
+        (mt/with-temp model [object {:collection_id (u/get-id e), :archived true}]
+          (db/update! Collection (u/get-id c) :archived false)
+          (is (= false
+                 (db/select-one-field :archived model :id (u/get-id object)))))))))
+
+(deftest archive-while-moving-test
+  (testing "Test that we cannot archive a Collection at the same time we are moving it"
+    (with-collection-hierarchy [{:keys [c], :as collections}]
+      (is (thrown?
+           Exception
+           (db/update! Collection (u/get-id c), :archived true, :location "/")))))
+
+  (testing "Test that we cannot unarchive a Collection at the same time we are moving it"
+    (with-collection-hierarchy [{:keys [c], :as collections}]
+      (db/update! Collection (u/get-id c), :archived true)
+      (is (thrown?
+           Exception
+           (db/update! Collection (u/get-id c), :archived false, :location "/")))))
+
+  (testing "Passing in a value of archived that is the same as the value in the DB shouldn't affect anything however!"
+    (with-collection-hierarchy [{:keys [c], :as collections}]
+      (db/update! Collection (u/get-id c), :archived false, :location "/")
+      (is (= "/"
+             (db/select-one-field :location Collection :id (u/get-id c)))))))
+
+(deftest archive-noop-shouldnt-affect-descendants-test
+  (testing "Check that attempting to unarchive a Card that's not archived doesn't affect archived descendants"
+    (with-collection-hierarchy [{:keys [c e], :as collections}]
+      (db/update! Collection (u/get-id e), :archived true)
+      (db/update! Collection (u/get-id c), :archived false)
+      (is (= true
+             (db/select-one-field :archived Collection :id (u/get-id e)))))))
 
 ;; TODO - can you unarchive a Card that is inside an archived Collection??
 
@@ -1422,30 +1124,30 @@
 (expect
   #{"/collection/{new}/"
     "/collection/root/"}
-  (tt/with-temp PermissionsGroup [group]
+  (mt/with-temp PermissionsGroup [group]
     (perms/grant-collection-readwrite-permissions! group collection/root-collection)
-    (tt/with-temp Collection [collection {:name "{new}"}]
+    (mt/with-temp Collection [collection {:name "{new}"}]
       (group->perms [collection] group))))
 
 (expect
   #{"/collection/{new}/read/"
     "/collection/root/read/"}
-  (tt/with-temp PermissionsGroup [group]
+  (mt/with-temp PermissionsGroup [group]
     (perms/grant-collection-read-permissions! group collection/root-collection)
-    (tt/with-temp Collection [collection {:name "{new}"}]
+    (mt/with-temp Collection [collection {:name "{new}"}]
       (group->perms [collection] group))))
 
 ;; Needless to say, no perms before hand = no perms after
 (expect
   #{}
-  (tt/with-temp PermissionsGroup [group]
-    (tt/with-temp Collection [collection {:name "{new}"}]
+  (mt/with-temp PermissionsGroup [group]
+    (mt/with-temp Collection [collection {:name "{new}"}]
       (group->perms [collection] group))))
 
 ;; ...and granting perms after shouldn't affect Collections already created
 (expect
   #{"/collection/root/read/"}
-  (tt/with-temp* [PermissionsGroup [group]
+  (mt/with-temp* [PermissionsGroup [group]
                   Collection [collection {:name "{new}"}]]
     (perms/grant-collection-read-permissions! group collection/root-collection)
     (group->perms [collection] group)))
@@ -1454,31 +1156,31 @@
 (expect
   #{"/collection/{parent}/"
     "/collection/{child}/"}
-  (tt/with-temp* [PermissionsGroup [group]
+  (mt/with-temp* [PermissionsGroup [group]
                   Collection       [parent {:name "{parent}"}]]
     (perms/grant-collection-readwrite-permissions! group parent)
-    (tt/with-temp Collection [child {:name "{child}", :location (collection/children-location parent)}]
+    (mt/with-temp Collection [child {:name "{child}", :location (collection/children-location parent)}]
       (group->perms [parent child] group))))
 
 (expect
   #{"/collection/{parent}/read/"
     "/collection/{child}/read/"}
-  (tt/with-temp* [PermissionsGroup [group]
+  (mt/with-temp* [PermissionsGroup [group]
                   Collection       [parent {:name "{parent}"}]]
     (perms/grant-collection-read-permissions! group parent)
-    (tt/with-temp Collection [child {:name "{child}", :location (collection/children-location parent)}]
+    (mt/with-temp Collection [child {:name "{child}", :location (collection/children-location parent)}]
       (group->perms [parent child] group))))
 
 (expect
   #{}
-  (tt/with-temp* [PermissionsGroup [group]
+  (mt/with-temp* [PermissionsGroup [group]
                   Collection       [parent {:name "{parent}"}]
                   Collection       [child {:name "{child}", :location (collection/children-location parent)}]]
     (group->perms [parent child] group)))
 
 (expect
   #{"/collection/{parent}/read/"}
-  (tt/with-temp* [PermissionsGroup [group]
+  (mt/with-temp* [PermissionsGroup [group]
                   Collection       [parent {:name "{parent}"}]
                   Collection       [child {:name "{child}", :location (collection/children-location parent)}]]
     (perms/grant-collection-read-permissions! group parent)
@@ -1487,23 +1189,23 @@
 ;; If we have Root Collection perms they shouldn't be copied for a Child
 (expect
   #{"/collection/root/read/"}
-  (tt/with-temp* [PermissionsGroup [group]
+  (mt/with-temp* [PermissionsGroup [group]
                   Collection       [parent {:name "{parent}"}]]
     (perms/grant-collection-read-permissions! group collection/root-collection)
-    (tt/with-temp Collection [child {:name "{child}", :location (collection/children-location parent)}]
+    (mt/with-temp Collection [child {:name "{child}", :location (collection/children-location parent)}]
       (group->perms [parent child] group))))
 
 ;; Make sure that when creating a new Collection as child of a Personal Collection, no group permissions are created
 (expect
   false
-  (tt/with-temp Collection [child {:name "{child}", :location (lucky-collection-children-location)}]
+  (mt/with-temp Collection [child {:name "{child}", :location (lucky-collection-children-location)}]
     (db/exists? Permissions :object [:like (format "/collection/%d/%%" (u/get-id child))])))
 
 ;; Make sure that when creating a new Collection as grandchild of a Personal Collection, no group permissions are
 ;; created
 (expect
   false
-  (tt/with-temp* [Collection [child {:location (lucky-collection-children-location)}]
+  (mt/with-temp* [Collection [child {:location (lucky-collection-children-location)}]
                   Collection [grandchild {:location (collection/children-location child)}]]
     (or (db/exists? Permissions :object [:like (format "/collection/%d/%%" (u/get-id child))])
         (db/exists? Permissions :object [:like (format "/collection/%d/%%" (u/get-id grandchild))]))))
@@ -1513,34 +1215,35 @@
 ;;; |                                              Personal Collections                                              |
 ;;; +----------------------------------------------------------------------------------------------------------------+
 
-;; Make sure we're not allowed to *unarchive* a Personal Collection
-(expect
-  Exception
-  (tt/with-temp User [my-cool-user]
-    (let [personal-collection (collection/user->personal-collection my-cool-user)]
-      (db/update! Collection (u/get-id personal-collection) :archived true))))
-
-;; Make sure we're not allowed to *move* a Personal Collection
-(expect
-  Exception
-  (tt/with-temp* [User       [my-cool-user]
-                  Collection [some-other-collection]]
-    (let [personal-collection (collection/user->personal-collection my-cool-user)]
-      (db/update! Collection (u/get-id personal-collection) :location (collection/location-path some-other-collection)))))
-
-;; Make sure we're not allowed to change the owner of a Personal Collection
-(expect
-  Exception
-  (tt/with-temp User [my-cool-user]
-    (let [personal-collection (collection/user->personal-collection my-cool-user)]
-      (db/update! Collection (u/get-id personal-collection) :personal_owner_id (test-users/user->id :crowberto)))))
-
-;; Does hydrating `:personal_collection_id` force creation of Personal Collections?
-(expect
-  (tt/with-temp User [temp-user]
-    (-> (hydrate temp-user :personal_collection_id)
-        :personal_collection_id
-        integer?)))
+(deftest personal-collections-restrictions-test
+  (testing "Make sure we're not allowed to *unarchive* a Personal Collection"
+    (mt/with-temp User [my-cool-user]
+      (let [personal-collection (collection/user->personal-collection my-cool-user)]
+        (is (thrown?
+             Exception
+             (db/update! Collection (u/get-id personal-collection) :archived true))))))
+
+  (testing "Make sure we're not allowed to *move* a Personal Collection"
+    (mt/with-temp* [User       [my-cool-user]
+                    Collection [some-other-collection]]
+      (let [personal-collection (collection/user->personal-collection my-cool-user)]
+        (is (thrown?
+             Exception
+             (db/update! Collection (u/get-id personal-collection)
+               :location (collection/location-path some-other-collection)))))))
+
+  (testing "Make sure we're not allowed to change the owner of a Personal Collection"
+    (mt/with-temp User [my-cool-user]
+      (let [personal-collection (collection/user->personal-collection my-cool-user)]
+        (is (thrown?
+             Exception
+             (db/update! Collection (u/get-id personal-collection) :personal_owner_id (mt/user->id :crowberto)))))))
+
+  (testing "Does hydrating `:personal_collection_id` force creation of Personal Collections?"
+    (mt/with-temp User [temp-user]
+      (is (schema= {:personal_collection_id su/IntGreaterThanZero
+                    s/Keyword               s/Any}
+                   (hydrate temp-user :personal_collection_id))))))
 
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
@@ -1559,13 +1262,13 @@
 ;;                           ===>
 ;; Root Collection                  Root Collection > A
 (expect
-  #{"/collection/root/read/"
-    "/collection/A/read/"}
-  (tt/with-temp* [PermissionsGroup [group]
-                  Collection       [a {:name "A", :location (lucky-collection-children-location)}]]
-    (perms/grant-collection-read-permissions! group collection/root-collection)
-    (db/update! Collection (u/get-id a) :location (collection/children-location collection/root-collection))
-    (group->perms [a] group)))
+ #{"/collection/root/read/"
+   "/collection/A/read/"}
+ (mt/with-temp* [PermissionsGroup [group]
+                 Collection       [a {:name "A", :location (lucky-collection-children-location)}]]
+   (perms/grant-collection-read-permissions! group collection/root-collection)
+   (db/update! Collection (u/get-id a) :location (collection/children-location collection/root-collection))
+   (group->perms [a] group)))
 
 ;; When moving a Collection from a *descendant* of a Personal Collection to the Root Collection, we should create
 ;; perms entries that match the Root Collection's entries for any groups that have Root Collection perms.
@@ -1576,7 +1279,7 @@
 (expect
   #{"/collection/root/"
     "/collection/B/"}
-  (tt/with-temp* [PermissionsGroup [group]
+  (mt/with-temp* [PermissionsGroup [group]
                   Collection       [a {:name "A", :location (lucky-collection-children-location)}]
                   Collection       [b {:name "B", :location (collection/children-location a)}]]
     (perms/grant-collection-readwrite-permissions! group collection/root-collection)
@@ -1592,7 +1295,7 @@
 (expect
   #{"/collection/A/read/"
     "/collection/B/read/"}
-  (tt/with-temp* [PermissionsGroup [group]
+  (mt/with-temp* [PermissionsGroup [group]
                   Collection       [a {:name "A", :location (lucky-collection-children-location)}]
                   Collection       [b {:name "B", :location (collection/children-location collection/root-collection)}]]
     (perms/grant-collection-read-permissions! group b)
@@ -1608,7 +1311,7 @@
 (expect
   #{"/collection/B/"
     "/collection/C/"}
-  (tt/with-temp* [PermissionsGroup [group]
+  (mt/with-temp* [PermissionsGroup [group]
                   Collection       [a {:name "A", :location (lucky-collection-children-location)}]
                   Collection       [b {:name "B", :location (collection/children-location a)}]
                   Collection       [c {:name "C", :location (collection/children-location collection/root-collection)}]]
@@ -1625,7 +1328,7 @@
   #{"/collection/A/"
     "/collection/B/"
     "/collection/C/"}
-  (tt/with-temp* [PermissionsGroup [group]
+  (mt/with-temp* [PermissionsGroup [group]
                   Collection       [a {:name "A", :location (lucky-collection-children-location)}]
                   Collection       [b {:name "B", :location (collection/children-location a)}]
                   Collection       [c {:name "C", :location (collection/children-location collection/root-collection)}]]
@@ -1643,7 +1346,7 @@
 ;; Root Collection > A        Root Collection
 (expect
   #{}
-  (tt/with-temp* [PermissionsGroup [group]
+  (mt/with-temp* [PermissionsGroup [group]
                   Collection       [a {:name "A", :location (collection/children-location collection/root-collection)}]]
     (perms/grant-collection-readwrite-permissions! group a)
     (db/update! Collection (u/get-id a) :location (lucky-collection-children-location))
@@ -1657,7 +1360,7 @@
 ;; Root Collection > A > B        Root Collection > A
 (expect
   #{"/collection/A/"}
-  (tt/with-temp* [PermissionsGroup [group]
+  (mt/with-temp* [PermissionsGroup [group]
                   Collection       [a {:name "A", :location (collection/children-location collection/root-collection)}]
                   Collection       [b {:name "B", :location (collection/children-location a)}]]
     (perms/grant-collection-readwrite-permissions! group a)
@@ -1672,7 +1375,7 @@
 ;; Root Collection > B            Root Collection
 (expect
   #{}
-  (tt/with-temp* [PermissionsGroup [group]
+  (mt/with-temp* [PermissionsGroup [group]
                   Collection       [a {:name "A", :location (lucky-collection-children-location)}]
                   Collection       [b {:name "B", :location (collection/children-location collection/root-collection)}]]
     (perms/grant-collection-readwrite-permissions! group b)
@@ -1687,7 +1390,7 @@
 ;; Root Collection > B > C        Root Collection > B
 (expect
   #{"/collection/B/"}
-  (tt/with-temp* [PermissionsGroup [group]
+  (mt/with-temp* [PermissionsGroup [group]
                   Collection       [a {:name "A", :location (lucky-collection-children-location)}]
                   Collection       [b {:name "B", :location (collection/children-location collection/root-collection)}]
                   Collection       [c {:name "C", :location (collection/children-location b)}]]
@@ -1703,7 +1406,7 @@
 ;; Root Collection > B > C        Root Collection
 (expect
   #{}
-  (tt/with-temp* [PermissionsGroup [group]
+  (mt/with-temp* [PermissionsGroup [group]
                   Collection       [a {:name "A", :location (lucky-collection-children-location)}]
                   Collection       [b {:name "B", :location (collection/children-location collection/root-collection)}]
                   Collection       [c {:name "C", :location (collection/children-location b)}]]
@@ -1711,3 +1414,66 @@
     (perms/grant-collection-readwrite-permissions! group c)
     (db/update! Collection (u/get-id b) :location (collection/children-location a))
     (group->perms [a b c] group)))
+
+(deftest valid-location-path?-test
+  (doseq [[path expected] {nil       false
+                           ""        false
+                           "/"       true
+                           "/1"      false
+                           "/1/"     true
+                           "/1/2/"   true
+                           "/1/1/"   false
+                           "/1/2/1/" false
+                           "/1/2/3/" true
+                           "/abc/"   false
+                           "1"       false
+                           "/1.0/"   false
+                           "/-1/"    false
+                           1         false
+                           1.0       false}]
+    (testing (pr-str path)
+      (is (= expected
+             (#'collection/valid-location-path? path))))))
+
+(deftest check-parent-collection-namespace-matches-test
+  (doseq [[parent-namespace child-namespace] [[nil "x"]
+                                              ["x" nil]
+                                              ["x" "y"]]]
+    (mt/with-temp Collection [parent-collection {:namespace parent-namespace}]
+      (testing (format "You should not be able to create a Collection in namespace %s inside a Collection in namespace %s"
+                       (pr-str child-namespace) (pr-str parent-namespace))
+        (is (thrown-with-msg?
+             clojure.lang.ExceptionInfo
+             #"Collection must be in the same namespace as its parent"
+             (db/insert! Collection
+               {:location  (format "/%d/" (:id parent-collection))
+                :color     "#F38630"
+                :name      "Child Collection"
+                :namespace child-namespace}))))
+
+      (testing (format "You should not be able to move a Collection of namespace %s into a Collection of namespace %s"
+                       (pr-str child-namespace) (pr-str parent-namespace))
+        (mt/with-temp Collection [collection-2 {:namespace child-namespace}]
+          (is (thrown-with-msg?
+               clojure.lang.ExceptionInfo
+               #"Collection must be in the same namespace as its parent"
+               (collection/move-collection! collection-2 (format "/%d/" (:id parent-collection)))))))
+
+      (testing (format "You should not be able to change the namespace of a Collection from %s to %s"
+                       (pr-str parent-namespace) (pr-str child-namespace))
+        (is (thrown-with-msg?
+             clojure.lang.ExceptionInfo
+             #"You cannot move a Collection to a different namespace once it has been created"
+             (db/update! Collection (:id parent-collection) :namespace child-namespace)))))))
+
+(deftest check-special-collection-namespace-cannot-be-personal-collection
+  (testing "You should not be able to create a Personal Collection with a non-nil `:namespace`."
+    (mt/with-temp User [{user-id :id}]
+      (is (thrown-with-msg?
+           clojure.lang.ExceptionInfo
+           #"Personal Collections must be in the default namespace"
+           (db/insert! Collection
+             {:color             "#F38630"
+              :name              "Personal Collection"
+              :namespace         "x"
+              :personal_owner_id user-id}))))))
diff --git a/test/metabase/models/dashboard_test.clj b/test/metabase/models/dashboard_test.clj
index 661e27e6fa45245cc1f9be61ef7b43017123fc1f..8aa9ce628d67f6398d4678b548fa85fb0d5debcd 100644
--- a/test/metabase/models/dashboard_test.clj
+++ b/test/metabase/models/dashboard_test.clj
@@ -1,5 +1,9 @@
 (ns metabase.models.dashboard-test
-  (:require [expectations :refer [expect]]
+  (:require [clojure.test :refer :all]
+            [expectations :refer [expect]]
+            [metabase
+             [test :as mt]
+             [util :as u]]
             [metabase.api.common :as api]
             [metabase.automagic-dashboards.core :as magic]
             [metabase.models
@@ -17,7 +21,6 @@
              [data :refer :all]
              [util :as tu]]
             [metabase.test.data.users :as users]
-            [metabase.util :as u]
             [toucan.db :as db]
             [toucan.util.test :as tt]))
 
@@ -167,18 +170,18 @@
          (update (serialize-dashboard (Dashboard dashboard-id)) :cards check-ids)]))))
 
 
-;; test that a Dashboard's :public_uuid comes back if public sharing is enabled...
-(expect
-  (tu/with-temporary-setting-values [enable-public-sharing true]
-    (tt/with-temp Dashboard [dashboard {:public_uuid (str (java.util.UUID/randomUUID))}]
-      (boolean (:public_uuid dashboard)))))
+(deftest public-sharing-test
+  (testing "test that a Dashboard's :public_uuid comes back if public sharing is enabled..."
+    (tu/with-temporary-setting-values [enable-public-sharing true]
+      (tt/with-temp Dashboard [dashboard {:public_uuid (str (java.util.UUID/randomUUID))}]
+        (is (schema= u/uuid-regex
+                     (:public_uuid dashboard)))))
 
-;; ...but if public sharing is *disabled* it should come back as `nil`
-(expect
-  nil
-  (tu/with-temporary-setting-values [enable-public-sharing false]
-    (tt/with-temp Dashboard [dashboard {:public_uuid (str (java.util.UUID/randomUUID))}]
-      (:public_uuid dashboard))))
+    (testing "...but if public sharing is *disabled* it should come back as `nil`"
+      (tu/with-temporary-setting-values [enable-public-sharing false]
+        (tt/with-temp Dashboard [dashboard {:public_uuid (str (java.util.UUID/randomUUID))}]
+          (is (= nil
+                 (:public_uuid dashboard))))))))
 
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
@@ -226,23 +229,36 @@
     (binding [api/*current-user-permissions-set* (atom #{(perms/collection-readwrite-path collection)})]
       (mi/can-write? dash))))
 
+(deftest transient-dashboards-test
+  (testing "test that we save a transient dashboard"
+    (tu/with-model-cleanup ['Card 'Dashboard 'DashboardCard 'Collection]
+      (binding [api/*current-user-id*              (users/user->id :rasta)
+                api/*current-user-permissions-set* (-> :rasta
+                                                       users/user->id
+                                                       user/permissions-set
+                                                       atom)]
+        (let [dashboard                  (magic/automagic-analysis (Table (id :venues)) {})
+              rastas-personal-collection (db/select-one-field :id 'Collection
+                                           :personal_owner_id api/*current-user-id*)
+              saved-dashboard            (save-transient-dashboard! dashboard rastas-personal-collection)]
+          (is (= (db/count 'DashboardCard :dashboard_id (:id saved-dashboard))
+                 (-> dashboard :ordered_cards count))))))))
 
-;;; +----------------------------------------------------------------------------------------------------------------+
-;;; |                                           Transient Dashboard Tests                                            |
-;;; +----------------------------------------------------------------------------------------------------------------+
+(deftest validate-collection-namespace-test
+  (mt/with-temp Collection [{collection-id :id} {:namespace "currency"}]
+    (testing "Shouldn't be able to create a Dashboard in a non-normal Collection"
+      (let [dashboard-name (mt/random-name)]
+        (try
+          (is (thrown-with-msg?
+               clojure.lang.ExceptionInfo
+               #"A Dashboard can only go in Collections in the \"default\" namespace"
+               (db/insert! Dashboard (assoc (tt/with-temp-defaults Dashboard) :collection_id collection-id, :name dashboard-name))))
+          (finally
+            (db/delete! Dashboard :name dashboard-name)))))
 
-;; test that we save a transient dashboard
-(expect
-  (tu/with-model-cleanup ['Card 'Dashboard 'DashboardCard 'Collection]
-    (binding [api/*current-user-id*              (users/user->id :rasta)
-              api/*current-user-permissions-set* (-> :rasta
-                                                     users/user->id
-                                                     user/permissions-set
-                                                     atom)]
-      (let [dashboard                  (magic/automagic-analysis (Table (id :venues)) {})
-            rastas-personal-collection (db/select-one-field :id 'Collection
-                                         :personal_owner_id api/*current-user-id*)]
-        (->> (save-transient-dashboard! dashboard rastas-personal-collection)
-             :id
-             (db/count 'DashboardCard :dashboard_id)
-             (= (-> dashboard :ordered_cards count)))))))
+    (testing "Shouldn't be able to move a Dashboard to a non-normal Collection"
+      (mt/with-temp Dashboard [{card-id :id}]
+        (is (thrown-with-msg?
+             clojure.lang.ExceptionInfo
+             #"A Dashboard can only go in Collections in the \"default\" namespace"
+             (db/update! Dashboard card-id {:collection_id collection-id})))))))
diff --git a/test/metabase/models/native_query_snippet_test.clj b/test/metabase/models/native_query_snippet_test.clj
index f06ae358926a1bf7078263a9b9e96f922915295a..ee8fa7c7fa586d779a2ae9da687dc3fbcf1c3ec4 100644
--- a/test/metabase/models/native_query_snippet_test.clj
+++ b/test/metabase/models/native_query_snippet_test.clj
@@ -1,7 +1,8 @@
 (ns metabase.models.native-query-snippet-test
   (:require [clojure.test :refer :all]
-            [metabase.models.native-query-snippet :refer [NativeQuerySnippet]]
-            [metabase.test :as mt]
+            [metabase
+             [models :refer [Collection NativeQuerySnippet]]
+             [test :as mt]]
             [toucan.db :as db]))
 
 (deftest disallow-updating-creator-id-test
@@ -13,3 +14,46 @@
            (db/update! NativeQuerySnippet snippet-id :creator_id (mt/user->id :rasta))))
       (is (= (mt/user->id :lucky)
              (db/select-one-field :creator_id NativeQuerySnippet :id snippet-id))))))
+
+(deftest snippet-collection-test
+  (testing "Should be allowed to create snippets in a Collection in the :snippets namespace"
+    (mt/with-temp* [Collection         [{collection-id :id} {:namespace "snippets"}]
+                    NativeQuerySnippet [{snippet-id :id} {:collection_id collection-id}]]
+      (is (= collection-id
+             (db/select-one-field :collection_id NativeQuerySnippet :id snippet-id)))))
+
+  (doseq [[source dest] [[nil "snippets"]
+                         ["snippets" "snippets"]
+                         ["snippets" nil]]]
+    (testing (format "Should be allowed to move snippets from %s to %s"
+                     (if source "a :snippets Collection" "no Collection")
+                     (if dest "a :snippets Collection" "no Collection"))
+      (mt/with-temp* [Collection         [{source-collection-id :id} {:namespace source}]
+                      Collection         [{dest-collection-id :id}   {:namespace dest}]
+                      NativeQuerySnippet [{snippet-id :id} (when source
+                                                             {:collection_id source-collection-id})]]
+        (db/update! NativeQuerySnippet snippet-id :collection_id (when dest dest-collection-id))
+        (is (= (when dest dest-collection-id)
+               (db/select-one-field :collection_id NativeQuerySnippet :id snippet-id))))))
+
+  (doseq [collection-namespace [nil "x"]]
+    (testing (format "Should *not* be allowed to create snippets in a Collection in the %s namespace"
+                     (pr-str collection-namespace))
+      (mt/with-temp Collection [{collection-id :id} {:namespace collection-namespace}]
+        (is (thrown-with-msg?
+             clojure.lang.ExceptionInfo
+             #"A NativeQuerySnippet can only go in Collections in the :snippets namespace"
+             (db/insert! NativeQuerySnippet
+               {:name          (mt/random-name)
+                :content       "1 = 1"
+                :creator_id    (mt/user->id :rasta)
+                :collection_id collection-id})))))
+
+    (testing (format "Should *not* be allowed to move snippets into a Collection in the namespace %s" (pr-str collection-namespace))
+      (mt/with-temp* [Collection         [{source-collection-id :id} {:namespace "snippets"}]
+                      NativeQuerySnippet [{snippet-id :id}           {:collection_id source-collection-id}]
+                      Collection         [{dest-collection-id :id}   {:namespace collection-namespace}]]
+        (is (thrown-with-msg?
+             clojure.lang.ExceptionInfo
+             #"A NativeQuerySnippet can only go in Collections in the :snippets namespace"
+             (db/update! NativeQuerySnippet snippet-id :collection_id dest-collection-id)))))))
diff --git a/test/metabase/models/pulse_test.clj b/test/metabase/models/pulse_test.clj
index b8f85f280ef169f1848e2c731e03bc34ef518ad8..6303b15e1f3b1a7b063025bc8f35f0c7468fcdf3 100644
--- a/test/metabase/models/pulse_test.clj
+++ b/test/metabase/models/pulse_test.clj
@@ -1,6 +1,6 @@
 (ns metabase.models.pulse-test
   (:require [clojure.test :refer :all]
-            [expectations :refer :all]
+            [expectations :refer [expect]]
             [medley.core :as m]
             [metabase
              [test :as mt]
@@ -128,10 +128,10 @@
                           (dissoc (user-details :rasta) :is_superuser :is_qbnewb)]})
   (mt/with-temp Pulse [{:keys [id]}]
     (update-notification-channels! {:id id} [{:enabled       true
-                                       :channel_type  :email
-                                       :schedule_type :daily
-                                       :schedule_hour 4
-                                       :recipients    [{:email "foo@bar.com"} {:id (user->id :rasta)}]}])
+                                              :channel_type  :email
+                                              :schedule_type :daily
+                                              :schedule_hour 4
+                                              :recipients    [{:email "foo@bar.com"} {:id (user->id :rasta)}]}])
     (-> (PulseChannel :pulse_id id)
         (hydrate :recipients)
         (dissoc :id :pulse_id :created_at :updated_at)
@@ -258,3 +258,22 @@
   (with-pulse-in-collection [db _ pulse]
     (binding [api/*current-user-permissions-set* (atom #{(perms/object-path (u/get-id db))})]
       (mi/can-read? pulse))))
+
+(deftest validate-collection-namespace-test
+  (mt/with-temp Collection [{collection-id :id} {:namespace "currency"}]
+    (testing "Shouldn't be able to create a Pulse in a non-normal Collection"
+      (let [pulse-name (mt/random-name)]
+        (try
+          (is (thrown-with-msg?
+               clojure.lang.ExceptionInfo
+               #"A Pulse can only go in Collections in the \"default\" namespace"
+               (db/insert! Pulse (assoc (tt/with-temp-defaults Pulse) :collection_id collection-id, :name pulse-name))))
+          (finally
+            (db/delete! Pulse :name pulse-name)))))
+
+    (testing "Shouldn't be able to move a Pulse to a non-normal Collection"
+      (mt/with-temp Pulse [{card-id :id}]
+        (is (thrown-with-msg?
+             clojure.lang.ExceptionInfo
+             #"A Pulse can only go in Collections in the \"default\" namespace"
+             (db/update! Pulse card-id {:collection_id collection-id})))))))
diff --git a/test/metabase/sync_database/analyze_test.clj b/test/metabase/sync_database/analyze_test.clj
index ea4529cdbc7f2db777534e156645e9c71af9a9cc..faa7a4873f4c1a194d43ec0b143bbf08a524ddbe 100644
--- a/test/metabase/sync_database/analyze_test.clj
+++ b/test/metabase/sync_database/analyze_test.clj
@@ -17,9 +17,6 @@
 
 (defn- classified-special-type [values]
   (let [field (field/map->FieldInstance {:base_type :type/Text})]
-    (println "RESULT =" (classify-text-fingerprint/infer-special-type
-                         field
-                         (transduce identity (fingerprinters/fingerprinter field) values)))
     (:special_type (classify-text-fingerprint/infer-special-type
                     field
                     (transduce identity (fingerprinters/fingerprinter field) values)))))
diff --git a/test/metabase/test/util.clj b/test/metabase/test/util.clj
index 4146c5b6d4df04daf5c023d62a47b4e8d487f638..b3e03dcf6454609752e23f90b69f9ba9db2f3880 100644
--- a/test/metabase/test/util.clj
+++ b/test/metabase/test/util.clj
@@ -166,7 +166,9 @@
 
 (u/strict-extend (class NativeQuerySnippet)
   tt/WithTempDefaults
-  {:with-temp-defaults (fn [_] {:creator_id (user-id :crowberto)})})
+  {:with-temp-defaults (fn [_] {:creator_id (user-id :crowberto)
+                                :name       (random-name)
+                                :content    "1 = 1"})})
 
 (u/strict-extend (class PermissionsGroup)
   tt/WithTempDefaults
@@ -575,7 +577,7 @@
 
 (defn do-with-model-cleanup [model-seq f]
   (try
-    (testing (str (pr-str (cons 'with-model-cleanup model-seq)) "\n")
+    (testing (str "\n" (pr-str (cons 'with-model-cleanup (map name model-seq))) "\n")
       (f))
     (finally
       (doseq [model model-seq]