(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")))))))) (deftest caching-test (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" (try (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)))))))))