reitit/test/cljc/reitit/interceptor_test.cljc
2023-01-21 10:58:53 +02:00

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")))))))