(ns reitit.core-test (:require [clojure.test :refer [are deftest is testing]] [reitit.core :as r #?@(:cljs [:refer [Router]])] [reitit.impl :as impl]) #?(:clj (:import (clojure.lang ExceptionInfo) (reitit.core Router)))) (defn- var-handler [& _] "var-handler") (deftest reitit-test (testing "routers handling wildcard paths" (are [r name] (testing (str name) (testing "simple" (let [router (r/router ["/api" ["/ipa" ["/:size" ::beer]]] {:router r})] (is (= name (r/router-name router))) (is (= [["/api/ipa/:size" {:name ::beer}]] (r/routes router))) (is (map? (r/options router))) (is (= nil (r/match-by-path router "/api"))) (is (= (r/map->Match {:template "/api/ipa/:size" :data {:name ::beer} :path "/api/ipa/large" :path-params {:size "large"}}) (r/match-by-path router "/api/ipa/large"))) (is (= (r/map->Match {:template "/api/ipa/:size" :data {:name ::beer} :path "/api/ipa/large" :path-params {:size "large"}}) (r/match-by-name router ::beer {:size "large"}))) (is (= (r/map->Match {:template "/api/ipa/:size" :data {:name ::beer} :path "/api/ipa/large" :path-params {:size "large"}}) (r/match-by-name router ::beer {:size :large}))) (is (= nil (r/match-by-name router "ILLEGAL"))) (is (= [::beer] (r/route-names router))) (testing "name-based routing with missing parameters" (is (= (r/map->PartialMatch {:template "/api/ipa/:size" :data {:name ::beer} :required #{:size} :path-params nil}) (r/match-by-name router ::beer))) (is (r/partial-match? (r/match-by-name router ::beer))) (is (r/partial-match? (r/match-by-name router ::beer {:size nil})) "nil counts as missing") (is (thrown-with-msg? ExceptionInfo #"^missing path-params for route /api/ipa/:size -> \#\{:size\}$" (r/match-by-name! router ::beer))) (is (thrown-with-msg? ExceptionInfo #"^missing path-params for route /api/ipa/:size -> \#\{:size\}$" (r/match-by-name! router ::beer {:size nil})) "nil counts as missing")))) (testing "decode %-encoded path params" (let [router (r/router [["/one-param-path/:param1" ::one] ["/two-param-path/:param1/:param2"] ["/catchall/*remaining-path"]] {:router r}) decoded-params #(-> router (r/match-by-path %) :path-params) decoded-param1 #(-> (decoded-params %) :param1) decoded-remaining-path #(-> (decoded-params %) :remaining-path)] (is (= {:param1 "käki"} (:path-params (r/match-by-name router ::one {:param1 "käki"})))) (is (= "/one-param-path/k%C3%A4ki" (:path (r/match-by-name router ::one {:param1 "käki"})))) (is (= "foo bar" (decoded-param1 "/one-param-path/foo%20bar"))) (is (= {:param1 "foo bar" :param2 "baz qux"} (decoded-params "/two-param-path/foo%20bar/baz%20qux"))) (is (= "foo bar" (decoded-remaining-path "/catchall/foo%20bar"))) (is (= "!#$&'()*+,/:;=?@[]" (decoded-param1 "/one-param-path/%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D"))))) (testing "complex" (let [router (r/router [["/:abba" ::abba] ["/abba/1" ::abba2] ["/:jabba/2" ::jabba2] ["/:abba/:dabba/doo" ::doo] ["/abba/dabba/boo/baa" ::baa] ["/abba/:dabba/boo" ::boo] ["/:jabba/:dabba/:doo/:daa/*foo" ::wild]] {:router r}) by-path #(-> router (r/match-by-path %) :data :name)] (is (= ::abba (by-path "/abba"))) (is (= ::abba2 (by-path "/abba/1"))) (is (= ::jabba2 (by-path "/abba/2"))) (is (= ::doo (by-path "/abba/1/doo"))) (is (= ::boo (by-path "/abba/1/boo"))) (is (= ::baa (by-path "/abba/dabba/boo/baa"))) (is (= ::boo (by-path "/abba/dabba/boo"))) (is (= ::wild (by-path "/olipa/kerran/avaruus/vaan/"))) (is (= ::wild (by-path "/olipa/kerran/avaruus/vaan/ei/toista/kertaa"))))) (testing "bracket-params" (testing "successful" (let [router (r/router [["/{abba}" ::abba] ["/abba/1" ::abba2] ["/{jabba}/2" ::jabba2] ["/{abba}/{dabba}/doo" ::doo] ["/abba/dabba/boo/baa" ::baa] ["/abba/{dabba}/boo" ::boo] ["/{a/jabba}/{a.b/dabba}/{a.b.c/doo}/{a.b.c.d/daa}/{*foo/bar}" ::wild] ["/files/file-{name}.html" ::html] ["/files/file-{name}.json" ::json] ["/{eskon}/{saum}/pium\u2215paum" ::loru] ["/{🌈}🤔/🎈" ::emoji] ["/extra-end}s-are/ok" ::bracket]] {:router r}) by-path #(-> router (r/match-by-path %) ((juxt (comp :name :data) :path-params)))] (is (= [::abba {:abba "abba"}] (by-path "/abba"))) (is (= [::abba2 {}] (by-path "/abba/1"))) (is (= [::jabba2 {:jabba "abba"}] (by-path "/abba/2"))) (is (= [::doo {:abba "abba", :dabba "1"}] (by-path "/abba/1/doo"))) (is (= [::boo {:dabba "1"}] (by-path "/abba/1/boo"))) (is (= [::baa {}] (by-path "/abba/dabba/boo/baa"))) (is (= [::boo {:dabba "dabba"}] (by-path "/abba/dabba/boo"))) (is (= [::wild {:a/jabba "olipa" :a.b/dabba "kerran" :a.b.c/doo "avaruus" :a.b.c.d/daa "vaan" :foo/bar "ei/toista/kertaa"}] (by-path "/olipa/kerran/avaruus/vaan/ei/toista/kertaa"))) (is (= [::html {:name "10"}] (by-path "/files/file-10.html"))) (is (= [nil nil] (by-path "/files/file-..html"))) (is (= [::loru {:eskon "viitan", :saum "aa"}] (by-path "/viitan/aa/pium\u2215paum"))) (is (= [nil nil] (by-path "/ei/osu/pium/paum"))) (is (= [::emoji {:🌈 "brackets"}] (by-path "/brackets🤔/🎈"))) (is (= [::bracket {}] (by-path "/extra-end}s-are/ok"))))) (testing "invalid syntax fails fast" (testing "unclosed brackets" (is (thrown-with-msg? ExceptionInfo #":reitit.trie/unclosed-brackets" (r/router ["/kikka/{kukka"])))) (testing "multiple terminators" (is (thrown-with-msg? ExceptionInfo #":reitit.trie/multiple-terminators" (r/router [["/{kukka}.json"] ["/{kukka}-json"]])))))) (testing "empty path segments" (let [router (r/router [["/items" ::list] ["/items/:id" ::item] ["/items/:id/:side" ::deep]] {:router r}) matches #(-> router (r/match-by-path %) :data :name)] (is (= ::list (matches "/items"))) (is (= nil (matches "/items/"))) (is (= ::item (matches "/items/1"))) (is (= ::deep (matches "/items/1/2"))) (is (= nil (matches "/items//2"))) (is (= nil (matches "")))))) r/linear-router :linear-router r/trie-router :trie-router r/mixed-router :mixed-router r/quarantine-router :quarantine-router)) (testing "routers handling static paths" (are [r name] (let [router (r/router ["/api" ["/ipa" ["/large" ::beer]]] {:router r})] (is (= name (r/router-name router))) (is (= [["/api/ipa/large" {:name ::beer}]] (r/routes router))) (is (map? (r/options router))) (is (= nil (r/match-by-path router "/api"))) (is (= (r/map->Match {:template "/api/ipa/large" :data {:name ::beer} :path "/api/ipa/large" :path-params {}}) (r/match-by-path router "/api/ipa/large"))) (is (= (r/map->Match {:template "/api/ipa/large" :data {:name ::beer} :path "/api/ipa/large" :path-params {:size "large"}}) (r/match-by-name router ::beer {:size "large"}))) (is (= nil (r/match-by-name router "ILLEGAL"))) (is (= [::beer] (r/route-names router))) (testing "can't be created with wildcard routes" (is (thrown-with-msg? ExceptionInfo #"can't create :lookup-router with wildcard routes" (r/lookup-router (impl/resolve-routes ["/api/:version/ping"] (r/default-router-options))))))) r/lookup-router :lookup-router r/single-static-path-router :single-static-path-router r/linear-router :linear-router r/trie-router :trie-router r/mixed-router :mixed-router r/quarantine-router :quarantine-router)) (testing "nil routes are stripped" (is (= [] (r/routes (r/router nil)))) (is (= [] (r/routes (r/router [nil ["/ping"]])))) (is (= [] (r/routes (r/router [nil [nil] [[nil nil nil]]])))) (is (= [] (r/routes (r/router ["/ping" [nil "/pong"]]))))) (testing "route coercion & compilation" (testing "custom compile" (let [compile-times (atom 0) coerce (fn [[path data] _] (if-not (:invalid? data) [path (assoc data :path path)])) compile (fn [[path data] _] (swap! compile-times inc) (constantly path)) router (r/router ["/api" {:roles #{:admin}} ["/ping" ::ping] ["/pong" ::pong] ["/hidden" {:invalid? true} ["/utter"] ["/crap"]]] {:coerce coerce :compile compile})] (testing "routes are coerced" (is (= [["/api/ping" {:name ::ping :path "/api/ping", :roles #{:admin}}] ["/api/pong" {:name ::pong :path "/api/pong", :roles #{:admin}}]] (r/routes router)))) (testing "route match contains compiled handler" (is (= 2 @compile-times)) (let [{:keys [result]} (r/match-by-path router "/api/pong")] (is result) (is (= "/api/pong" (result))) (is (= 2 @compile-times)))))) (testing "default compile" (let [router (r/router ["/ping" (constantly "ok")]) {:keys [result]} (r/match-by-path router "/ping")] (is result) (is (= "ok" (result)))) (testing "var handler gets expanded" (let [router (r/router ["/ping" #'var-handler]) {:keys [result]} (r/match-by-path router "/ping")] (is (= #'var-handler result)))))) (testing "custom router" (let [router (r/router ["/ping"] {:router (fn [_ _] (reify Router (r/router-name [_] ::custom)))})] (is (= ::custom (r/router-name router))))) (testing "bide sample" (let [routes [["/auth/login" :auth/login] ["/auth/recovery/token/:token" :auth/recovery] ["/workspace/:project-uuid/:page-uuid" :workspace/page]] expected [["/auth/login" {:name :auth/login}] ["/auth/recovery/token/:token" {:name :auth/recovery}] ["/workspace/:project-uuid/:page-uuid" {:name :workspace/page}]]] (is (= expected (impl/resolve-routes routes (r/default-router-options)))))) (testing "ring sample" (let [pong (constantly "ok") routes ["/api" {:mw [:api]} ["/ping" :kikka] ["/user/:id" {:parameters {:path {:id :string}}} ["/:sub-id" {:parameters {:path {:sub-id :string}}}]] ["/pong" pong] ["/admin" {:mw [:admin] :roles #{:admin}} ["/user" {:roles ^:replace #{:user}}] ["/db" {:mw [:db]}]]] expected [["/api/ping" {:mw [:api], :name :kikka}] ["/api/user/:id/:sub-id" {:mw [:api], :parameters {:path [{:id :string} {:sub-id :string}]}}] ["/api/pong" {:mw [:api], :handler pong}] ["/api/admin/user" {:mw [:api :admin], :roles #{:user}}] ["/api/admin/db" {:mw [:api :admin :db], :roles #{:admin}}]] router (r/router routes)] (is (= expected (impl/resolve-routes routes (r/default-router-options)))) (is (= (r/map->Match {:template "/api/user/:id/:sub-id" :data {:mw [:api], :parameters {:path [{:id :string} {:sub-id :string}]}} :path "/api/user/1/2" :path-params {:id "1", :sub-id "2"}}) (r/match-by-path router "/api/user/1/2")))))) (deftest conflicting-routes-test (testing "path conflicts" (are [conflicting? data] (let [routes (impl/resolve-routes data (r/default-router-options)) conflicts (-> routes (impl/resolve-routes (r/default-router-options)) (impl/path-conflicting-routes nil))] (if conflicting? (seq conflicts) (nil? conflicts))) true [["/a"] ["/a"]] true [["/a"] ["/:b"]] true [["/a"] ["/*b"]] true [["/a/1/2"] ["/*b"]] false [["/a"] ["/a/"]] false [["/a"] ["/a/1"]] false [["/a"] ["/a/:b"]] false [["/a"] ["/a/*b"]] true [["/v2/public/messages/dataset/bulk"] ["/v2/public/messages/dataset/:dataset-id"]]) (testing "all conflicts are returned" (is (= {["/a" {}] #{["/*d" {}] ["/:b" {}]}, ["/:b" {}] #{["/c" {}] ["/*d" {}]}, ["/c" {}] #{["/*d" {}]}} (-> [["/a"] ["/:b"] ["/c"] ["/*d"]] (impl/resolve-routes (r/default-router-options)) (impl/path-conflicting-routes nil))))) (testing "router with conflicting routes" (testing "throws by default" (is (thrown-with-msg? ExceptionInfo #"Router contains conflicting route paths" (r/router [["/a"] ["/a"]])))) (testing "can be configured to ignore with route data" (are [paths expected] (let [router (r/router paths)] (is (not (nil? router))) (is (= expected (r/router-name router)))) [["/a" {:conflicting true}] ["/a" {:conflicting true}]] :quarantine-router [["/a" {:conflicting true}] ["/:b" {:conflicting true}] ["/c" {:conflicting true}] ["/*d" {:conflicting true}]] :quarantine-router [["/:a" ["/:b" {:conflicting true}] ["/:c" {:conflicting true}] ["/:d" ["/:e" {:conflicting true}] ["/:f" {:conflicting true}]]]] :quarantine-router [["/:a" {:conflicting true} ["/:b"] ["/:c"] ["/:d" ["/:e"] ["/:f"]]]] :quarantine-router) (testing "unmarked path conflicts throw" (are [paths] (is (thrown-with-msg? ExceptionInfo #"Router contains conflicting route paths" (r/router paths))) [["/a"] ["/a" {:conflicting true}]] [["/a" {:conflicting true}] ["/a"]]))) (testing "can be configured to ignore with router option" (is (not (nil? (r/router [["/a"] ["/a"]] {:conflicts nil}))))))) (testing "name conflicts" (testing "router with conflicting routes always throws" (is (thrown-with-msg? ExceptionInfo #"Router contains conflicting route names" (r/router [["/1" ::1] ["/2" ::1]])))))) (deftest match->path-test (let [router (r/router ["/:a/:b" ::route])] (is (= "/olipa/kerran" (-> router (r/match-by-name! ::route {:a "olipa", :b "kerran"}) (r/match->path)))) (is (= "/olipa/kerran" (-> router (r/match-by-name! ::route {:a "olipa", :b "kerran"}) (r/match->path {})))) (is (= "/olipa/kerran?iso=p%C3%B6ril%C3%A4inen" (-> router (r/match-by-name! ::route {:a "olipa", :b "kerran"}) (r/match->path {:iso "pöriläinen"})))))) (deftest sequential-routes (testing "sequential child routes work" (is (= [["/api/0" {}] ["/api/1" {}]] (-> ["/api" (for [i (range 2)] [(str "/" i)])] (r/router) (r/routes))))) (testing "sequential route definition fails" (is (thrown? #?(:clj Exception, :cljs js/Error) (-> ["/api" (list "/ipa")] (r/router)))))) (defrecord Named [n] r/Expand (r/expand [_ _] {:name n})) (deftest default-expand-test (let [router (r/router ["/endpoint" (Named. :kikka)])] (is (= [["/endpoint" {:name :kikka}]] (r/routes router))))) (deftest routing-order-test-229 (let [router (r/router [["/" :root] ["/" {:name :create :method :post}]] {:conflicts nil}) router2 (r/router [["/*a" :root] ["/:a/b/c/d" {:name :create :method :post}]] {:conflicts nil})] (is (= :root (-> (r/match-by-path router "/") :data :name))) (is (= :root (-> (r/match-by-path router2 "/") :data :name))))) (deftest routing-bug-test-538 (let [router (r/router [["/:a"] ["/:b"]] {:conflicts nil})] (is (nil? (r/match-by-path router ""))))) (deftest metadata-regression-679 (is (= ["/context/leaf" {:roles {:foo true}}] (-> ["/context" {:roles {:foo false :bar true}} ["/leaf" {:roles ^:replace {:foo true}}]] (r/router) (r/routes) (first)))))