Skip to content
Snippets Groups Projects
field_test.clj 12.3 KiB
Newer Older
  • Learn to ignore specific revisions
  • (ns metabase-enterprise.sandbox.api.field-test
      "Tests for special behavior of `/api/metabase/field` endpoints in the Metabase Enterprise Edition."
      (:require [clojure.test :refer :all]
                [metabase-enterprise.sandbox.test-util :as mt.tu]
    
                [metabase.models :refer [Field FieldValues User]]
                [metabase.models.field-values :as field-values]
    
                [metabase.test :as mt]
                [toucan.db :as db]))
    
    (deftest fetch-field-test
      (testing "GET /api/field/:id"
    
        (mt/with-gtaps {:gtaps      {:venues {:query      (mt.tu/restricted-column-query (mt/id))
                                              :remappings {:cat [:variable [:field-id (mt/id :venues :category_id)]]}}}
                        :attributes {:cat 50}}
          (testing "Can I fetch a Field that I don't have read access for if I have segmented table access for it?"
            (let [result (mt/user-http-request :rasta :get 200 (str "field/" (mt/id :venues :name)))]
              (is (map? result))
              (is (= {:name             "NAME"
                      :display_name     "Name"
                      :has_field_values "list"}
                     (select-keys result [:name :display_name :has_field_values]))))))))
    
    
    (deftest field-values-test
      (testing "GET /api/field/:id/values"
    
        (mt/with-temp-copy-of-db
          (doseq [[query-type gtap-rule]
                  [["MBQL"
                    {:gtaps      {:venues {:query      (mt.tu/restricted-column-query (mt/id))
                                           :remappings {:cat [:dimension (mt/id :venues :category_id)]}}}
                     :attributes {:cat 50}}]
                   ["native"
                    {:gtaps      {:venues {:query
                                           (mt/native-query
                                             {:query "SELECT id, name, category_id FROM venues WHERE category_id = {{cat}}"
                                              :template-tags {"cat" {:id           "__MY_CAT__"
                                                                     :name         "cat"
                                                                     :display-name "Cat id"
                                                                     :type         :number}}})
                                           :remappings {:cat [:variable [:template-tag "cat"]]}}}
                     :attributes {:cat 50}}]]]
            (testing (format "GTAP rule is a %s query" query-type)
              (mt/with-gtaps gtap-rule
                (testing (str "When I call the FieldValues API endpoint for a Field that I have segmented table access only "
                              "for, will I get ad-hoc values?\n")
                  (letfn [(fetch-values [user field]
                            (-> (mt/user-http-request user :get 200 (format "field/%d/values" (mt/id :venues field)))
                                (update :values (partial take 3))))]
                    ;; Rasta Toucan is only allowed to see Venues that are in the "Mexican" category [category_id = 50]. n
                    ;; fetching FieldValues for `venue.name` should do an ad-hoc fetch and only return the names of venues in
                    ;; that category.
                    (is (= {:field_id        (mt/id :venues :name)
                            :values          [["Garaje"]
                                              ["Gordo Taqueria"]
                                              ["La Tortilla"]]
                            :has_more_values false}
                           (fetch-values :rasta :name)))
    
                    (testing (str "Now in this case recall that the `restricted-column-query` GTAP we're using does *not* include "
                                  "`venues.price` in the results. (Toucan isn't allowed to know the number of dollar signs!) So "
                                  "make sure if we try to fetch the field values instead of seeing `[[1] [2] [3] [4]]` we get no "
                                  "results")
                      (is (= {:field_id        (mt/id :venues :price)
                              :values          []
                              :has_more_values false}
                             (fetch-values :rasta :price))))
    
                    (testing "Reset field values; if another User fetches them first, do I still see sandboxed values? (metabase/metaboat#128)"
                      (field-values/clear-field-values-for-field! (mt/id :venues :name))
                      ;; fetch Field values with an admin
                      (testing "Admin should see all Field values"
                        (is (= {:field_id        (mt/id :venues :name)
                                :values          [["20th Century Cafe"]
                                                  ["25°"]
                                                  ["33 Taps"]]
                                :has_more_values false}
                               (fetch-values :crowberto :name))))
                      (testing "Sandboxed User should still see only their values after an admin fetches the values"
    
                        (is (= {:field_id        (mt/id :venues :name)
    
                                :values          [["Garaje"]
                                                  ["Gordo Taqueria"]
                                                  ["La Tortilla"]]
    
                                :has_more_values false}
    
                               (fetch-values :rasta :name))))
                      (testing "A User with a *different* sandbox should see their own values"
                        (let [password (mt/random-name)]
                          (mt/with-temp User [another-user {:password password}]
                            (mt/with-gtaps-for-user another-user {:gtaps      {:venues
                                                                               {:remappings
                                                                                {:cat
                                                                                 [:dimension (mt/id :venues :category_id)]}}}
                                                                  :attributes {:cat 5 #_BBQ}}
                              (is (= {:field_id        (mt/id :venues :name)
                                      :values          [["Baby Blues BBQ"]
                                                        ["Bludso's BBQ"]
                                                        ["Boneyard Bistro"]]
                                      :has_more_values false}
                                     (-> (mt/client {:username (:email another-user), :password password}
                                                    :get 200
                                                    (format "field/%d/values" (mt/id :venues :name)))
                                         (update :values (partial take 3))))))))))))))))))
    
    (deftest human-readable-values-test
      (testing "GET /api/field/:id/values should returns correct human readable mapping if exists"
        (mt/with-temp-copy-of-db
          (let [field-id   (mt/id :venues :price)
                full-fv-id (db/select-one-id FieldValues :field_id field-id :type :full)]
            (db/update! FieldValues full-fv-id
                        :human_readable_values ["$" "$$" "$$$" "$$$$"])
            ;; sanity test without gtap
            (is (= [[1 "$"] [2 "$$"] [3 "$$$"] [4 "$$$$"]]
                   (:values (mt/user-http-request :rasta :get 200 (format "field/%d/values" field-id)))))
            (mt/with-gtaps {:gtaps      {:venues
                                         {:remappings {:cat [:variable [:field-id (mt/id :venues :category_id)]]}}}
                            :attributes {:cat 4}}
              (is (= [[1 "$"] [3 "$$$"]]
                     (:values (mt/user-http-request :rasta :get 200 (format "field/%d/values" (mt/id :venues :price)))))))))))
    
    
    (deftest search-test
      (testing "GET /api/field/:id/search/:search-id"
    
        (mt/with-gtaps {:gtaps      {:venues
                                     {:remappings {:cat [:variable [:field-id (mt/id :venues :category_id)]]}
                                      :query      (mt.tu/restricted-column-query (mt/id))}}
                        :attributes {:cat 50}}
          (testing (str "Searching via the query builder needs to use a GTAP when the user has segmented permissions. "
                        "This tests out a field search on a table with segmented permissions")
            ;; Rasta Toucan is only allowed to see Venues that are in the "Mexican" category [category_id = 50]. So
            ;; searching whould only include venues in that category
            (let [url (format "field/%s/search/%s" (mt/id :venues :name) (mt/id :venues :name))]
    
              (is (= [["Gordo Taqueria"         "Gordo Taqueria"]
                      ["Tacos Villa Corona"     "Tacos Villa Corona"]
    
                      ["Taqueria Los Coyotes"   "Taqueria Los Coyotes"]
    
                      ["Taqueria San Francisco" "Taqueria San Francisco"]
                      ["Tito's Tacos"           "Tito's Tacos"]
                      ["Yuca's Taqueria"        "Yuca's Taqueria"]]
    
                     (mt/user-http-request :rasta :get 200 url :value "Ta"))))))))
    
      (mt/with-gtaps {:gtaps
                      {:venues
                       {:remappings {:cat [:variable [:field-id (mt/id :venues :category_id)]]}
                        :query      (mt.tu/restricted-column-query (mt/id))}}
                      :attributes {:cat 50}}
    
        (let [field (db/select-one Field :id (mt/id :venues :name))]
    
          ;; Make sure FieldValues are populated
    
          (field-values/get-or-create-full-field-values! field)
    
          ;; Warm up the cache
    
          (mt/user-http-request :rasta :get 200 (str "field/" (:id field) "/values"))
    
          (testing "Do we use cached values when available?"
            (with-redefs [field-values/distinct-values (fn [_] (assert false "Should not be called"))]
    
              (is (some? (:values (mt/user-http-request :rasta :get 200 (str "field/" (:id field) "/values")))))
              (is (= 1 (db/count FieldValues
                                 :field_id (:id field)
                                 :type :sandbox)))))
    
          (testing "Do different users has different sandbox FieldValues"
            (let [password (mt/random-name)]
              (mt/with-temp User [another-user {:password password}]
                (mt/with-gtaps-for-user another-user {:gtaps      {:venues
                                                                   {:remappings {:cat [:variable [:field-id (mt/id :venues :category_id)]]}
                                                                    :query      (mt.tu/restricted-column-query (mt/id))}}
                                                      :attributes {:cat 5}}
                  (mt/user-http-request another-user :get 200 (str "field/" (:id field) "/values"))
                  ;; create another one for the new user
                  (is (= 2 (db/count FieldValues
                                     :field_id (:id field)
                                     :type :sandbox)))))))
    
          (testing "Do we invalidate the cache when full FieldValues change"
    
              (let [;; Updating FieldValues which should invalidate the cache
                    fv-id      (db/select-one-id FieldValues :field_id (:id field) :type :full)
                    new-values ["foo" "bar"]]
    
                (testing "Sanity check: make sure FieldValues exist"
                  (is (some? fv-id)))
                (db/update! FieldValues fv-id
    
                            {:values new-values})
    
                (with-redefs [field-values/distinct-values (constantly {:values          new-values
                                                                        :has_more_values false})]
    
                  (is (= (map vector new-values)
    
                         (:values (mt/user-http-request :rasta :get 200 (str "field/" (:id field) "/values")))))))
    
              (finally
                ;; Put everything back as it was
    
                (field-values/get-or-create-full-field-values! field))))
    
          (testing "When a sandbox fieldvalues expired, do we delete it then create a new one?"
            (#'field-values/clear-advanced-field-values-for-field! field)
            ;; make sure we have a cache
            (mt/user-http-request :rasta :get 200 (str "field/" (:id field) "/values"))
            (let [old-sandbox-fv-id (db/select-one-id FieldValues :field_id (:id field) :type :sandbox)]
              (with-redefs [field-values/advanced-field-values-expired? (fn [fv]
                                                                          (= (:id fv) old-sandbox-fv-id))]
                (mt/user-http-request :rasta :get 200 (str "field/" (:id field) "/values"))
                ;; did the old one get deleted?
                (is (not (db/exists? FieldValues :id old-sandbox-fv-id)))
                ;; make sure we created a new one
                (is (= 1 (db/count FieldValues :field_id (:id field) :type :sandbox)))))))))