mirror of
https://github.com/metosin/reitit.git
synced 2026-02-26 19:02:22 +00:00
The code was not finding the closest ancestor to the error type, because `ancestors` is not ordered. Now the code does a DFS to find a nearest ancestor. If the nearest ancestor is non-unique, an arbitrary one is picked.
254 lines
12 KiB
Clojure
254 lines
12 KiB
Clojure
(ns reitit.ring.middleware.exception-test
|
|
(:require [clojure.spec.alpha :as s]
|
|
[clojure.test :refer [deftest is testing]]
|
|
[muuntaja.core :as m]
|
|
[reitit.coercion :as coercion]
|
|
[reitit.coercion.spec]
|
|
[reitit.ring :as ring]
|
|
[reitit.ring.coercion]
|
|
[reitit.ring.middleware.exception :as exception]
|
|
[ring.util.http-response :as http-response])
|
|
(:import (clojure.lang ExceptionInfo)
|
|
(java.sql SQLException SQLWarning)))
|
|
|
|
(derive ::kukka ::kikka)
|
|
|
|
(deftest exception-test
|
|
(letfn [(create
|
|
([f]
|
|
(create f nil))
|
|
([f wrap]
|
|
(ring/ring-handler
|
|
(ring/router
|
|
[["/defaults"
|
|
{:handler f}]
|
|
["/http-response"
|
|
{:handler (fn [req]
|
|
(http-response/unauthorized! "Unauthorized"))}]
|
|
["/coercion"
|
|
{:middleware [reitit.ring.coercion/coerce-request-middleware
|
|
reitit.ring.coercion/coerce-response-middleware]
|
|
:coercion reitit.coercion.spec/coercion
|
|
:parameters {:query {:x int?, :y int?}}
|
|
:responses {200 {:body {:total pos-int?}}}
|
|
:handler f}]]
|
|
{:data {:middleware [(exception/create-exception-middleware
|
|
(merge
|
|
exception/default-handlers
|
|
{::kikka (constantly (http-response/bad-request "kikka"))
|
|
SQLException (constantly (http-response/bad-request "sql"))
|
|
::exception/wrap wrap}))]}}))))]
|
|
|
|
(testing "normal calls work ok"
|
|
(let [response {:status 200, :body "ok"}
|
|
app (create (fn [_] response))]
|
|
(is (= response (app {:request-method :get, :uri "/defaults"})))))
|
|
|
|
(testing "unknown exception"
|
|
(let [app (create (fn [_] (throw (NullPointerException.))))
|
|
{:keys [status body] :as resp} (app {:request-method :get, :uri "/defaults"})]
|
|
(is (http-response/response? resp))
|
|
(is (= status 500))
|
|
(is (= body {:type "exception"
|
|
:class "java.lang.NullPointerException"})))
|
|
(let [app (create (fn [_] (throw (ex-info "fail" {:type ::invalid}))))
|
|
{:keys [status body] :as resp} (app {:request-method :get, :uri "/defaults"})]
|
|
(is (http-response/response? resp))
|
|
(is (= status 500))
|
|
(is (= body {:type "exception"
|
|
:class "clojure.lang.ExceptionInfo"}))))
|
|
|
|
(testing "::ring/response"
|
|
(let [response (http-response/ok "ok")
|
|
app (create (fn [_] (throw (ex-info "fail" {:type ::ring/response, :response response}))))]
|
|
(is (= response (app {:request-method :get, :uri "/defaults"})))))
|
|
|
|
(testing "::ring.util.http-response/response"
|
|
(let [response {:status 401 :body "Unauthorized" :headers {}}
|
|
app (create (fn [_] (throw (ex-info "Unauthorized!" {:type ::http-response/response
|
|
:response response}))))]
|
|
(is (= response (app {:request-method :post, :uri "/http-response"})))))
|
|
|
|
(testing ":muuntaja/decode"
|
|
(let [app (create (fn [_] (m/decode m/instance "application/json" "{:so \"invalid\"}")))]
|
|
(is (= {:body "Malformed \"application/json\" request."
|
|
:headers {"Content-Type" "text/plain"}
|
|
:status 400}
|
|
(app {:request-method :get, :uri "/defaults"}))))
|
|
|
|
(testing "::coercion/request-coercion"
|
|
(let [app (create (fn [{{{:keys [x y]} :query} :parameters}]
|
|
(http-response/ok {:total (+ x y)})))]
|
|
|
|
(let [{:keys [status body] :as resp} (app {:request-method :get
|
|
:uri "/coercion"
|
|
:query-params {"x" "1", "y" "2"}})]
|
|
(is (http-response/response? resp))
|
|
(is (= 200 status))
|
|
(is (= {:total 3} body)))
|
|
|
|
(let [{:keys [status body] :as resp} (app {:request-method :get
|
|
:uri "/coercion"
|
|
:query-params {"x" "abba", "y" "2"}})]
|
|
(is (http-response/response? resp))
|
|
(is (= 400 status))
|
|
(is (= :reitit.coercion/request-coercion (:type body))))
|
|
|
|
(let [{:keys [status body] :as resp} (app {:request-method :get
|
|
:uri "/coercion"
|
|
:query-params {"x" "-10", "y" "2"}})]
|
|
(is (http-response/response? resp))
|
|
(is (= 500 status))
|
|
(is (= :reitit.coercion/response-coercion (:type body)))))))
|
|
|
|
(testing "exact :type"
|
|
(let [app (create (fn [_] (throw (ex-info "fail" {:type ::kikka}))))
|
|
{:keys [status body] :as resp} (app {:request-method :get, :uri "/defaults"})]
|
|
(is (http-response/response? resp))
|
|
(is (= status 400))
|
|
(is (= body "kikka"))))
|
|
|
|
(testing "parent :type"
|
|
(let [app (create (fn [_] (throw (ex-info "fail" {:type ::kukka}))))
|
|
{:keys [status body] :as resp} (app {:request-method :get, :uri "/defaults"})]
|
|
(is (http-response/response? resp))
|
|
(is (= status 400))
|
|
(is (= body "kikka"))))
|
|
|
|
(testing "exact Exception"
|
|
(let [app (create (fn [_] (throw (SQLException.))))
|
|
{:keys [status body] :as resp} (app {:request-method :get, :uri "/defaults"})]
|
|
(is (http-response/response? resp))
|
|
(is (= status 400))
|
|
(is (= body "sql"))))
|
|
|
|
(testing "Exception SuperClass"
|
|
(let [app (create (fn [_] (throw (SQLWarning.))))
|
|
{:keys [status body] :as resp} (app {:request-method :get, :uri "/defaults"})]
|
|
(is (http-response/response? resp))
|
|
(is (= status 400))
|
|
(is (= body "sql"))))
|
|
|
|
(testing "::exception/wrap"
|
|
(let [calls (atom 0)
|
|
app (create (fn [_] (throw (SQLWarning.)))
|
|
(fn [handler exception request]
|
|
(if (< (swap! calls inc) 2)
|
|
(handler exception request)
|
|
{:status 500
|
|
:headers {}
|
|
:body "too many tries"})))]
|
|
(let [{:keys [status body] :as resp} (app {:request-method :get, :uri "/defaults"})]
|
|
(is (http-response/response? resp))
|
|
(is (= status 400))
|
|
(is (= body "sql")))
|
|
(let [{:keys [status body] :as resp} (app {:request-method :get, :uri "/defaults"})]
|
|
(is (http-response/response? resp))
|
|
(is (= status 500))
|
|
(is (= body "too many tries")))))))
|
|
|
|
(derive ::table ::object)
|
|
(derive ::living ::object)
|
|
(derive ::plant ::living)
|
|
(derive ::animal ::living)
|
|
(derive ::dog ::animal)
|
|
(derive ::cat ::animal)
|
|
(derive ::garfield ::cat)
|
|
|
|
(deftest exception-hierarchy-test
|
|
(letfn [(create [f]
|
|
(ring/ring-handler
|
|
(ring/router
|
|
[["/defaults"
|
|
{:handler f}]]
|
|
{:data {:middleware [(exception/create-exception-middleware
|
|
(merge
|
|
exception/default-handlers
|
|
{::object (constantly (http-response/bad-request "object"))
|
|
::living (constantly (http-response/bad-request "living"))
|
|
::animal (constantly (http-response/bad-request "animal"))
|
|
::cat (constantly (http-response/bad-request "cat"))}))]}})))
|
|
(call [ex-typ]
|
|
(let [app (create (fn [_] (throw (ex-info "fail" {:type ex-typ}))))]
|
|
(app {:request-method :get, :uri "/defaults"})))]
|
|
(let [{:keys [status body]} (call ::object)]
|
|
(is (= status 400))
|
|
(is (= body "object")))
|
|
(let [{:keys [status body]} (call ::table)]
|
|
(is (= status 400))
|
|
(is (= body "object")))
|
|
(let [{:keys [status body]} (call ::living)]
|
|
(is (= status 400))
|
|
(is (= body "living")))
|
|
(let [{:keys [status body]} (call ::plant)]
|
|
(is (= status 400))
|
|
(is (= body "living")))
|
|
(let [{:keys [status body]} (call ::animal)]
|
|
(is (= status 400))
|
|
(is (= body "animal")))
|
|
(let [{:keys [status body]} (call ::dog)]
|
|
(is (= status 400))
|
|
(is (= body "animal")))
|
|
(let [{:keys [status body]} (call ::cat)]
|
|
(is (= status 400))
|
|
(is (= body "cat")))
|
|
(let [{:keys [status body]} (call ::garfield)]
|
|
(is (= status 400))
|
|
(is (= body "cat")))))
|
|
|
|
(deftest spec-coercion-exception-test
|
|
(let [app (ring/ring-handler
|
|
(ring/router
|
|
["/plus"
|
|
{:get
|
|
{:parameters {:query {:x int?, :y int?}}
|
|
:responses {200 {:body {:total pos-int?}}}
|
|
:handler (fn [{{{:keys [x y]} :query} :parameters}]
|
|
(http-response/ok {:total (+ x y)}))}}]
|
|
{:data {:coercion reitit.coercion.spec/coercion
|
|
:middleware [(exception/create-exception-middleware
|
|
(merge
|
|
exception/default-handlers
|
|
{::coercion/request-coercion (fn [e _] (http-response/bad-request (ex-data e)) )
|
|
::coercion/response-coercion (fn [e _] {:status 500
|
|
:headers {}
|
|
:body (ex-data e)})}))
|
|
reitit.ring.coercion/coerce-request-middleware
|
|
reitit.ring.coercion/coerce-response-middleware]}}))]
|
|
(testing "success"
|
|
(let [{:keys [status body] :as resp} (app {:uri "/plus", :request-method :get, :query-params {"x" "1", "y" "2"}})]
|
|
(is (http-response/response? resp))
|
|
(is (= 200 status))
|
|
(is (= body {:total 3}))))
|
|
|
|
(testing "request error"
|
|
(let [{:keys [status body] :as resp} (app {:uri "/plus", :request-method :get, :query-params {"x" "1", "y" "fail"}})]
|
|
(is (http-response/response? resp))
|
|
(is (= 400 status))
|
|
(testing "spec error is exposed as is"
|
|
(let [problems (:problems body)]
|
|
(is (contains? problems ::s/spec))
|
|
(is (contains? problems ::s/value))
|
|
(is (contains? problems ::s/problems))))))
|
|
|
|
(testing "response error"
|
|
(let [{:keys [status body] :as resp} (app {:uri "/plus", :request-method :get, :query-params {"x" "1", "y" "-2"}})]
|
|
(is (http-response/response? resp))
|
|
(is (= 500 status))
|
|
(testing "spec error is exposed as is"
|
|
(let [problems (:problems body)]
|
|
(is (contains? problems ::s/spec))
|
|
(is (contains? problems ::s/value))
|
|
(is (contains? problems ::s/problems))))))))
|
|
|
|
(deftest response-keys-test
|
|
(is (thrown-with-msg?
|
|
ExceptionInfo
|
|
#"Response status must be int"
|
|
(ring/ring-handler
|
|
(ring/router
|
|
[["/coercion"
|
|
{:middleware [reitit.ring.coercion/coerce-response-middleware]
|
|
:coercion reitit.coercion.spec/coercion
|
|
:responses {:200 {:body {:total pos-int?}}}
|
|
:handler identity}]])))))
|