diff --git a/test/duct/reitit/middleware_test.clj b/test/duct/reitit/middleware_test.clj deleted file mode 100644 index c95d9f2..0000000 --- a/test/duct/reitit/middleware_test.clj +++ /dev/null @@ -1,160 +0,0 @@ -(ns duct.reitit.middleware-test - (:require [duct.reitit.middleware] - [clojure.test :refer [is are deftest testing]] - [integrant.core :as ig :refer [init-key]] - [reitit.ring :as ring] - [duct.reitit.util :refer [compact spy]] - [reitit.coercion.spec :as coercion.spec] - [clojure.string :as str]) - (:import [clojure.lang PersistentArrayMap] - [java.util UUID] - [java.util Date] - [java.lang String])) - -(defn- new-router [routes config] - (ring/router routes {:data config})) - -(defn- new-middleware [cfg] - (init-key :duct.reitit/middleware cfg)) - -(defn- count-result [res] - (-> res :reitit.core/match :result compact count)) - -(deftest default-middleware-behavior - (let [middleware (new-middleware {:munntaja false :coercion nil}) - routes [["/identity" identity] - ["/identity-get" {:get identity}] - ["/user/:id" {:get identity}] - ["/err" #(throw (ex-info "error" {}))]] - router (new-router routes {:middleware middleware :environment {:name "tami5"}}) - app (ring/ring-handler router)] - - (testing "Match result" - (are [m req c] (= c (count-result (app (assoc req :request-method m)))) - :get {:uri "/identity"} 9 ;; Creates the same handler for all http methods. - :get {:uri "/identity-get"} 2)) ;; Creates get and options handler - - (testing "Request Keys" - (let [request (app {:request-method :get :uri "/identity"})] - (is (= [:form-params ;; .. - :id ;; Request id [environment-middleware] - :name ;; injected key [environment-middleware] - :params ;; Merge of all types of params. NOTE: Doesn't seem accurate. - :path-params ;; .. - :query-params ;; .. - :request-method ;; Request method - :start-date ;; Request Start date [environment-middleware] - :uri ;; .. - :reitit.core/match ;; Matches - :reitit.core/router] ;; Reitit Router - (sort (keys request)))) - (are [path expected-type] (= expected-type (type (get-in request path))) - [:id] UUID ;; Random UUID to the request injected by [environment-middleware] - [:start-date] Date ;; Start date injected by [environment-middleware] - [:name] String ;; injected key by [environment-middleware] - [:params] PersistentArrayMap))))) ;; A marge of all params types injected by [parameters-middleware] -(defn is-int [str] (try (Integer/parseInt str) (catch Exception _ nil))) - -(deftest coercion-middleware-behavior - (let [middleware (new-middleware {:munntaja false :coercion {}}) - base {:get {:coercion coercion.spec/coercion - :parameters {:path {:company #(and (string? %) - (not (is-int %))) - :user-id int?}} - :handler identity}} - routes [["/identity/:company/users/:user-id" base] - ["/:company/users/:user-id" - (-> base - (assoc-in [:get :response] - {200 {:body #(and (string? %) (str/includes? % "github"))}}) - (assoc-in [:get :handler] - (fn [{{:keys [db]} :environment - {{:keys [user-id company]} :path} :parameters}] - {:status 200 :body (format "%s works at %s" (get-in db [:users user-id :user-name]) company)})))]] - environment {:db {:companies {1 {:company "github"}} - :users {1 {:company 1 :user-name "tami5"}}}} - router (new-router routes {:middleware middleware :environment environment}) - app (ring/ring-handler router)] - - (testing "Request Keys" - (let [request (app {:request-method :get :uri "/identity/github/users/1"})] - (is (= {:user-id 1 :company "github"} (:path (:parameters request))) - "Should place path arguments inside parameters") - - (are [path expected-type] (= expected-type (type (get-in request path))) - [:id] UUID ;; Random UUID to the request injected by environment-middleware - [:start-date] Date ;; Start date injected by environment-middleware - [:db] PersistentArrayMap ;; injected key by [environment-middleware] - [:params] PersistentArrayMap))) - - (testing "Coercion Spec Response" - (let [request (app {:request-method :get :uri "/github/users/1"})] - (is (= 200 (:status request))) - (is (string? (:body request))))) - - (testing "Coercion Spec Exception" - (let [requesta (app {:request-method :get :uri (str "/identity/" 32 "/users/" 1)}) - requestb (app {:request-method :get :uri (str "/identity/company/users/tami")}) - requestc (app {:request-method :get :uri "/apple/users/1"})] - - (is (= [:spec ;; the actual spec? - :problems ;; Problems, where to look to understand the issue? - :type ;; :reitit.coercion/request-coercion? - :coercion ;; coercion type used? - :value ;; value checked {:company "32" :user-id "1"} - :in] ;; path to where the spec validation fail? - (keys (:body requesta))) - "Should provide the following keys in body") - - (testing "Parameters validation" - (is (= 400 (:status requesta)) "Should result in 400 status") - (comment "get-in :body :problems" [{:path [:company] :pred ":clojure.spec.alpha/unknown" :val "32" :via [:spec$57761/company] :in [:company]}]) - - (is (= 400 (:status requestb)) "Should result in 400 status") - (comment "get-in :body :problems" [{:path [:user-id] :pred "clojure.core/int?" :p:val "tami" :p:via [:spec$59018/user-id] :p:in [:user-id]}])) - - (testing "Response Validation" - (is (= 200 (:status requestc))) - (is (string? (:body requestc))) - ;; doesn't fail even thoguh the body won't pass the test - (is (not (str/includes? (:body requestc) "github")))))) - - (testing "Coercion Pretty Exception" - (let [middleware (new-middleware {:munntaja false :coercion {} :logging {:pretty? true - :types [:coercion]}}) - app (->> {:middleware middleware :environment environment} - (new-router routes) - (ring/ring-handler)) - request {:request-method :get :uri (str "/identity/company/users/tami")}] - - (is (= [:reitit.ring.middleware.parameters/parameters - :duct.reitit.middleware/environment-middleware - :reitit.ring.middleware.exception/exception - :reitit.ring.coercion/coerce-request - :reitit.ring.coercion/coerce-response] - (mapv :name middleware)) - "Shouldn't include coerce-exceptions") - - (is (str/includes? (with-out-str (app request)) "-- Spec failed --------------------") - "Should only print to stdout and not return it"))))) - -(deftest custom-error-handling - (let [exception {java.lang.NullPointerException - (fn [_ r] - {:status 500 - :cause "No parameters received" - :uri (:uri r)}) - java.lang.ArithmeticException - (fn [e r] - {:status 500 - :cause (ex-message e) - :data (:body-params r) - :uri (:uri r)})} - middleware (new-middleware {:munntaja false :coercion nil :exception exception}) - router (new-router [["/math" (fn [{{:keys [lhs rhs]} :body-params}] (/ lhs rhs))]] {:middleware middleware}) - handler (ring/ring-handler router) - math-response (handler {:request-method :get :uri "/math" :body-params {:lhs 5 :rhs 0}}) - no-params-response (handler {:request-method :get :uri "/math"})] - (is (= "Divide by zero" (:cause math-response))) - (is (= {:lhs 5, :rhs 0} (:data math-response))) - (is (= "No parameters received" (:cause no-params-response))))) diff --git a/test/duct/reitit_test.clj b/test/duct/reitit_test.clj index 4bb8667..532c99f 100644 --- a/test/duct/reitit_test.clj +++ b/test/duct/reitit_test.clj @@ -13,174 +13,130 @@ (core/load-hierarchy) +(derive :duct.reitit/logging ::logging) +(derive :duct.reitit/coercion ::coercion) +(derive :duct.module/reitit ::module) +(derive :duct.router/reitit ::router) +(derive :foo/database :duct/const) +(derive :foo/index-path :duct/const) + (defn- new-config-handling [base & [profiles]] (let [config (core/build-config base profiles) - in-config (partial get-in config) - in-options #(in-config (cons :duct.reitit/options (if (vector? %) % [%])))] - [config in-options])) - -(deftest configuration-handling - (testing "Configuration keys and values" - (let [base {:duct.module/reitit {} - :duct.profile/base {:duct.core/project-ns 'foo}} - config (core/prep-config base)] - - ;; Reitit Module keys used for futher processing - (is (= [:options :registry :middleware :logging] - (->> (keys config) - (filterv #(= "duct.reitit" (namespace %))) - (mapv #(keyword (name %)))))) - - (are [key value] (= value (key config)) - ;; Defaulhandler middleware namespace - :duct.core/handler-ns 'handler - - ;; Default middleware namespace - :duct.core/middleware-ns 'middleware - - ;; Configuration Pass it reitit router to initialize it - :duct.router/reitit {:routes nil, - :middleware (ig/ref :duct.reitit/middleware) - :registry (ig/ref :duct.reitit/registry) - :options (ig/ref :duct.reitit/options) - :namespaces ["foo.handler" "foo.middleware"]} - - ;; Configuration Pass it ring handler to initialize it - :duct.handler/root {:router (ig/ref :duct.router/reitit) - :options (ig/ref :duct.reitit/options)})) + in-config (partial get-in config)] + #(in-config (cons :duct.reitit/options (if (vector? %) % [%]))))) - (testing "Default Environment Options" - (let [base {:duct.profile/dev {} - :duct.profile/base - {:duct.core/project-ns 'foo} - :duct.module/reitit {}} - [_ in-options] (new-config-handling base)] - (are [path value] (-> path in-options (= value)) - [:logging :exceptions?] true ;; default types supported by default - [:logging :pretty?] false ;; No pretty logging by default. - [:logging :logger] nil ;; No logger by default. - :muuntaja true ;; Muuntaja formatting is enabled by default - :environment {} ;; Empty Environment - :middleware [] ;; Empty Middleware - :coercion nil))) ;; no :coercion configuration - - (testing "Default Development Environment Options" - (let [base {:duct.profile/dev {} - :duct.profile/base - {:duct.core/project-ns 'foo} - :duct.module/reitit {}} - [_ in-options] (new-config-handling base [:duct.profile/dev])] - (are [path value] (-> path in-options (= value)) - [:logging :exceptions?] true ;; default types supported by default - [:logging :coercions?] true ;; default types supported by default - [:logging :requests?] true ;; default types supported by default - [:logging :pretty?] true ;; pretty logging by default. - [:logging :logger] nil ;; No logger by default. - :muuntaja true ;; Muuntaja formatting is enabled by default - :environment {} ;; Empty Environment - :middleware [] ;; Empty Middleware - [:cross-origin :methods] [:get :post :delete :options]) ;; Cross-origin methods - (is (= ".*" (str (in-options [:cross-origin :origin 0])))))) ;; Cross-origin origin allowed - - (testing "Default Production Environment Options" - (let [base {:duct.profile/prod {} - :duct.profile/base - {:duct.core/project-ns 'foo} - :duct.module/reitit {}} - [_ in-options] (new-config-handling base [:duct.profile/prod])] - (are [path value] (-> path in-options (= value)) - [:logging :requests?] true ;; default types supported by default - [:logging :coercions?] false ;; default types supported by default - [:logging :exceptions?] false ;; default types supported by default - [:logging :pretty?] false ;; No pretty logging by default. - [:logging :logger] nil ;; No logger by default. - :muuntaja true ;; Muuntaja formatting is enabled by default - :environment {} ;; Empty Environment - :middleware [] ;; Empty Middleware - :cross-origin nil))))) ;; No Cross-origin +(defn- request [method uri req] + (merge req {:request-method method :uri uri})) -(derive :foo/database :duct/const) -(derive :foo/index-path :duct/const) +(defn- routes [router] + (reduce (fn [acc [k v]] (assoc acc k v)) {} (r/routes router))) (def base-config - {:duct.core/project-ns 'foo - - ;; Where should handlers keys be localated - :duct.core/handler-ns 'handler - - ;; Where should middleware keys be located - :duct.core/middleware-ns 'middleware + {:duct.module/reitit {} + :duct.module/logging {} + :duct.profile/base {:duct.core/project-ns 'foo}}) - ;; Routes Configuration - :duct.reitit/routes [["/" :index] +(def test-config + {:duct.core/project-ns 'foo + :duct.core/handler-ns 'handler ;; Where should handlers keys be localated + :duct.core/middleware-ns 'middleware ;; Where should middleware keys be located + :duct.reitit/routes [["/" :index] ;; Routes Configuration ["/author" :get-author] ["/ping" {:get {:handler :ping}}] ["/plus" {:post :plus/with-body :get 'plus/with-query}] ["/divide" {:get :divide}]] - - ;; Registry to find handlers and local and global middleware :duct.reitit/registry {:index {:path (ig/ref :foo/index-path)} ;; init foo.handler/index with {:path} :ping {:message "pong"} ;; init foo.handler/ping with {:message} :plus/with-body {} ;; init foo.handler.plus/with-body :get-author {} ;; init foo.handler/get-author :divide {}} ;; init foo.handler/divide - - ;; Enable exception handlers through a map of class/types and their response function :duct.reitit/exception (ig/ref :foo.handler/exceptions) - ;; System specific keys + :duct.reitit/environment {:db (ig/ref :foo/database)} :foo/database [{:author "tami5"}] :foo/index-path "resources/index.html" :foo.handler/exceptions {}}) (defn- with-base-config [config] - {:duct.module/reitit {} - :duct.module/logging {} - :duct.profile/base (merge base-config config)}) - -(def reitit-module-config - {:duct.logger/timbre {:set-root-config? true :level :trace} - - ;; Logging Configuration - :duct.reitit/logging {:enable true - :logger (ig/ref :duct/logger) ;; Logger to be used in reitit module. - :exceptions? true - :coercions? true - :pretty? true} - - ;; Whether to use muuntaja for formatting. default true, can be a modified instance of muuntaja. - :duct.reitit/muuntaja true - - ;; Keywords to be injected in requests for convenience. - :duct.reitit/environment {:db (ig/ref :foo/database)} - - ;; Global middleware to be injected. expected registry key only - :duct.reitit/middleware [] - - ;; Exception handling configuration - :duct.reitit/exception (ig/ref :foo.handler/exceptions) - - ;; Coercion configuration - :duct.reitit/coercion {:enable true - :coercer 'spec - :with-formatted-message? true} ; Coercer to be used - - ;; Cross-origin configuration, the following defaults in for dev and local profile - :duct.reitit/cross-origin {:origin [#".*"] ;; What origin to allow. - :methods [:get :post :delete :options]}}) ;; Which methods to allow. - -(defn- routes [router] - (reduce (fn [acc [k v]] (assoc acc k v)) {} (r/routes router))) + (->> config + (merge test-config) + (assoc base-config :duct.profile/base))) (defn- init "Takes reitit options and merge it to base-config for testing" [config] - (-> config with-base-config core/prep-config ig/init)) + (-> config with-base-config core/prep-config ig/init)) -(deftest module-test - (let [config (-> reitit-module-config with-base-config core/prep-config ig/init)] - (testing "Init Result" - (is (map? config))) +(deftest test-default-base-options + (let [config (core/prep-config base-config) + in-options (new-config-handling base-config)] + + ;; Reitit Module keys used for futher processing + (is (->> (keys config) + (filterv #(= "duct.reitit" (namespace %))) + (mapv #(keyword (name %))) + (= [:options :registry :middleware :logging]))) + + (are [key value] (= value (key config)) + ;; Defaulhandler middleware namespace + :duct.core/handler-ns 'handler + ;; Default middleware namespace + :duct.core/middleware-ns 'middleware + ;; Configuration Pass it reitit router to initialize it + :duct.router/reitit {:routes nil, + :middleware (ig/ref :duct.reitit/middleware) + :registry (ig/ref :duct.reitit/registry) + :options (ig/ref :duct.reitit/options) + :namespaces ["foo.handler" "foo.middleware"]} + ;; Configuration Pass it ring handler to initialize it + :duct.handler/root {:router (ig/ref :duct.router/reitit) + :options (ig/ref :duct.reitit/options)}) + + ;; Configuration Values + (are [path value] (= (in-options path) value) + [:logging :exceptions?] true ;; default types supported by default + [:logging :pretty?] false ;; No pretty logging by default. + [:logging :logger] nil ;; No logger by default. + :muuntaja true ;; Muuntaja formatting is enabled by default + :environment {} ;; Empty Environment + :middleware [] ;; Empty Middleware + :coercion nil))) ;; no :coercion configuration + +(deftest test-default-dev-options + (let [in-options (-> (assoc base-config :duct.profile/dev {}) + (new-config-handling [:duct.profile/dev]))] + (are [path value] (= (in-options path) value) + [:logging :exceptions?] true ;; default types supported by default + [:logging :coercions?] true ;; default types supported by default + [:logging :requests?] true ;; default types supported by default + [:logging :pretty?] true ;; pretty logging by default. + [:logging :logger] nil ;; No logger by default. + :muuntaja true ;; Muuntaja formatting is enabled by default + :environment {} ;; Empty Environment + :middleware [] ;; Empty Middleware + [:cross-origin :methods] [:get :post :delete :options]) ;; Cross-origin methods + (is (= ".*" (str (in-options [:cross-origin :origin 0])))))) ;; Cross-origin origin allowed + +(deftest test-default-prod-options + (let [in-options (-> (assoc base-config :duct.profile/prod {}) + (new-config-handling [:duct.profile/prod]))] + (are [path value] (= (in-options path) value) + [:logging :requests?] true ;; default types supported by default + [:logging :coercions?] false ;; default types supported by default + [:logging :exceptions?] false ;; default types supported by default + [:logging :pretty?] false ;; No pretty logging by default. + [:logging :logger] nil ;; No logger by default. + :muuntaja true ;; Muuntaja formatting is enabled by default + :environment {} ;; Empty Environment + :middleware [] ;; Empty Middleware + :cross-origin nil))) ;; No Cross-origin + +(deftest test-foo-module + (let [extra {::coercion {:enable true :coercer 'spec}} + config (-> extra with-base-config core/prep-config ig/init) + router (config :duct.router/reitit) + routes (routes router) + handler (config :duct.handler/root)] (testing "Registry Merge" (are [x] (not= nil (x config)) @@ -189,38 +145,33 @@ :foo.handler.plus/with-body)) (testing "Resulting Router" - (let [router (config :duct.router/reitit) - routes (routes router)] - (is (= :reitit.core/router (type router))) - (is (= 5 (count routes))) - (is (vector? (-> (get routes "/") :environment :db))) - (are [route path] (fn? (get-in (r/match-by-path router route) path)) - "/" [:data :handler] - "/ping" [:data :get :handler] - "/plus" [:data :get :handler] - "/plus" [:data :post :handler] - "/author" [:data :handler]))) - - (testing "Resulting Ring Handler" - (let [handler (:duct.handler/root config)] - (is (fn? handler)) - - (testing "Ring Routing" - (is (nil? (handler {:request-method :get :uri "/not-a-route"}))) - (is (string? (:body (handler {:request-method :get :uri "/"})))) - (is (= "pong" (-> {:request-method :get :uri "/ping"} handler to-edn :message))) - (is (= 9 (-> {:request-method :post :uri "/plus" :body-params {:y 3 :x 6}} handler to-edn :total))) - (is (= 9 (-> {:request-method :get :uri "/plus" :query-params {:y 3 :x 6}} handler to-edn :total)))) - - (testing "Environment-Keys-Access" - (is (= "tami5" (-> {:request-method :get :uri "/author"} handler to-edn :author)))))))) - -; (derive :duct.reitit :duct.reitit-test) -(derive :duct.reitit/logging ::logging) -(derive :duct.reitit/coercion ::coercion) - -(defn- request [method uri req] - (merge req {:request-method method :uri uri})) + (is (= :reitit.core/router (type router))) + (is (= 5 (count routes))) + (is (vector? (-> (get routes "/") :environment :db))) + (are [route path] (fn? (get-in (r/match-by-path router route) path)) + "/" [:data :handler] + "/ping" [:data :get :handler] + "/plus" [:data :get :handler] + "/plus" [:data :post :handler] + "/author" [:data :handler]) + + (is (nil? (handler {:request-method :get :uri "/not-a-route"}))) + + (are [method uri extra-req-params body-path val] + (-> (request method uri extra-req-params) handler to-edn (get-in body-path) (= val) is) + :get "/ping" {} [:message] "pong" + :post "/plus" {:body-params {:y 3 :x 6}} [:total] 9 + :get "/plus" {:query-params {:y 3 :x 6}} [:total] 9 + :get "/author" {} [:author] "tami5")) + + (testing "Custom Error Handling Repsonse" + (let [divide-by-zero-response (to-edn (handler (request :get "/divide" {:body-params {:y 0 :x 0}}))) + no-params-response (to-edn (handler (request :get "/divide" {})))] + + (is (= "Divide by zero" (:cause divide-by-zero-response))) + (is (= {:y 0 :x 0} (:data divide-by-zero-response))) + (is (= {:y 0 :x 0} (:data divide-by-zero-response))) + (is (= "No parameters received" (:cause no-params-response))))))) (deftest module-behavior (testing "Logging:"