mirror of
https://github.com/metosin/reitit.git
synced 2025-12-16 16:01:11 +00:00
248 lines
9 KiB
Clojure
248 lines
9 KiB
Clojure
(ns reitit.interceptor-test
|
|
(:require [clojure.test :refer [deftest is testing]]
|
|
[reitit.core :as r]
|
|
[reitit.interceptor :as interceptor])
|
|
#?(:clj
|
|
(:import (clojure.lang ExceptionInfo))))
|
|
|
|
(def ctx (interceptor/context []))
|
|
|
|
(defn execute [interceptors ctx]
|
|
(as-> ctx $
|
|
(reduce #(%2 %1) $ (keep :enter interceptors))
|
|
(reduce #(%2 %1) $ (reverse (keep :leave interceptors)))
|
|
(:response $)))
|
|
|
|
(defn f [value ctx]
|
|
(update ctx :request conj value))
|
|
|
|
(defn kws [k qk]
|
|
(keyword (namespace qk) (str (name k) "_" (name qk))))
|
|
|
|
(defn interceptor [value]
|
|
{:name value
|
|
:enter #(update % :request (fnil conj []) (kws :enter value))
|
|
:leave #(update % :response (fnil conj []) (kws :leave value))})
|
|
|
|
(defn enter [value]
|
|
{:name value
|
|
:enter (partial f value)})
|
|
|
|
(defn handler [request]
|
|
(conj request :ok))
|
|
|
|
(defn create
|
|
([interceptors]
|
|
(create interceptors nil))
|
|
([interceptors opts]
|
|
(let [chain (interceptor/chain
|
|
(conj interceptors handler)
|
|
:data opts)]
|
|
(partial execute chain))))
|
|
|
|
(deftest expand-interceptor-test
|
|
|
|
(testing "interceptor records"
|
|
|
|
(testing "interceptor"
|
|
(let [calls (atom 0)
|
|
enter (fn [value]
|
|
(swap! calls inc)
|
|
{:enter (fn [ctx]
|
|
(update ctx :request conj value))})]
|
|
|
|
(testing "as function"
|
|
(reset! calls 0)
|
|
(let [app (create [(enter :value)])]
|
|
(dotimes [_ 10]
|
|
(is (= [:value :ok] (app ctx)))
|
|
(is (= 1 @calls)))))
|
|
|
|
(testing "as interceptor vector"
|
|
(reset! calls 0)
|
|
(let [app (create [[enter :value]])]
|
|
(dotimes [_ 10]
|
|
(is (= [:value :ok] (app ctx)))
|
|
(is (= 1 @calls)))))
|
|
|
|
(testing "as keyword"
|
|
(reset! calls 0)
|
|
(let [app (create [:enter] {::interceptor/registry {:enter (enter :value)}})]
|
|
(dotimes [_ 10]
|
|
(is (= [:value :ok] (app ctx)))
|
|
(is (= 1 @calls)))))
|
|
|
|
(testing "missing keyword"
|
|
(is (thrown-with-msg?
|
|
ExceptionInfo
|
|
#"Interceptor :enter not found in registry"
|
|
(create [:enter]))))
|
|
|
|
(testing "existing keyword, compiling to nil"
|
|
(let [app (create [:enter] {::interceptor/registry {:enter {:compile (constantly nil)}}})]
|
|
(is (= [:ok] (app ctx)))))
|
|
|
|
(testing "as map"
|
|
(reset! calls 0)
|
|
(let [app (create [{:enter (:enter (enter :value))}])]
|
|
(dotimes [_ 10]
|
|
(is (= [:value :ok] (app ctx)))
|
|
(is (= 1 @calls)))))
|
|
|
|
(testing "as Interceptor"
|
|
(reset! calls 0)
|
|
(let [app (create [(interceptor/map->Interceptor {:enter (:enter (enter :value))})])]
|
|
(dotimes [_ 10]
|
|
(is (= [:value :ok] (app ctx)))
|
|
(is (= 1 @calls)))))))
|
|
|
|
(testing "compiled interceptor"
|
|
(let [calls (atom 0)
|
|
i1 (fn [value]
|
|
{:compile (fn [data _]
|
|
(swap! calls inc)
|
|
{:enter (fn [ctx]
|
|
(update ctx :request into [data value]))})})
|
|
i3 (fn [value]
|
|
{:compile (fn [_ _]
|
|
(swap! calls inc)
|
|
{:compile (fn [_ _]
|
|
(swap! calls inc)
|
|
(i1 value))})})]
|
|
|
|
(testing "as function"
|
|
(reset! calls 0)
|
|
(let [app (create [[i1 :value]])]
|
|
(dotimes [_ 10]
|
|
(is (= [:data :value :ok] (app ctx)))
|
|
(is (= 1 @calls)))))
|
|
|
|
(testing "as interceptor"
|
|
(reset! calls 0)
|
|
(let [app (create [(i1 :value)])]
|
|
(dotimes [_ 10]
|
|
(is (= [:data :value :ok] (app ctx)))
|
|
(is (= 1 @calls)))))
|
|
|
|
(testing "deeply compiled interceptor"
|
|
(reset! calls 0)
|
|
(let [app (create [[i3 :value]])]
|
|
(dotimes [_ 10]
|
|
(is (= [:data :value :ok] (app ctx)))
|
|
(is (= 3 @calls)))))
|
|
|
|
(testing "too deeply compiled interceptor fails"
|
|
(binding [interceptor/*max-compile-depth* 2]
|
|
(is (thrown?
|
|
ExceptionInfo
|
|
#"Too deep Interceptor compilation"
|
|
(create [[i3 :value]])))))
|
|
|
|
(testing "nil unmounts the interceptor"
|
|
(let [app (create [{:compile (constantly nil)}
|
|
{:compile (constantly nil)}])]
|
|
(dotimes [_ 10]
|
|
(is (= [:ok] (app ctx))))))))))
|
|
|
|
(defn create-app [router]
|
|
(let [handler (interceptor/interceptor-handler router)]
|
|
(fn [path]
|
|
(when-let [interceptors (handler path)]
|
|
(execute interceptors ctx)))))
|
|
|
|
(deftest interceptor-handler-test
|
|
|
|
(testing "interceptor-handler"
|
|
(let [api-interceptor (interceptor :api)
|
|
router (interceptor/router
|
|
[["/ping" handler]
|
|
["/api" {:interceptors [api-interceptor]}
|
|
["/ping" handler]
|
|
["/admin" {:interceptors [[interceptor :admin]]}
|
|
["/ping" handler]]]])
|
|
app (create-app router)]
|
|
|
|
(testing "not found"
|
|
(is (= nil (app "/favicon.ico"))))
|
|
|
|
(testing "normal handler"
|
|
(is (= [:ok] (app "/ping"))))
|
|
|
|
(testing "with interceptor"
|
|
(is (= [:enter_api :ok :leave_api] (app "/api/ping"))))
|
|
|
|
(testing "with nested interceptor"
|
|
(is (= [:enter_api :enter_admin :ok :leave_admin :leave_api] (app "/api/admin/ping"))))
|
|
|
|
(testing ":compile interceptor can be unmounted at creation-time"
|
|
(let [i1 {:name ::i1, :compile (constantly (interceptor ::i1))}
|
|
i2 {:name ::i2, :compile (constantly nil)}
|
|
i3 (interceptor ::i3)
|
|
router (interceptor/router
|
|
["/api" {:interceptors [i1 i2 i3 i2]
|
|
:handler handler}])
|
|
app (create-app router)]
|
|
|
|
(is (= [::enter_i1 ::enter_i3 :ok ::leave_i3 ::leave_i1] (app "/api")))
|
|
|
|
(testing "routes contain list of actually applied interceptors"
|
|
(is (= [::i1 ::i3 ::interceptor/handler]
|
|
(->> (r/compiled-routes router)
|
|
first
|
|
last
|
|
:interceptors
|
|
(map :name)))))
|
|
|
|
(testing "match contains list of actually applied interceptors"
|
|
(is (= [::i1 ::i3 ::interceptor/handler]
|
|
(->> "/api"
|
|
(r/match-by-path router)
|
|
:result
|
|
:interceptors
|
|
(map :name))))))))))
|
|
|
|
(deftest chain-test
|
|
(testing "chain can produce interceptor chain of any IntoInterceptor"
|
|
(let [i1 {:compile (constantly (interceptor ::i1))}
|
|
i2 {:compile (constantly nil)}
|
|
i3 (interceptor ::i3)
|
|
i4 (interceptor ::i4)
|
|
i5 {:compile (fn [{:keys [mount?]} _]
|
|
(when mount?
|
|
(interceptor ::i5)))}
|
|
chain1 (interceptor/chain [i1 i2 i3 i4 i5 handler] {:mount? true})
|
|
chain2 (interceptor/chain [i1 i2 i3 i4 i5 handler] {:mount? false})
|
|
chain3 (interceptor/chain [i1 i2 i3 i4 i5] {:mount? false})]
|
|
(is (= [::enter_i1 ::enter_i3 ::enter_i4 ::enter_i5 :ok ::leave_i5 ::leave_i4 ::leave_i3 ::leave_i1] (execute chain1 ctx)))
|
|
(is (= [::enter_i1 ::enter_i3 ::enter_i4 :ok ::leave_i4 ::leave_i3 ::leave_i1] (execute chain2 ctx)))
|
|
(is (= [::leave_i4 ::leave_i3 ::leave_i1] (execute chain3 ctx))))))
|
|
|
|
(deftest interceptor-transform-test
|
|
(let [debug-i (enter ::debug)
|
|
create (fn [options]
|
|
(create-app
|
|
(interceptor/router
|
|
["/ping" {:interceptors [(enter ::olipa)
|
|
(enter ::kerran)
|
|
(enter ::avaruus)]
|
|
:handler handler}]
|
|
options)))
|
|
inject-debug (interceptor/transform-butlast #(interleave % (repeat debug-i)))
|
|
sort-interceptors (interceptor/transform-butlast (partial sort-by :name))]
|
|
|
|
(testing "by default, all interceptors are applied in order"
|
|
(let [app (create nil)]
|
|
(is (= [::olipa ::kerran ::avaruus :ok] (app "/ping")))))
|
|
|
|
(testing "interceptors can be re-ordered"
|
|
(let [app (create {::interceptor/transform sort-interceptors})]
|
|
(is (= [::avaruus ::kerran ::olipa :ok] (app "/ping")))))
|
|
|
|
(testing "adding debug interceptor between interceptors"
|
|
(let [app (create {::interceptor/transform inject-debug})]
|
|
(is (= [::olipa ::debug ::kerran ::debug ::avaruus ::debug :ok] (app "/ping")))))
|
|
|
|
(testing "vector of transformations"
|
|
(let [app (create {::interceptor/transform [inject-debug
|
|
sort-interceptors]})]
|
|
(is (= [::avaruus ::debug ::debug ::debug ::kerran ::olipa :ok] (app "/ping")))))))
|