-
Ngoc Khuat authored
* use resolve-model with keyword check * do the same for primary-key and fix a check in serialization
Ngoc Khuat authored* use resolve-model with keyword check * do the same for primary-key and fix a check in serialization
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
test.clj 13.20 KiB
(ns metabase.test
"The stuff you need to write almost every test, all in one place. Nice!
(Prefer using `metabase.test` to requiring bits and pieces from these various namespaces going forward, since it
reduces the cognitive load required to write tests.)"
(:refer-clojure :exclude [compile])
(:require
[clojure.data]
[clojure.test :refer :all]
[environ.core :as env]
[hawk.init]
[hawk.parallel]
[humane-are.core :as humane-are]
[java-time :as t]
[medley.core :as m]
[metabase.actions.test-util :as actions.test-util]
[metabase.config :as config]
[metabase.db.util :as mdb.u]
[metabase.driver :as driver]
[metabase.driver.sql-jdbc.test-util :as sql-jdbc.tu]
[metabase.driver.sql.query-processor-test-util :as sql.qp-test-util]
[metabase.email-test :as et]
[metabase.http-client :as client]
[metabase.models :refer [PermissionsGroupMembership User]]
[metabase.models.permissions-group :as perms-group]
[metabase.query-processor :as qp]
[metabase.query-processor-test :as qp.test]
[metabase.query-processor.context :as qp.context]
[metabase.query-processor.reducible :as qp.reducible]
[metabase.query-processor.test-util :as qp.test-util]
[metabase.server.middleware.session :as mw.session]
[metabase.test-runner.assert-exprs :as test-runner.assert-exprs]
[metabase.test.data :as data]
[metabase.test.data.datasets :as datasets]
[metabase.test.data.env :as tx.env]
[metabase.test.data.impl :as data.impl]
[metabase.test.data.interface :as tx]
[metabase.test.data.users :as test.users]
[metabase.test.initialize :as initialize]
[metabase.test.persistence :as test.persistence]
[metabase.test.redefs]
[metabase.test.util :as tu]
[metabase.test.util.async :as tu.async]
[metabase.test.util.i18n :as i18n.tu]
[metabase.test.util.log :as tu.log]
[metabase.test.util.random :as tu.random]
[metabase.test.util.timezone :as test.tz]
[metabase.util.log :as log]
[pjstadig.humane-test-output :as humane-test-output]
[potemkin :as p]
[toucan.db :as db]
[toucan.util.test :as tt]
[toucan2.core :as t2]))
(set! *warn-on-reflection* true)
(humane-are/install!)
;; don't enable humane-test-output when running tests from the CLI, it breaks diffs.
(when-not config/is-test?
(humane-test-output/activate!))
;; Fool the linters into thinking these namespaces are used! See discussion on
;; https://github.com/clojure-emacs/refactor-nrepl/pull/270
(comment
client/keep-me
data/keep-me
data.impl/keep-me
datasets/keep-me
driver/keep-me
et/keep-me
i18n.tu/keep-me
initialize/keep-me
metabase.test.redefs/keep-me
mw.session/keep-me
test.persistence/keep-me
qp/keep-me
qp.test-util/keep-me
qp.test/keep-me
sql-jdbc.tu/keep-me
sql.qp-test-util/keep-me
test-runner.assert-exprs/keep-me
test.users/keep-me
tt/keep-me
tu/keep-me
tu.async/keep-me
tu.log/keep-me
tu.random/keep-me
test.tz/keep-me
tx/keep-me
tx.env/keep-me)
;; Add more stuff here as needed
(p/import-vars
[actions.test-util
with-actions
with-actions-disabled
with-actions-enabled
with-actions-test-data
with-actions-test-data-tables
with-actions-test-data-and-actions-enabled
with-temp-test-data]
[data
$ids
dataset
db
format-name
id
mbql-query
native-query
query
run-mbql-query
with-db
with-temp-copy-of-db
with-empty-h2-app-db]
[data.impl
*db-is-temp-copy?*]
[datasets
test-driver
test-drivers
when-testing-driver]
[driver
with-driver]
[et
email-to
fake-inbox-email-fn
inbox
received-email-body?
received-email-subject?
regex-email-bodies
reset-inbox!
summarize-multipart-email
with-expected-messages
with-fake-inbox]
[client
authenticate
build-url
client
client-full-response]
[i18n.tu
with-mock-i18n-bundles
with-user-locale]
[initialize
initialize-if-needed!]
[mw.session
with-current-user]
[qp
compile
preprocess
process-query]
[qp.test
col
cols
first-row
format-rows-by
formatted-rows
nest-query
normal-drivers
normal-drivers-except
normal-drivers-with-feature
normal-drivers-without-feature
rows
rows+column-names
with-bigquery-fks]
[qp.test-util
card-with-source-metadata-for-query
store-contents
with-database-timezone-id
with-everything-store
with-report-timezone-id
with-results-timezone-id]
[sql-jdbc.tu
sql-jdbc-drivers]
[sql.qp-test-util
with-native-query-testing-context]
[test-runner.assert-exprs
derecordize]
[test.persistence
with-persistence-enabled]
[test.users
fetch-user
test-user?
user->credentials
user->id
user-descriptor
user-http-request
with-group
with-group-for-user
with-test-user]
[tt
with-temp
with-temp*
with-temp-defaults]
[tu
boolean-ids-and-timestamps
call-with-paused-query
discard-setting-changes
doall-recursive
file->bytes
is-uuid-string?
obj->json->obj
postwalk-pred
round-all-decimals
scheduler-current-tasks
secret-value-equals?
select-keys-sequentially
throw-if-called
with-all-users-permission
with-column-remappings
with-discarded-collections-perms-changes
with-env-keys-renamed-by
with-locale
with-model-cleanup
with-non-admin-groups-no-root-collection-for-namespace-perms
with-non-admin-groups-no-root-collection-perms
with-temp-env-var-value
with-temp-dir
with-temp-file
with-temp-scheduler
with-temp-vals-in-db
with-temporary-setting-values
with-temporary-raw-setting-values
with-user-in-groups]
[tu.async
wait-for-result
with-open-channels]
[tu.log
ns-log-level
set-ns-log-level!
with-log-messages-for-level
with-log-level]
[tu.random
random-name
random-hash
random-email]
[test.tz
with-system-timezone-id]
[tx
count-with-template-tag-query
count-with-field-filter-query
dataset-definition
db-qualified-table-name
db-test-env-var
db-test-env-var!
db-test-env-var-or-throw
dbdef->connection-details
defdataset
dispatch-on-driver-with-test-extensions
get-dataset-definition
has-questionable-timezone-support?
has-test-extensions?
metabase-instance
sorts-nil-first?
supports-time-type?
supports-timestamptz-type?]
[tx.env
set-test-drivers!
with-test-drivers])
;;; TODO -- move all the stuff below into some other namespace and import it here.
(defn do-with-clock [clock thunk]
(hawk.parallel/assert-test-is-not-parallel "with-clock")
(testing (format "\nsystem clock = %s" (pr-str clock))
(let [clock (cond
(t/clock? clock) clock
(t/zoned-date-time? clock) (t/mock-clock (t/instant clock) (t/zone-id clock))
:else (throw (Exception. (format "Invalid clock: ^%s %s"
(.getName (class clock))
(pr-str clock)))))]
#_{:clj-kondo/ignore [:discouraged-var]}
(t/with-clock clock
(thunk)))))
(defmacro with-clock
"Same as [[t/with-clock]], but adds [[testing]] context, and also supports using `ZonedDateTime` instances
directly (converting them to a mock clock automatically).
(mt/with-clock #t \"2019-12-10T00:00-08:00[US/Pacific]\"
...)"
[clock & body]
`(do-with-clock ~clock (fn [] ~@body)))
(defn do-with-single-admin-user
[attributes thunk]
(let [existing-admin-memberships (t2/select PermissionsGroupMembership :group_id (:id (perms-group/admin)))
_ (t2/delete! (t2/table-name PermissionsGroupMembership) :group_id (:id (perms-group/admin)))
existing-admin-ids (t2/select-pks-set User :is_superuser true)
_ (when (seq existing-admin-ids)
(t2/update! (t2/table-name User) {:id [:in existing-admin-ids]} {:is_superuser false}))
temp-admin (first (t2/insert-returning-instances! User (merge (with-temp-defaults User)
attributes
{:is_superuser true})))
primary-key (mdb.u/primary-key User)]
(try
(thunk temp-admin)
(finally
(t2/delete! User primary-key (primary-key temp-admin))
(when (seq existing-admin-ids)
(t2/update! (t2/table-name User) {:id [:in existing-admin-ids]} {:is_superuser true}))
(db/insert-many! PermissionsGroupMembership existing-admin-memberships)))))
(defmacro with-single-admin-user
"Creates an admin user (with details described in the `options-map`) and (temporarily) removes the administrative
powers of all other users in the database.
Example:
(testing \"Check that the last superuser cannot deactivate themselves\"
(mt/with-single-admin-user [{id :id}]
(is (= \"You cannot remove the last member of the 'Admin' group!\"
(mt/user-http-request :crowberto :delete 400 (format \"user/%d\" id))))))"
[[binding-form & [options-map]] & body]
`(do-with-single-admin-user ~options-map (fn [~binding-form]
~@body)))
;;;; New QP middleware test util fns. Experimental. These will be put somewhere better if confirmed useful.
(defn test-qp-middleware
"Helper for testing QP middleware that uses the
(defn middleware [qp]
(fn [query rff context]
(qp query rff context)))
pattern, such as stuff in [[metabase.query-processor/around-middleware]]. Changes are returned in a map with keys:
* `:result` final result
* `:pre` `query` after preprocessing
* `:metadata` `metadata` after post-processing. Should be a map e.g. with `:cols`
* `:post` `rows` after post-processing transduction"
([middleware-fn]
(test-qp-middleware middleware-fn {}))
([middleware-fn query]
(test-qp-middleware middleware-fn query []))
([middleware-fn query rows]
(test-qp-middleware middleware-fn query {} rows))
([middleware-fn query metadata rows]
(test-qp-middleware middleware-fn query metadata rows nil))
([middleware-fn query metadata rows {:keys [run async?], :as context}]
{:pre [((some-fn nil? map?) metadata)]}
(let [async-qp (qp.reducible/async-qp
(qp.reducible/combine-middleware
(if (sequential? middleware-fn)
middleware-fn
[middleware-fn])))
context (merge
;; CI is S U P E R S L O W so give this a longer timeout.
{:timeout (if (env/env :ci)
5000
500)
:runf (fn [query rff context]
(try
(when run (run))
(qp.context/reducef rff context (assoc metadata :pre query) rows)
(catch Throwable e
(log/errorf "Error in test-qp-middleware runf: %s" e)
(throw e))))}
context)]
(if async?
(async-qp query context)
(binding [qp.reducible/*run-on-separate-thread?* true]
(let [qp (qp.reducible/sync-qp async-qp)
result (qp query context)]
{:result (m/dissoc-in result [:data :pre])
:pre (-> result :data :pre)
:post (-> result :data :rows)
:metadata (update result :data #(dissoc % :pre :rows))}))))))
(def ^{:arglists '([toucan-model])} object-defaults
"Return the default values for columns in an instance of a `toucan-model`, excluding ones that differ between
instances such as `:id`, `:name`, or `:created_at`. Useful for writing tests and comparing objects from the
application DB. Example usage:
(deftest update-user-first-name-test
(mt/with-temp User [user]
(update-user-first-name! user \"Cam\")
(is (= (merge (mt/object-defaults User)
(select-keys user [:id :last_name :created_at :updated_at])
{:name \"Cam\"})
(mt/decrecordize (t2/select-one User :id (:id user)))))))"
(comp
(memoize
(fn [toucan-model]
(with-temp* [toucan-model [x]
toucan-model [y]]
(let [[_ _ things-in-both] (clojure.data/diff x y)]
;; don't include created_at/updated_at even if they're the exactly the same, as might be the case with MySQL
;; TIMESTAMP columns (which only have second resolution by default)
(dissoc things-in-both :created_at :updated_at)))))
(fn [toucan-model]
(hawk.init/assert-tests-are-not-initializing (list 'object-defaults (symbol (name toucan-model))))
(initialize/initialize-if-needed! :db)
(mdb.u/resolve-model toucan-model))))
(defmacro disable-flaky-test-when-running-driver-tests-in-ci
"Only run `body` when we're not running driver tests in CI (i.e., `DRIVERS` and `CI` are both not set). Perfect for
disabling those damn flaky tests that cause CI to fail all the time. You should obviously only do this for things
that have nothing to do with drivers but tend to flake anyway."
{:style/indent 0}
[& body]
`(when (and (not (seq (env/env :drivers)))
(not (seq (env/env :ci))))
~@body))