2018-06-08 13:00:49 +00:00
|
|
|
(ns reitit.frontend.core-test
|
|
|
|
|
(:require [clojure.test :refer [deftest testing is are]]
|
|
|
|
|
[reitit.core :as r]
|
|
|
|
|
[reitit.frontend :as rf]
|
2018-07-11 07:26:53 +00:00
|
|
|
[reitit.coercion :as rc]
|
2018-06-08 13:00:49 +00:00
|
|
|
[schema.core :as s]
|
2023-01-18 18:24:33 +00:00
|
|
|
[reitit.coercion.schema :as rcs]
|
|
|
|
|
[reitit.coercion.malli :as rcm]
|
2023-03-24 11:59:02 +00:00
|
|
|
[reitit.frontend.test-utils :refer [capture-console]]
|
|
|
|
|
[reitit.impl :as impl]))
|
2018-06-08 13:00:49 +00:00
|
|
|
|
2023-03-24 09:16:09 +00:00
|
|
|
(deftest query-params-test
|
|
|
|
|
(is (= {:foo "1"}
|
|
|
|
|
(rf/query-params (.parse goog.Uri "?foo=1"))))
|
|
|
|
|
|
|
|
|
|
(is (= {:foo "1" :bar "aaa"}
|
|
|
|
|
(rf/query-params (.parse goog.Uri "?foo=1&bar=aaa"))))
|
|
|
|
|
|
|
|
|
|
(is (= {:foo ""}
|
|
|
|
|
(rf/query-params (.parse goog.Uri "?foo="))))
|
|
|
|
|
|
|
|
|
|
(is (= {:foo ""}
|
|
|
|
|
(rf/query-params (.parse goog.Uri "?foo")))))
|
|
|
|
|
|
2018-07-11 06:52:35 +00:00
|
|
|
(defn m [x]
|
|
|
|
|
(assoc x :data nil :result nil))
|
|
|
|
|
|
2023-01-18 18:24:33 +00:00
|
|
|
(defn decode-form [s]
|
|
|
|
|
;; RFC 6749 4.2.2 specifies OAuth token response uses
|
|
|
|
|
;; form-urlencoded format to encode values in the fragment string.
|
|
|
|
|
;; Use built-in JS function to decode.
|
|
|
|
|
;; ring.util.codec/decode-form works on Clj.
|
|
|
|
|
(when s
|
|
|
|
|
(->> (.entries (js/URLSearchParams. s))
|
|
|
|
|
(map (fn [[k v]] [(keyword k) v]))
|
|
|
|
|
(into {}))))
|
|
|
|
|
|
2018-06-08 13:00:49 +00:00
|
|
|
(deftest match-by-path-test
|
|
|
|
|
(testing "simple"
|
|
|
|
|
(let [router (r/router ["/"
|
|
|
|
|
["" ::frontpage]
|
|
|
|
|
["foo" ::foo]
|
|
|
|
|
["bar" ::bar]])]
|
2018-06-12 10:49:24 +00:00
|
|
|
(is (= (r/map->Match
|
|
|
|
|
{:template "/"
|
|
|
|
|
:data {:name ::frontpage}
|
|
|
|
|
:path-params {}
|
2018-08-22 08:01:53 +00:00
|
|
|
:query-params {}
|
2018-06-12 10:49:24 +00:00
|
|
|
:path "/"
|
2023-01-18 18:24:33 +00:00
|
|
|
:fragment nil
|
2018-06-12 10:49:24 +00:00
|
|
|
:parameters {:query {}
|
2023-01-18 18:28:26 +00:00
|
|
|
:path {}
|
2023-01-18 18:24:33 +00:00
|
|
|
:fragment nil}})
|
2018-06-08 13:00:49 +00:00
|
|
|
(rf/match-by-path router "/")))
|
2018-07-11 06:52:35 +00:00
|
|
|
|
|
|
|
|
(is (= "/"
|
|
|
|
|
(r/match->path (rf/match-by-name router ::frontpage))))
|
|
|
|
|
|
2018-06-12 10:49:24 +00:00
|
|
|
(is (= (r/map->Match
|
|
|
|
|
{:template "/foo"
|
|
|
|
|
:data {:name ::foo}
|
|
|
|
|
:path-params {}
|
2018-08-22 08:01:53 +00:00
|
|
|
:query-params {}
|
2018-06-12 10:49:24 +00:00
|
|
|
:path "/foo"
|
2023-01-18 18:24:33 +00:00
|
|
|
:fragment nil
|
2018-06-12 10:49:24 +00:00
|
|
|
:parameters {:query {}
|
2023-01-18 18:28:26 +00:00
|
|
|
:path {}
|
2023-01-18 18:24:33 +00:00
|
|
|
:fragment nil}})
|
2018-07-11 06:52:35 +00:00
|
|
|
(rf/match-by-path router "/foo")))
|
|
|
|
|
|
2019-11-15 10:53:32 +00:00
|
|
|
(is (= (r/map->Match
|
|
|
|
|
{:template "/foo"
|
|
|
|
|
:data {:name ::foo}
|
|
|
|
|
:path-params {}
|
|
|
|
|
:query-params {:mode ["foo", "bar"]}
|
|
|
|
|
:path "/foo"
|
2023-01-18 18:24:33 +00:00
|
|
|
:fragment nil
|
2019-11-15 10:53:32 +00:00
|
|
|
:parameters {:query {:mode ["foo", "bar"]}
|
2023-01-18 18:28:26 +00:00
|
|
|
:path {}
|
2023-01-18 18:24:33 +00:00
|
|
|
:fragment nil}})
|
2019-11-15 10:53:32 +00:00
|
|
|
(rf/match-by-path router "/foo?mode=foo&mode=bar")))
|
|
|
|
|
|
2018-07-11 06:52:35 +00:00
|
|
|
(is (= "/foo"
|
2018-07-11 09:55:16 +00:00
|
|
|
(r/match->path (rf/match-by-name router ::foo))))
|
|
|
|
|
|
2018-08-22 07:47:04 +00:00
|
|
|
(testing "console warning about missing route"
|
|
|
|
|
(is (= [{:type :warn
|
|
|
|
|
:message ["missing route" ::asd]}]
|
|
|
|
|
(:messages
|
2023-01-10 06:05:42 +00:00
|
|
|
(capture-console
|
|
|
|
|
(fn []
|
|
|
|
|
(rf/match-by-name! router ::asd)))))))))
|
2018-06-08 13:00:49 +00:00
|
|
|
|
|
|
|
|
(testing "schema coercion"
|
|
|
|
|
(let [router (r/router ["/"
|
2018-06-12 10:49:24 +00:00
|
|
|
[":id" {:name ::foo
|
|
|
|
|
:parameters {:path {:id s/Int}
|
2023-01-18 18:28:26 +00:00
|
|
|
:query {(s/optional-key :mode) s/Keyword}
|
2023-01-18 18:24:33 +00:00
|
|
|
:fragment (s/maybe s/Str)}}]]
|
2018-07-11 07:26:53 +00:00
|
|
|
{:compile rc/compile-request-coercers
|
2023-01-18 18:24:33 +00:00
|
|
|
:data {:coercion rcs/coercion}})]
|
2018-08-22 07:47:04 +00:00
|
|
|
|
2018-06-12 10:49:24 +00:00
|
|
|
(is (= (r/map->Match
|
|
|
|
|
{:template "/:id"
|
|
|
|
|
:path-params {:id "5"}
|
2018-08-22 08:01:53 +00:00
|
|
|
:query-params {}
|
2018-06-12 10:49:24 +00:00
|
|
|
:path "/5"
|
2023-01-18 18:24:33 +00:00
|
|
|
:fragment nil
|
2018-06-12 10:49:24 +00:00
|
|
|
:parameters {:query {}
|
2023-01-18 18:28:26 +00:00
|
|
|
:path {:id 5}
|
2023-01-18 18:24:33 +00:00
|
|
|
:fragment nil}})
|
2018-07-11 06:52:35 +00:00
|
|
|
(m (rf/match-by-path router "/5"))))
|
|
|
|
|
|
|
|
|
|
(is (= "/5"
|
|
|
|
|
(r/match->path (rf/match-by-name router ::foo {:id 5}))))
|
|
|
|
|
|
2022-04-05 14:33:25 +00:00
|
|
|
(testing "coercion error"
|
|
|
|
|
(testing "throws without options"
|
|
|
|
|
(is (thrown? js/Error (m (rf/match-by-path router "/a")))))
|
|
|
|
|
|
|
|
|
|
(testing "thows and calles on-coercion-error"
|
2022-04-05 14:55:09 +00:00
|
|
|
(let [exception (atom nil)
|
|
|
|
|
match (atom nil)]
|
|
|
|
|
(is (thrown? js/Error (m (rf/match-by-path router "/a" {:on-coercion-error (fn [m e]
|
|
|
|
|
(reset! match m)
|
|
|
|
|
(reset! exception e))}))))
|
|
|
|
|
(is (= {:id "a"} (-> @match :path-params)))
|
2022-04-05 14:33:25 +00:00
|
|
|
(is (= {:id "a"} (-> @exception (ex-data) :value))))))
|
|
|
|
|
|
2018-08-22 07:47:04 +00:00
|
|
|
(testing "query param is read"
|
|
|
|
|
(is (= (r/map->Match
|
|
|
|
|
{:template "/:id"
|
|
|
|
|
:path-params {:id "5"}
|
2018-08-22 08:01:53 +00:00
|
|
|
:query-params {:mode "foo"}
|
2018-08-22 07:47:04 +00:00
|
|
|
:path "/5"
|
2023-01-18 18:24:33 +00:00
|
|
|
:fragment nil
|
2018-08-22 07:47:04 +00:00
|
|
|
:parameters {:path {:id 5}
|
2023-01-18 18:28:26 +00:00
|
|
|
:query {:mode :foo}
|
2023-01-18 18:24:33 +00:00
|
|
|
:fragment nil}})
|
2018-08-22 07:47:04 +00:00
|
|
|
(m (rf/match-by-path router "/5?mode=foo"))))
|
|
|
|
|
|
|
|
|
|
(is (= "/5?mode=foo"
|
|
|
|
|
(r/match->path (rf/match-by-name router ::foo {:id 5}) {:mode :foo}))))
|
2018-07-11 06:52:35 +00:00
|
|
|
|
2023-01-18 18:24:33 +00:00
|
|
|
(testing "fragment string is read"
|
2018-08-22 07:47:04 +00:00
|
|
|
(is (= (r/map->Match
|
|
|
|
|
{:template "/:id"
|
|
|
|
|
:path-params {:id "5"}
|
2018-08-22 08:01:53 +00:00
|
|
|
:query-params {:mode "foo"}
|
2018-08-22 07:47:04 +00:00
|
|
|
:path "/5"
|
2023-01-18 18:24:33 +00:00
|
|
|
:fragment "fragment"
|
2018-08-22 07:47:04 +00:00
|
|
|
:parameters {:path {:id 5}
|
2023-01-18 18:28:26 +00:00
|
|
|
:query {:mode :foo}
|
2023-01-18 18:24:33 +00:00
|
|
|
:fragment "fragment"}})
|
|
|
|
|
(m (rf/match-by-path router "/5?mode=foo#fragment")))))
|
2018-07-11 09:55:16 +00:00
|
|
|
|
2018-08-22 07:47:04 +00:00
|
|
|
(testing "console warning about missing params"
|
|
|
|
|
(is (= [{:type :warn
|
|
|
|
|
:message ["missing path-params for route" ::foo
|
|
|
|
|
{:template "/:id"
|
|
|
|
|
:missing #{:id}
|
|
|
|
|
:required #{:id}
|
|
|
|
|
:path-params {}}]}]
|
|
|
|
|
(:messages
|
2023-01-10 06:05:42 +00:00
|
|
|
(capture-console
|
|
|
|
|
(fn []
|
2023-01-18 18:24:33 +00:00
|
|
|
(rf/match-by-name! router ::foo {})))))))))
|
|
|
|
|
|
|
|
|
|
(testing "malli coercion"
|
|
|
|
|
(let [router (r/router ["/"
|
|
|
|
|
[":id" {:name ::foo
|
|
|
|
|
:parameters {:path [:map
|
|
|
|
|
[:id :int]]
|
|
|
|
|
:query [:map
|
|
|
|
|
[:mode {:optional true} :keyword]]
|
|
|
|
|
:fragment [:maybe
|
|
|
|
|
[:map
|
|
|
|
|
{:decode/string decode-form}
|
|
|
|
|
[:access_token :string]
|
|
|
|
|
[:refresh_token :string]
|
|
|
|
|
[:expires_in :int]
|
|
|
|
|
[:provider_token :string]
|
|
|
|
|
[:token_type :string]]]}}]]
|
|
|
|
|
{:compile rc/compile-request-coercers
|
|
|
|
|
:data {:coercion rcm/coercion}})]
|
|
|
|
|
|
|
|
|
|
(is (= (r/map->Match
|
|
|
|
|
{:template "/:id"
|
|
|
|
|
:path-params {:id "5"}
|
|
|
|
|
:query-params {}
|
|
|
|
|
:path "/5"
|
|
|
|
|
:fragment nil
|
|
|
|
|
:parameters {:query {}
|
|
|
|
|
:path {:id 5}
|
|
|
|
|
:fragment nil}})
|
|
|
|
|
(m (rf/match-by-path router "/5"))))
|
|
|
|
|
|
|
|
|
|
(is (= "/5"
|
|
|
|
|
(r/match->path (rf/match-by-name router ::foo {:id 5}))))
|
|
|
|
|
|
|
|
|
|
(testing "coercion error"
|
|
|
|
|
(testing "throws without options"
|
|
|
|
|
(is (thrown? js/Error (m (rf/match-by-path router "/a")))))
|
|
|
|
|
|
|
|
|
|
(testing "thows and calles on-coercion-error"
|
|
|
|
|
(let [exception (atom nil)
|
|
|
|
|
match (atom nil)]
|
|
|
|
|
(is (thrown? js/Error (m (rf/match-by-path router "/a" {:on-coercion-error (fn [m e]
|
|
|
|
|
(reset! match m)
|
|
|
|
|
(reset! exception e))}))))
|
|
|
|
|
(is (= {:id "a"} (-> @match :path-params)))
|
|
|
|
|
(is (= {:id "a"} (-> @exception (ex-data) :value))))))
|
|
|
|
|
|
|
|
|
|
(testing "query param is read"
|
|
|
|
|
(is (= (r/map->Match
|
|
|
|
|
{:template "/:id"
|
|
|
|
|
:path-params {:id "5"}
|
|
|
|
|
:query-params {:mode "foo"}
|
|
|
|
|
:path "/5"
|
|
|
|
|
:fragment nil
|
|
|
|
|
:parameters {:path {:id 5}
|
|
|
|
|
:query {:mode :foo}
|
|
|
|
|
:fragment nil}})
|
|
|
|
|
(m (rf/match-by-path router "/5?mode=foo"))))
|
|
|
|
|
|
|
|
|
|
(is (= "/5?mode=foo"
|
|
|
|
|
(r/match->path (rf/match-by-name router ::foo {:id 5}) {:mode :foo}))))
|
|
|
|
|
|
|
|
|
|
(testing "fragment string is read"
|
|
|
|
|
(is (= (r/map->Match
|
|
|
|
|
{:template "/:id"
|
|
|
|
|
:path-params {:id "5"}
|
|
|
|
|
:query-params {:mode "foo"}
|
|
|
|
|
:path "/5"
|
|
|
|
|
:fragment "access_token=foo&refresh_token=bar&provider_token=baz&token_type=bearer&expires_in=3600"
|
|
|
|
|
:parameters {:path {:id 5}
|
|
|
|
|
:query {:mode :foo}
|
|
|
|
|
:fragment {:access_token "foo"
|
|
|
|
|
:refresh_token "bar"
|
|
|
|
|
:provider_token "baz"
|
|
|
|
|
:token_type "bearer"
|
|
|
|
|
:expires_in 3600}}})
|
|
|
|
|
(m (rf/match-by-path router "/5?mode=foo#access_token=foo&refresh_token=bar&provider_token=baz&token_type=bearer&expires_in=3600"))))))))
|
2023-03-23 13:36:48 +00:00
|
|
|
|
2023-03-24 09:16:09 +00:00
|
|
|
(deftest set-query-params-test
|
2023-03-23 13:36:48 +00:00
|
|
|
(is (= "foo?bar=1"
|
2023-03-24 09:16:09 +00:00
|
|
|
(rf/set-query-params "foo" {:bar 1})
|
2023-03-24 11:59:02 +00:00
|
|
|
(rf/set-query-params "foo" #(assoc % :bar 1))
|
|
|
|
|
;; Also compare to reitit.impl version which is used by match->path (and history fns)
|
|
|
|
|
(str "foo?" (impl/query-string {:bar 1}))))
|
|
|
|
|
|
|
|
|
|
(testing "Encoding"
|
|
|
|
|
(is (= "foo?bar=foo%20bar"
|
|
|
|
|
(rf/set-query-params "foo" {:bar "foo bar"})
|
|
|
|
|
(rf/set-query-params "foo" #(assoc % :bar "foo bar"))
|
|
|
|
|
;; FIXME: Reitit.impl encodes space as "+"
|
|
|
|
|
; (str "foo?" (impl/query-string {:bar "foo bar"}))
|
|
|
|
|
)))
|
2023-03-23 13:36:48 +00:00
|
|
|
|
2023-03-23 13:46:25 +00:00
|
|
|
(testing "Keep fragment"
|
|
|
|
|
(is (= "foo?bar=1&zzz=2#aaa"
|
2023-03-24 09:16:09 +00:00
|
|
|
(rf/set-query-params "foo?bar=1#aaa" #(assoc % :zzz 2)))))
|
2023-03-23 13:46:25 +00:00
|
|
|
|
2023-03-23 13:36:48 +00:00
|
|
|
(is (= "foo?asd=1&bar=1"
|
2023-03-24 09:16:09 +00:00
|
|
|
(rf/set-query-params "foo?asd=1" #(assoc % :bar 1))))
|
2023-03-23 13:36:48 +00:00
|
|
|
|
|
|
|
|
(is (= "foo?bar=1"
|
2023-03-24 09:16:09 +00:00
|
|
|
(rf/set-query-params "foo?asd=1&bar=1" #(dissoc % :asd))))
|
|
|
|
|
|
|
|
|
|
(is (= "foo?bar"
|
|
|
|
|
(rf/set-query-params "foo?asd=1&bar" #(dissoc % :asd))))
|
|
|
|
|
|
|
|
|
|
(is (= "foo?bar"
|
2023-03-24 11:59:02 +00:00
|
|
|
(rf/set-query-params "foo" #(assoc % :bar ""))
|
|
|
|
|
;; FIXME: Reitit.impl adds "=" for empty string values
|
|
|
|
|
; (str "foo?" (impl/query-string {:bar ""}))
|
|
|
|
|
))
|
2023-03-23 13:36:48 +00:00
|
|
|
|
|
|
|
|
(is (= "foo"
|
2023-03-24 09:16:09 +00:00
|
|
|
(rf/set-query-params "foo?asd=1" #(dissoc % :asd))))
|
2023-03-23 13:36:48 +00:00
|
|
|
|
|
|
|
|
(testing "Need to coerce current values manually"
|
|
|
|
|
(is (= "foo?foo=2"
|
2023-03-24 09:16:09 +00:00
|
|
|
(rf/set-query-params "foo?foo=1" (fn [q] (update q :foo #(inc (js/parseInt %)))))))))
|
2023-03-24 12:29:36 +00:00
|
|
|
|
|
|
|
|
(deftest match->path-test
|
|
|
|
|
(is (= "foo"
|
2024-06-21 07:59:33 +00:00
|
|
|
(rf/match->path {:path "foo"})
|
|
|
|
|
(rf/match->path {:path "foo"} nil)
|
2023-03-24 12:29:36 +00:00
|
|
|
(rf/match->path {:path "foo"} nil nil)
|
|
|
|
|
(rf/match->path {:path "foo"} {} "")))
|
|
|
|
|
(is (= "foo?a=1&b=&c=foo+bar"
|
|
|
|
|
;; NOTE: This encoding differs from set-query
|
|
|
|
|
(rf/match->path {:path "foo"} {:a "1" :b "" :c "foo bar"} nil)))
|
|
|
|
|
(is (= "foo#aaa"
|
|
|
|
|
(rf/match->path {:path "foo"} nil "aaa")))
|
|
|
|
|
(testing "Fragment encoding"
|
|
|
|
|
(is (= "foo#foo+bar+%25"
|
|
|
|
|
(rf/match->path {:path "foo"} nil "foo bar %")))))
|