mirror of
https://github.com/penpot/penpot.git
synced 2026-08-09 22:38:40 +00:00
✨ Enable closed schemas for RPC methods
This commit is contained in:
parent
b5bec4f983
commit
0eed852e2e
@ -92,12 +92,12 @@
|
||||
(handle-response-transformation request mdata)
|
||||
(handle-before-comple-hook mdata))))
|
||||
|
||||
(defn- make-rpc-handler
|
||||
(defn make-rpc-handler
|
||||
"Ring handler that dispatches cmd requests and convert between
|
||||
internal async flow into ring async flow."
|
||||
[methods]
|
||||
(let [methods (update-vals methods peek)]
|
||||
(fn [{:keys [params path-params method] :as request}]
|
||||
(fn [{:keys [path-params method] :as request}]
|
||||
(let [handler-name (:method-name path-params)
|
||||
etag (yreq/get-header request "if-none-match")
|
||||
session-id (yreq/get-header request "x-session-id")
|
||||
@ -111,7 +111,7 @@
|
||||
|
||||
ip-addr (inet/parse-request request)
|
||||
|
||||
data (-> params
|
||||
data (-> {}
|
||||
(assoc ::handler-name handler-name)
|
||||
(assoc ::ip-addr ip-addr)
|
||||
(assoc ::request-at (ct/now))
|
||||
@ -218,17 +218,6 @@
|
||||
f)
|
||||
f))
|
||||
|
||||
(defn- wrap-spec-conform
|
||||
[_ f mdata]
|
||||
;; NOTE: skip spec conform operation on rpc methods that already
|
||||
;; uses malli validation mechanism.
|
||||
(if (contains? mdata ::sm/params)
|
||||
f
|
||||
(if-let [spec (ex/ignoring (s/spec (::sv/spec mdata)))]
|
||||
(fn [cfg params]
|
||||
(f cfg (us/conform spec params)))
|
||||
f)))
|
||||
|
||||
(defn- wrap-params-validation
|
||||
[_ f mdata]
|
||||
(if-let [schema (::sm/params mdata)]
|
||||
@ -237,16 +226,15 @@
|
||||
decode (sm/decoder schema sm/json-transformer)
|
||||
encode (sm/encoder schema sm/json-transformer)]
|
||||
(fn [cfg params]
|
||||
(let [params (decode params)]
|
||||
(if (validate params)
|
||||
(let [result (f cfg params)]
|
||||
(let [request-params (-> params meta ::http/request :params decode)]
|
||||
(if (validate request-params)
|
||||
(let [result (f cfg (merge params request-params))]
|
||||
(if (instance? clojure.lang.IObj result)
|
||||
(vary-meta result assoc :encode/json encode)
|
||||
result))
|
||||
(let [params (d/without-qualified params)]
|
||||
(ex/raise :type :validation
|
||||
:code :params-validation
|
||||
::sm/explain (explain params)))))))
|
||||
(ex/raise :type :validation
|
||||
:code :params-validation
|
||||
::sm/explain (explain request-params))))))
|
||||
f))
|
||||
|
||||
|
||||
@ -334,10 +322,9 @@
|
||||
(wrap-metrics cfg $ mdata)
|
||||
(rlimit/wrap cfg $ mdata)
|
||||
(wrap-audit cfg $ mdata)
|
||||
(wrap-spec-conform cfg $ mdata)
|
||||
(wrap-nitrate-sso cfg $ mdata)
|
||||
(wrap-params-validation cfg $ mdata)
|
||||
(wrap-authentication cfg $ mdata)
|
||||
(wrap-nitrate-sso cfg $ mdata)))
|
||||
(wrap-authentication cfg $ mdata)))
|
||||
|
||||
(defn- wrap-management
|
||||
[cfg f mdata]
|
||||
@ -347,12 +334,9 @@
|
||||
(climit/wrap cfg $ mdata)
|
||||
(wrap-metrics cfg $ mdata)
|
||||
(wrap-audit cfg $ mdata)
|
||||
(wrap-spec-conform cfg $ mdata)
|
||||
(wrap-nitrate-sso cfg $ mdata)
|
||||
(wrap-params-validation cfg $ mdata)
|
||||
(wrap-authentication cfg $ mdata)
|
||||
(wrap-nitrate-sso cfg $ mdata)))
|
||||
|
||||
|
||||
(wrap-authentication cfg $ mdata)))
|
||||
|
||||
(defn- process-method
|
||||
[cfg wrap-fn [f mdata]]
|
||||
|
||||
@ -118,7 +118,7 @@
|
||||
|
||||
(def ^:private schema:import-binfile
|
||||
[:and
|
||||
[:map {:title "import-binfile"}
|
||||
[:map {:title "import-binfile" :closed true}
|
||||
[:name [:or [:string {:max 250}]
|
||||
[:map-of ::sm/uuid [:string {:max 250}]]]]
|
||||
[:project-id ::sm/uuid]
|
||||
|
||||
@ -49,7 +49,9 @@
|
||||
[promesa.core :as p]
|
||||
[promesa.exec :as px]
|
||||
[ring.core.protocols :as rcp]
|
||||
[yetti.request :as yreq]
|
||||
[yetti.request :as yrq]
|
||||
[yetti.response :as yres]
|
||||
[yetti.response :as yres])
|
||||
(:import
|
||||
java.io.PipedInputStream
|
||||
@ -379,6 +381,82 @@
|
||||
{:error (handle-error e#)
|
||||
:result nil})))
|
||||
|
||||
(defrecord DummyRequest [headers cookies method body-stream
|
||||
remote-addr server-name server-port
|
||||
scheme protocol path query ssl-client-cert]
|
||||
yreq/IRequestCookies
|
||||
(get-cookie [_ name]
|
||||
{:value (get cookies name)})
|
||||
|
||||
yreq/IRequest
|
||||
(get-header [_ name]
|
||||
(get headers name))
|
||||
(method [_] method)
|
||||
(body [_] body-stream)
|
||||
(path [_] path)
|
||||
(query [_] query)
|
||||
(server-port [_] server-port)
|
||||
(server-name [_] server-name)
|
||||
(remote-addr [_] remote-addr)
|
||||
(ssl-client-cert [_] ssl-client-cert)
|
||||
(scheme [_] scheme)
|
||||
(protocol [_] protocol))
|
||||
|
||||
(defn make-dummy-request
|
||||
"Constructs a DummyRequest from an options map. Every key is
|
||||
optional; missing values fall back to sensible defaults. New
|
||||
fields added to DummyRequest won't break existing call sites
|
||||
as long as this constructor keeps its `:or` defaults in sync.
|
||||
|
||||
Recognized keys:
|
||||
:headers — map of header name → value
|
||||
:cookies — map of cookie name → value
|
||||
:method — HTTP method keyword (default :get)
|
||||
:body-stream — InputStream for the body (used directly)
|
||||
:body-bytes — bytes or string for the body; wrapped in a
|
||||
ByteArrayInputStream if :body-stream is not
|
||||
given
|
||||
:remote-addr — string (default \"127.0.0.1\")
|
||||
:server-name — string (default \"test\")
|
||||
:server-port — long (default 0)
|
||||
:scheme — keyword (default :http)
|
||||
:protocol — string (default \"HTTP/1.1\")
|
||||
:path — string (default \"/test\")
|
||||
:query — string or nil (default nil)
|
||||
:ssl-client-cert — X509Certificate or nil (default nil)"
|
||||
[& {:keys [headers cookies method body-stream body-bytes
|
||||
remote-addr server-name server-port scheme protocol
|
||||
path query ssl-client-cert]
|
||||
:or {headers {} cookies {} method :get
|
||||
body-stream nil
|
||||
remote-addr "127.0.0.1" server-name "test" server-port 0
|
||||
scheme :http protocol "HTTP/1.1" path "/test" query nil
|
||||
ssl-client-cert nil}}]
|
||||
(let [body-stream (or body-stream
|
||||
(when body-bytes
|
||||
(java.io.ByteArrayInputStream.
|
||||
(if (string? body-bytes)
|
||||
(.getBytes ^String body-bytes "UTF-8")
|
||||
body-bytes))))]
|
||||
(->DummyRequest headers cookies method body-stream
|
||||
remote-addr server-name server-port
|
||||
scheme protocol path query ssl-client-cert)))
|
||||
|
||||
(defn- prepare-rpc-params
|
||||
[data]
|
||||
(let [params (reduce-kv (fn [params k v]
|
||||
(if (qualified-keyword? k)
|
||||
(assoc params k v)
|
||||
params))
|
||||
{}
|
||||
(dissoc data ::type))
|
||||
request (-> (make-dummy-request)
|
||||
(assoc :params (d/without-qualified data)))]
|
||||
|
||||
(-> params
|
||||
(assoc :app.rpc/request-at (ct/now))
|
||||
(with-meta {:app.http/request request}))))
|
||||
|
||||
(defn command!
|
||||
[{:keys [::type] :as data}]
|
||||
(let [[mdata method-fn] (get-in *system* [:app.rpc/methods type])]
|
||||
@ -387,10 +465,8 @@
|
||||
:code :rpc-method-not-found
|
||||
:hint (str/ffmt "rpc method '%' not found" (name type))))
|
||||
|
||||
;; (app.common.pprint/pprint (:app.rpc/methods *system*))
|
||||
(try-on! (method-fn (-> data
|
||||
(dissoc ::type)
|
||||
(assoc :app.rpc/request-at (ct/now)))))))
|
||||
(let [params (prepare-rpc-params data)]
|
||||
(try-on! (method-fn params)))))
|
||||
|
||||
(defn management-command!
|
||||
[{:keys [::type] :as data}]
|
||||
@ -399,9 +475,8 @@
|
||||
(ex/raise :type :assertion
|
||||
:code :rpc-method-not-found
|
||||
:hint (str/ffmt "management rpc method '%' not found" (name type))))
|
||||
(try-on! (method-fn (-> data
|
||||
(dissoc ::type)
|
||||
(assoc :app.rpc/request-at (ct/now)))))))
|
||||
(let [params (prepare-rpc-params data)]
|
||||
(try-on! (method-fn params)))))
|
||||
|
||||
(defn run-task!
|
||||
([name]
|
||||
|
||||
@ -28,78 +28,17 @@
|
||||
(t/use-fixtures :once th/state-init)
|
||||
(t/use-fixtures :each th/database-reset)
|
||||
|
||||
(defrecord DummyRequest [headers cookies method body-stream
|
||||
remote-addr server-name server-port
|
||||
scheme protocol path query ssl-client-cert]
|
||||
yreq/IRequestCookies
|
||||
(get-cookie [_ name]
|
||||
{:value (get cookies name)})
|
||||
|
||||
yreq/IRequest
|
||||
(get-header [_ name]
|
||||
(get headers name))
|
||||
(method [_] method)
|
||||
(body [_] body-stream)
|
||||
(path [_] path)
|
||||
(query [_] query)
|
||||
(server-port [_] server-port)
|
||||
(server-name [_] server-name)
|
||||
(remote-addr [_] remote-addr)
|
||||
(ssl-client-cert [_] ssl-client-cert)
|
||||
(scheme [_] scheme)
|
||||
(protocol [_] protocol))
|
||||
|
||||
(defn- make-dummy-request
|
||||
"Constructs a DummyRequest from an options map. Every key is
|
||||
optional; missing values fall back to sensible defaults. New
|
||||
fields added to DummyRequest won't break existing call sites
|
||||
as long as this constructor keeps its `:or` defaults in sync.
|
||||
|
||||
Recognized keys:
|
||||
:headers — map of header name → value
|
||||
:cookies — map of cookie name → value
|
||||
:method — HTTP method keyword (default :get)
|
||||
:body-stream — InputStream for the body (used directly)
|
||||
:body-bytes — bytes or string for the body; wrapped in a
|
||||
ByteArrayInputStream if :body-stream is not
|
||||
given
|
||||
:remote-addr — string (default \"127.0.0.1\")
|
||||
:server-name — string (default \"test\")
|
||||
:server-port — long (default 0)
|
||||
:scheme — keyword (default :http)
|
||||
:protocol — string (default \"HTTP/1.1\")
|
||||
:path — string (default \"/test\")
|
||||
:query — string or nil (default nil)
|
||||
:ssl-client-cert — X509Certificate or nil (default nil)"
|
||||
[{:keys [headers cookies method body-stream body-bytes
|
||||
remote-addr server-name server-port scheme protocol
|
||||
path query ssl-client-cert]
|
||||
:or {headers {} cookies {} method :get
|
||||
body-stream nil
|
||||
remote-addr "127.0.0.1" server-name "test" server-port 0
|
||||
scheme :http protocol "HTTP/1.1" path "/test" query nil
|
||||
ssl-client-cert nil}}]
|
||||
(let [body-stream (or body-stream
|
||||
(when body-bytes
|
||||
(java.io.ByteArrayInputStream.
|
||||
(if (string? body-bytes)
|
||||
(.getBytes ^String body-bytes "UTF-8")
|
||||
body-bytes))))]
|
||||
(->DummyRequest headers cookies method body-stream
|
||||
remote-addr server-name server-port
|
||||
scheme protocol path query ssl-client-cert)))
|
||||
|
||||
(t/deftest auth-middleware-1
|
||||
(let [request (volatile! nil)
|
||||
handler (#'app.http.middleware/wrap-auth
|
||||
(fn [req] (vreset! request req))
|
||||
{})]
|
||||
|
||||
(handler (make-dummy-request {}))
|
||||
(handler (th/make-dummy-request {}))
|
||||
|
||||
(t/is (nil? (::http/auth-data @request)))
|
||||
|
||||
(handler (make-dummy-request {:headers {"authorization" "Token aaaa"}}))
|
||||
(handler (th/make-dummy-request {:headers {"authorization" "Token aaaa"}}))
|
||||
|
||||
(let [{:keys [token claims] token-type :type} (get @request ::http/auth-data)]
|
||||
(t/is (= :token token-type))
|
||||
@ -112,10 +51,10 @@
|
||||
(fn [req] (vreset! request req))
|
||||
{})]
|
||||
|
||||
(handler (make-dummy-request {}))
|
||||
(handler (th/make-dummy-request {}))
|
||||
(t/is (nil? (::http/auth-data @request)))
|
||||
|
||||
(handler (make-dummy-request {:headers {"authorization" "Bearer aaaa"}}))
|
||||
(handler (th/make-dummy-request {:headers {"authorization" "Bearer aaaa"}}))
|
||||
|
||||
(let [{:keys [token claims] token-type :type} (get @request ::http/auth-data)]
|
||||
(t/is (= :bearer token-type))
|
||||
@ -128,10 +67,10 @@
|
||||
(fn [req] (vreset! request req))
|
||||
{})]
|
||||
|
||||
(handler (make-dummy-request {}))
|
||||
(handler (th/make-dummy-request {}))
|
||||
(t/is (nil? (::http/auth-data @request)))
|
||||
|
||||
(handler (make-dummy-request {:cookies {"auth-token" "foobar"}}))
|
||||
(handler (th/make-dummy-request {:cookies {"auth-token" "foobar"}}))
|
||||
|
||||
(let [{:keys [token claims] token-type :type} (get @request ::http/auth-data)]
|
||||
(t/is (= :cookie token-type))
|
||||
@ -143,16 +82,16 @@
|
||||
(fn [req] {::yres/status 200})
|
||||
{:test1 "secret-key"})]
|
||||
|
||||
(let [response (handler (make-dummy-request {}))]
|
||||
(let [response (handler (th/make-dummy-request {}))]
|
||||
(t/is (= 403 (::yres/status response))))
|
||||
|
||||
(let [response (handler (make-dummy-request {:headers {"x-shared-key" "secret-key2"}}))]
|
||||
(let [response (handler (th/make-dummy-request {:headers {"x-shared-key" "secret-key2"}}))]
|
||||
(t/is (= 403 (::yres/status response))))
|
||||
|
||||
(let [response (handler (make-dummy-request {:headers {"x-shared-key" "secret-key"}}))]
|
||||
(let [response (handler (th/make-dummy-request {:headers {"x-shared-key" "secret-key"}}))]
|
||||
(t/is (= 403 (::yres/status response))))
|
||||
|
||||
(let [response (handler (make-dummy-request {:headers {"x-shared-key" "test1 secret-key"}}))]
|
||||
(let [response (handler (th/make-dummy-request {:headers {"x-shared-key" "test1 secret-key"}}))]
|
||||
(t/is (= 200 (::yres/status response))))))
|
||||
|
||||
(t/deftest access-token-authz
|
||||
@ -263,7 +202,7 @@
|
||||
:user-agent "user agent"})
|
||||
(#'session/assign-token cfg))
|
||||
|
||||
response (handler (make-dummy-request {:cookies {"auth-token" (:token session)}}))
|
||||
response (handler (th/make-dummy-request {:cookies {"auth-token" (:token session)}}))
|
||||
|
||||
{:keys [token claims] token-type :type}
|
||||
(get response ::http/auth-data)]
|
||||
@ -290,7 +229,7 @@
|
||||
;; value with a backslash followed by '}', which
|
||||
;; clojure.data.json v0.5.x cannot handle.
|
||||
body (.getBytes "{\"x\": \"\\}\"}" "UTF-8")
|
||||
request (make-dummy-request
|
||||
request (th/make-dummy-request
|
||||
{:method :post
|
||||
:headers {"content-type" "application/json"}
|
||||
:body-bytes body})
|
||||
@ -309,7 +248,7 @@
|
||||
;; error.
|
||||
(let [handler (#'app.http.middleware/wrap-parse-request
|
||||
(fn [_] (throw (RequestTooBigException. "too large"))))
|
||||
request (make-dummy-request
|
||||
request (th/make-dummy-request
|
||||
{:method :post
|
||||
:headers {"content-type" "application/json"}
|
||||
:body-bytes (.getBytes "{}" "UTF-8")})
|
||||
@ -327,7 +266,7 @@
|
||||
;; should convert it to a 400 :malformed-json validation error.
|
||||
(let [handler (#'app.http.middleware/wrap-parse-request
|
||||
(fn [_] (throw (java.io.EOFException. "stream closed"))))
|
||||
request (make-dummy-request
|
||||
request (th/make-dummy-request
|
||||
{:method :post
|
||||
:headers {"content-type" "application/json"}
|
||||
:body-bytes (.getBytes "{}" "UTF-8")})
|
||||
@ -350,7 +289,7 @@
|
||||
(.initCause iae))
|
||||
handler (#'app.http.middleware/wrap-parse-request
|
||||
(fn [_] (throw wrapped)))
|
||||
request (make-dummy-request
|
||||
request (th/make-dummy-request
|
||||
{:method :post
|
||||
:headers {"content-type" "application/json"}
|
||||
:body-bytes (.getBytes "{}" "UTF-8")})
|
||||
@ -368,7 +307,7 @@
|
||||
;; :unexpected. This is the "true internal error" path.
|
||||
(let [handler (#'app.http.middleware/wrap-parse-request
|
||||
(fn [_] (throw (RuntimeException. "boom"))))
|
||||
request (make-dummy-request
|
||||
request (th/make-dummy-request
|
||||
{:method :post
|
||||
:headers {"content-type" "application/json"}
|
||||
:body-bytes (.getBytes "{}" "UTF-8")})
|
||||
@ -388,7 +327,7 @@
|
||||
;; with :code :io-exception.
|
||||
(let [handler (#'app.http.middleware/wrap-parse-request
|
||||
(fn [_] (throw (java.io.IOException. "network gone"))))
|
||||
request (make-dummy-request
|
||||
request (th/make-dummy-request
|
||||
{:method :post
|
||||
:headers {"content-type" "application/json"}
|
||||
:body-bytes (.getBytes "{}" "UTF-8")})
|
||||
|
||||
@ -22,13 +22,21 @@
|
||||
(let [schema @#'binfile/schema:import-binfile
|
||||
validator (sm/lazy-validator schema)
|
||||
|
||||
valid-params {:name "test"
|
||||
:project-id (uuid/random)
|
||||
:version 3
|
||||
:upload-id (uuid/random)}]
|
||||
valid-params
|
||||
{:name "test"
|
||||
:project-id (uuid/random)
|
||||
:version 3
|
||||
:upload-id (uuid/random)}
|
||||
|
||||
params-with-file-id
|
||||
(assoc valid-params :file-id (uuid/random))]
|
||||
|
||||
(t/is (true? (validator valid-params))
|
||||
"params without file-id should be valid")
|
||||
|
||||
(t/is (not (contains? (sm/keys (second schema)) :file-id))
|
||||
"file-id should not be a declared parameter")))
|
||||
"file-id should not be a declared parameter")
|
||||
|
||||
;; Params with file-id should fail (schema closed)
|
||||
(t/is (false? (validator params-with-file-id))
|
||||
"params with file-id should be rejected")))
|
||||
|
||||
@ -1107,8 +1107,11 @@
|
||||
body))
|
||||
|
||||
(let [[_ method-fn] (get-in th/*system* [:app.rpc/methods :get-nitrate-activation-code-request])
|
||||
result (method-fn {::rpc/profile-id (:id profile)
|
||||
::rpc/request-at now})
|
||||
request (assoc (th/make-dummy-request) :params {})
|
||||
params (-> {::rpc/profile-id (:id profile)
|
||||
::rpc/request-at now}
|
||||
(with-meta {:app.http/request request}))
|
||||
result (method-fn params)
|
||||
headers (::http/headers (meta result))]
|
||||
(t/is (rph/wrapped? result))
|
||||
(t/is (= "text/plain" (get headers "content-type")))
|
||||
|
||||
913
backend/test/backend_tests/rpc_test.clj
Normal file
913
backend/test/backend_tests/rpc_test.clj
Normal file
@ -0,0 +1,913 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns backend-tests.rpc-test
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.http :as http]
|
||||
[app.http.access-token :as actoken]
|
||||
[app.http.session :as session]
|
||||
[app.rpc :as rpc]
|
||||
[app.rpc.cond :as cond]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.util.inet :as inet]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
[yetti.response :as yres]))
|
||||
|
||||
(defn- echo-handler
|
||||
"Handler that returns the data map it receives, for inspection."
|
||||
[data]
|
||||
data)
|
||||
|
||||
(defn- make-handler
|
||||
"Creates an RPC handler with a single :test-method backed by echo-handler.
|
||||
Extra methods are merged in; each value is [metadata handler-fn]."
|
||||
([] (make-handler {}))
|
||||
([extra-methods]
|
||||
(let [methods (merge {:test-method [{} echo-handler]}
|
||||
extra-methods)]
|
||||
(rpc/make-rpc-handler methods))))
|
||||
|
||||
(defn- make-request
|
||||
"Builds a dummy request. Extra qualified-keyword keys are merged onto it."
|
||||
[& {:keys [method path-params headers extras]
|
||||
:or {method :post
|
||||
path-params {:method-name "test-method"}
|
||||
headers {}
|
||||
extras {}}}]
|
||||
(-> (th/make-dummy-request {:method method
|
||||
:headers headers})
|
||||
(assoc :path-params path-params
|
||||
:params {})
|
||||
(merge extras)))
|
||||
|
||||
(defn- call
|
||||
"Invokes handler with a built request, returns the full response map."
|
||||
([handler opts]
|
||||
(handler (make-request opts)))
|
||||
([handler]
|
||||
(call handler {})))
|
||||
|
||||
(defn- call-body
|
||||
"Invokes handler with a built request, returns ::yres/body from response."
|
||||
([handler opts]
|
||||
(::yres/body (call handler opts)))
|
||||
([handler]
|
||||
(::yres/body (call handler))))
|
||||
|
||||
(defn- call-ex
|
||||
"Invokes handler, catches and returns the exception."
|
||||
([handler opts]
|
||||
(try (call handler opts) (catch Exception e e)))
|
||||
([handler]
|
||||
(call-ex handler {})))
|
||||
|
||||
(def fixed-time (ct/now))
|
||||
(def fixed-uuid (uuid/custom 11111111 22222222))
|
||||
|
||||
;; --- BASIC HANDLER DISPATCH
|
||||
|
||||
(t/deftest handler-receives-handler-name
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [handler (make-handler)
|
||||
data (call-body handler)]
|
||||
(t/is (= "test-method" (::rpc/handler-name data))))))
|
||||
|
||||
(t/deftest handler-receives-ip-addr
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "10.0.0.1")]
|
||||
(let [handler (make-handler)
|
||||
data (call-body handler)]
|
||||
(t/is (= "10.0.0.1" (::rpc/ip-addr data))))))
|
||||
|
||||
(t/deftest handler-receives-request-at
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [handler (make-handler)
|
||||
data (call-body handler)]
|
||||
(t/is (= fixed-time (::rpc/request-at data))))))
|
||||
|
||||
(t/deftest handler-receives-request-id
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [handler (make-handler)
|
||||
data (call-body handler)]
|
||||
(t/is (= fixed-uuid (::rpc/request-id data))))))
|
||||
|
||||
(t/deftest unknown-handler-name-raises-not-found
|
||||
(let [handler (make-handler)
|
||||
ex (call-ex handler {:path-params {:method-name "unknown"}})]
|
||||
(t/is (some? ex))
|
||||
(t/is (th/ex-of-type? ex :not-found))))
|
||||
|
||||
;; --- HANDLER-NAME EDGE CASES
|
||||
|
||||
(t/deftest nil-path-params-falls-to-default-handler
|
||||
(let [handler (make-handler)
|
||||
request (make-request {:path-params nil})
|
||||
ex (try (handler request) (catch Exception e e))]
|
||||
(t/is (some? ex))
|
||||
(t/is (th/ex-of-type? ex :not-found))))
|
||||
|
||||
(t/deftest missing-method-name-falls-to-default-handler
|
||||
(let [handler (make-handler)
|
||||
request (make-request {:path-params {}})
|
||||
ex (try (handler request) (catch Exception e e))]
|
||||
(t/is (some? ex))
|
||||
(t/is (th/ex-of-type? ex :not-found))))
|
||||
|
||||
(t/deftest empty-method-name-falls-to-default-handler
|
||||
(let [handler (make-handler)
|
||||
request (make-request {:path-params {:method-name ""}})
|
||||
ex (try (handler request) (catch Exception e e))]
|
||||
(t/is (some? ex))
|
||||
(t/is (th/ex-of-type? ex :not-found))))
|
||||
|
||||
(t/deftest get-with-exact-get-prefix-no-suffix-allowed
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [handler (make-handler {:get- [{} echo-handler]})
|
||||
data (call-body handler
|
||||
{:method :get
|
||||
:path-params {:method-name "get-"}})]
|
||||
(t/is (= "get-" (::rpc/handler-name data))))))
|
||||
|
||||
;; --- AUTH: SESSION
|
||||
|
||||
(t/deftest session-auth-profile-id
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [pid (uuid/next)
|
||||
handler (make-handler)
|
||||
data (call-body handler
|
||||
{:extras {::session/profile-id pid}})]
|
||||
(t/is (= pid (::rpc/profile-id data))))))
|
||||
|
||||
(t/deftest session-auth-type
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [pid (uuid/next)
|
||||
handler (make-handler)
|
||||
data (call-body handler
|
||||
{:extras {::session/profile-id pid}})]
|
||||
(t/is (= :session (::rpc/auth-type data))))))
|
||||
|
||||
(t/deftest session-auth-no-token-perms
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [pid (uuid/next)
|
||||
handler (make-handler)
|
||||
data (call-body handler
|
||||
{:extras {::session/profile-id pid}})]
|
||||
(t/is (not (contains? data ::rpc/token-perms))))))
|
||||
|
||||
(t/deftest nil-session-profile-id-not-used
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [handler (make-handler)
|
||||
data (call-body handler
|
||||
{:extras {::session/profile-id nil}})]
|
||||
(t/is (not (contains? data ::rpc/profile-id)))
|
||||
(t/is (not (contains? data ::rpc/auth-type))))))
|
||||
|
||||
;; --- AUTH: TOKEN
|
||||
|
||||
(t/deftest token-auth-profile-id
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [pid (uuid/next)
|
||||
handler (make-handler)
|
||||
data (call-body handler
|
||||
{:extras {::actoken/profile-id pid}})]
|
||||
(t/is (= pid (::rpc/profile-id data))))))
|
||||
|
||||
(t/deftest token-auth-type
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [pid (uuid/next)
|
||||
handler (make-handler)
|
||||
data (call-body handler
|
||||
{:extras {::actoken/profile-id pid}})]
|
||||
(t/is (= :token (::rpc/auth-type data))))))
|
||||
|
||||
(t/deftest token-auth-with-perms
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [pid (uuid/next)
|
||||
handler (make-handler)
|
||||
data (call-body handler
|
||||
{:extras {::actoken/profile-id pid
|
||||
::actoken/perms #{"view" "edit"}}})]
|
||||
(t/is (= #{"view" "edit"} (::rpc/token-perms data))))))
|
||||
|
||||
(t/deftest token-auth-perms-default-to-empty-set
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [pid (uuid/next)
|
||||
handler (make-handler)
|
||||
data (call-body handler
|
||||
{:extras {::actoken/profile-id pid}})]
|
||||
(t/is (= #{} (::rpc/token-perms data))))))
|
||||
|
||||
(t/deftest token-auth-perms-coerced-from-vector
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [pid (uuid/next)
|
||||
handler (make-handler)
|
||||
data (call-body handler
|
||||
{:extras {::actoken/profile-id pid
|
||||
::actoken/perms ["view" "edit"]}})]
|
||||
(t/is (set? (::rpc/token-perms data)))
|
||||
(t/is (= #{"view" "edit"} (::rpc/token-perms data))))))
|
||||
|
||||
(t/deftest token-auth-perms-coerced-from-list
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [pid (uuid/next)
|
||||
handler (make-handler)
|
||||
data (call-body handler
|
||||
{:extras {::actoken/profile-id pid
|
||||
::actoken/perms (list "view" "edit")}})]
|
||||
(t/is (set? (::rpc/token-perms data)))
|
||||
(t/is (= #{"view" "edit"} (::rpc/token-perms data))))))
|
||||
|
||||
(t/deftest token-auth-with-nil-perms-defaults-to-empty-set
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [pid (uuid/next)
|
||||
handler (make-handler)
|
||||
data (call-body handler
|
||||
{:extras {::actoken/profile-id pid
|
||||
::actoken/perms nil}})]
|
||||
(t/is (= #{} (::rpc/token-perms data))))))
|
||||
|
||||
(t/deftest token-auth-with-empty-perms
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [pid (uuid/next)
|
||||
handler (make-handler)
|
||||
data (call-body handler
|
||||
{:extras {::actoken/profile-id pid
|
||||
::actoken/perms #{}}})]
|
||||
(t/is (= #{} (::rpc/token-perms data))))))
|
||||
|
||||
(t/deftest nil-actoken-profile-id-not-used
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [handler (make-handler)
|
||||
data (call-body handler
|
||||
{:extras {::actoken/profile-id nil}})]
|
||||
(t/is (not (contains? data ::rpc/profile-id)))
|
||||
(t/is (not (contains? data ::rpc/auth-type))))))
|
||||
|
||||
;; --- AUTH: SHARED KEY
|
||||
|
||||
(t/deftest key-auth-profile-id-is-zero
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [handler (make-handler)
|
||||
data (call-body handler
|
||||
{:extras {::http/auth-key-id "some-key"}})]
|
||||
(t/is (= uuid/zero (::rpc/profile-id data))))))
|
||||
|
||||
(t/deftest key-auth-key-id-in-data
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [handler (make-handler)
|
||||
data (call-body handler
|
||||
{:extras {::http/auth-key-id "some-key"}})]
|
||||
(t/is (= "some-key" (::rpc/auth-key-id data))))))
|
||||
|
||||
(t/deftest nil-auth-key-id-not-used
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [handler (make-handler)
|
||||
data (call-body handler
|
||||
{:extras {::http/auth-key-id nil}})]
|
||||
(t/is (not (contains? data ::rpc/profile-id)))
|
||||
(t/is (not (contains? data ::rpc/auth-key-id))))))
|
||||
|
||||
;; --- AUTH: UNAUTHENTICATED
|
||||
|
||||
(t/deftest unauthenticated-no-profile-id
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [handler (make-handler)
|
||||
data (call-body handler)]
|
||||
(t/is (not (contains? data ::rpc/profile-id))))))
|
||||
|
||||
(t/deftest unauthenticated-no-auth-type
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [handler (make-handler)
|
||||
data (call-body handler)]
|
||||
(t/is (not (contains? data ::rpc/auth-type))))))
|
||||
|
||||
;; --- AUTH: PRIORITY
|
||||
|
||||
(t/deftest session-overrides-token
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [session-pid (uuid/custom 1 1)
|
||||
token-pid (uuid/custom 2 2)
|
||||
handler (make-handler)
|
||||
data (call-body handler
|
||||
{:extras {::session/profile-id session-pid
|
||||
::actoken/profile-id token-pid}})]
|
||||
(t/is (= session-pid (::rpc/profile-id data)))
|
||||
(t/is (= :session (::rpc/auth-type data))))))
|
||||
|
||||
(t/deftest session-overrides-key
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [session-pid (uuid/custom 1 1)
|
||||
handler (make-handler)
|
||||
data (call-body handler
|
||||
{:extras {::session/profile-id session-pid
|
||||
::http/auth-key-id "key-id"}})]
|
||||
(t/is (= session-pid (::rpc/profile-id data)))
|
||||
(t/is (= :session (::rpc/auth-type data)))
|
||||
(t/is (= "key-id" (::rpc/auth-key-id data))))))
|
||||
|
||||
(t/deftest token-overrides-key
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [token-pid (uuid/custom 2 2)
|
||||
handler (make-handler)
|
||||
data (call-body handler
|
||||
{:extras {::actoken/profile-id token-pid
|
||||
::http/auth-key-id "key-id"}})]
|
||||
(t/is (= token-pid (::rpc/profile-id data)))
|
||||
(t/is (= :token (::rpc/auth-type data)))
|
||||
(t/is (= "key-id" (::rpc/auth-key-id data))))))
|
||||
|
||||
(t/deftest session-overrides-token-and-key
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [session-pid (uuid/custom 1 1)
|
||||
token-pid (uuid/custom 2 2)
|
||||
handler (make-handler)
|
||||
data (call-body handler
|
||||
{:extras {::session/profile-id session-pid
|
||||
::actoken/profile-id token-pid
|
||||
::http/auth-key-id "key-id"}})]
|
||||
(t/is (= session-pid (::rpc/profile-id data)))
|
||||
(t/is (= :session (::rpc/auth-type data))))))
|
||||
|
||||
;; --- REQUEST METADATA: SESSION-ID
|
||||
|
||||
(t/deftest session-id-parsed-from-header
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [sid (uuid/next)
|
||||
handler (make-handler)
|
||||
data (call-body handler
|
||||
{:headers {"x-session-id" (str sid)}})]
|
||||
(t/is (= sid (::rpc/session-id data))))))
|
||||
|
||||
(t/deftest session-id-nil-when-no-header
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [handler (make-handler)
|
||||
data (call-body handler)]
|
||||
(t/is (nil? (::rpc/session-id data))))))
|
||||
|
||||
(t/deftest session-id-nil-when-invalid-uuid
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [handler (make-handler)
|
||||
data (call-body handler
|
||||
{:headers {"x-session-id" "not-a-uuid"}})]
|
||||
(t/is (nil? (::rpc/session-id data))))))
|
||||
|
||||
(t/deftest session-id-nil-for-empty-string
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [handler (make-handler)
|
||||
data (call-body handler
|
||||
{:headers {"x-session-id" ""}})]
|
||||
(t/is (nil? (::rpc/session-id data))))))
|
||||
|
||||
(t/deftest session-id-parses-uppercase-uuid
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [sid (uuid/next)
|
||||
handler (make-handler)
|
||||
upper (clojure.string/upper-case (str sid))
|
||||
data (call-body handler
|
||||
{:headers {"x-session-id" upper}})]
|
||||
(t/is (= sid (::rpc/session-id data))))))
|
||||
|
||||
;; --- REQUEST METADATA: ETAG / COND KEY
|
||||
|
||||
(t/deftest cond-key-from-if-none-match
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [handler (make-handler)
|
||||
data (call-body handler
|
||||
{:headers {"if-none-match" "W/\"abc123\""}})]
|
||||
(t/is (= "W/\"abc123\"" (::cond/key data))))))
|
||||
|
||||
(t/deftest cond-key-nil-when-no-header
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [handler (make-handler)
|
||||
data (call-body handler)]
|
||||
(t/is (nil? (::cond/key data))))))
|
||||
|
||||
;; --- REQUEST METADATA: META
|
||||
|
||||
(t/deftest data-meta-contains-request
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [request (make-request {})
|
||||
handler (make-handler)
|
||||
data (call-body handler {})]
|
||||
(t/is (= request (::http/request (meta data)))))))
|
||||
|
||||
;; --- DATA COMPLETENESS
|
||||
|
||||
(t/deftest data-contains-all-expected-keys
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [pid (uuid/next)
|
||||
handler (make-handler)
|
||||
data (call-body handler
|
||||
{:extras {::session/profile-id pid
|
||||
::http/auth-key-id "key"}})]
|
||||
(t/is (contains? data ::rpc/handler-name))
|
||||
(t/is (contains? data ::rpc/ip-addr))
|
||||
(t/is (contains? data ::rpc/request-at))
|
||||
(t/is (contains? data ::rpc/request-id))
|
||||
(t/is (contains? data ::rpc/session-id))
|
||||
(t/is (contains? data ::cond/key))
|
||||
(t/is (contains? data ::rpc/profile-id))
|
||||
(t/is (contains? data ::rpc/auth-type))
|
||||
(t/is (contains? data ::rpc/auth-key-id)))))
|
||||
|
||||
;; --- GET/HEAD RESTRICTION
|
||||
|
||||
(t/deftest get-restriction-non-get-handler
|
||||
(let [handler (make-handler)
|
||||
ex (call-ex handler
|
||||
{:method :get
|
||||
:path-params {:method-name "foo"}})]
|
||||
(t/is (some? ex))
|
||||
(t/is (th/ex-of-type? ex :restriction))
|
||||
(t/is (th/ex-of-code? ex :method-not-allowed))))
|
||||
|
||||
(t/deftest head-restriction-non-get-handler
|
||||
(let [handler (make-handler)
|
||||
ex (call-ex handler
|
||||
{:method :head
|
||||
:path-params {:method-name "foo"}})]
|
||||
(t/is (some? ex))
|
||||
(t/is (th/ex-of-type? ex :restriction))
|
||||
(t/is (th/ex-of-code? ex :method-not-allowed))))
|
||||
|
||||
(t/deftest get-allowed-for-get-prefixed-handler
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [handler (make-handler {:get-profile [{} echo-handler]})
|
||||
data (call-body handler
|
||||
{:method :get
|
||||
:path-params {:method-name "get-profile"}})]
|
||||
(t/is (= "get-profile" (::rpc/handler-name data))))))
|
||||
|
||||
(t/deftest head-allowed-for-get-prefixed-handler
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [handler (make-handler {:get-files [{} echo-handler]})
|
||||
data (call-body handler
|
||||
{:method :head
|
||||
:path-params {:method-name "get-files"}})]
|
||||
(t/is (= "get-files" (::rpc/handler-name data))))))
|
||||
|
||||
(t/deftest post-always-allowed
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [handler (make-handler {:any-name [{} echo-handler]})
|
||||
data (call-body handler
|
||||
{:method :post
|
||||
:path-params {:method-name "any-name"}})]
|
||||
(t/is (= "any-name" (::rpc/handler-name data))))))
|
||||
|
||||
(t/deftest put-always-allowed
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [handler (make-handler {:any-name [{} echo-handler]})
|
||||
data (call-body handler
|
||||
{:method :put
|
||||
:path-params {:method-name "any-name"}})]
|
||||
(t/is (= "any-name" (::rpc/handler-name data))))))
|
||||
|
||||
(t/deftest delete-always-allowed
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [handler (make-handler {:any-name [{} echo-handler]})
|
||||
data (call-body handler
|
||||
{:method :delete
|
||||
:path-params {:method-name "any-name"}})]
|
||||
(t/is (= "any-name" (::rpc/handler-name data))))))
|
||||
|
||||
(t/deftest patch-always-allowed
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [handler (make-handler {:any-name [{} echo-handler]})
|
||||
data (call-body handler
|
||||
{:method :patch
|
||||
:path-params {:method-name "any-name"}})]
|
||||
(t/is (= "any-name" (::rpc/handler-name data))))))
|
||||
|
||||
(t/deftest get-to-non-get-handler-restriction-checks-before-handler-execution
|
||||
(let [called? (atom false)
|
||||
handler (make-handler {:foo [{} (fn [data]
|
||||
(reset! called? true)
|
||||
data)]})
|
||||
ex (call-ex handler
|
||||
{:method :get
|
||||
:path-params {:method-name "foo"}})]
|
||||
(t/is (some? ex))
|
||||
(t/is (false? @called?))))
|
||||
|
||||
;; --- RESPONSE: NIL -> 204
|
||||
|
||||
(t/deftest nil-response-returns-204
|
||||
(let [handler (make-handler {:nil-method [{} (constantly nil)]})
|
||||
response (call handler
|
||||
{:path-params {:method-name "nil-method"}})]
|
||||
(t/is (= 204 (::yres/status response)))))
|
||||
|
||||
(t/deftest nil-response-gets-stream-content-type
|
||||
(let [handler (make-handler {:nil-method [{} (constantly nil)]})
|
||||
response (call handler
|
||||
{:path-params {:method-name "nil-method"}})]
|
||||
(t/is (= "application/octet-stream"
|
||||
(get (::yres/headers response) "content-type")))))
|
||||
|
||||
;; --- RESPONSE: MAP -> 200
|
||||
|
||||
(t/deftest map-response-returns-200
|
||||
(let [handler (make-handler {:map-method [{} (constantly {:foo "bar"})]})
|
||||
response (call handler
|
||||
{:path-params {:method-name "map-method"}})]
|
||||
(t/is (= 200 (::yres/status response)))
|
||||
(t/is (= {:foo "bar"} (::yres/body response)))))
|
||||
|
||||
;; --- RESPONSE: FALSY NON-NIL VALUES
|
||||
|
||||
(t/deftest false-response-returns-200
|
||||
(let [handler (make-handler {:false-method [{} (constantly false)]})
|
||||
response (call handler
|
||||
{:path-params {:method-name "false-method"}})]
|
||||
(t/is (= 200 (::yres/status response)))
|
||||
(t/is (false? (::yres/body response)))))
|
||||
|
||||
(t/deftest zero-response-returns-200
|
||||
(let [handler (make-handler {:zero-method [{} (constantly 0)]})
|
||||
response (call handler
|
||||
{:path-params {:method-name "zero-method"}})]
|
||||
(t/is (= 200 (::yres/status response)))
|
||||
(t/is (= 0 (::yres/body response)))))
|
||||
|
||||
(t/deftest empty-map-response-returns-200
|
||||
(let [handler (make-handler {:empty-method [{} (constantly {})]})
|
||||
response (call handler
|
||||
{:path-params {:method-name "empty-method"}})]
|
||||
(t/is (= 200 (::yres/status response)))
|
||||
(t/is (= {} (::yres/body response)))))
|
||||
|
||||
;; --- RESPONSE: CUSTOM STATUS
|
||||
|
||||
(t/deftest custom-status-from-metadata
|
||||
(let [result (with-meta {:ok true} {::http/status 201})
|
||||
handler (make-handler {:created [{} (constantly result)]})
|
||||
response (call handler
|
||||
{:path-params {:method-name "created"}})]
|
||||
(t/is (= 201 (::yres/status response)))))
|
||||
|
||||
;; --- RESPONSE: CUSTOM HEADERS
|
||||
|
||||
(t/deftest custom-headers-from-metadata
|
||||
(let [result (with-meta {:ok true} {::http/headers {"x-custom" "val"}})
|
||||
handler (make-handler {:hdr-method [{} (constantly result)]})
|
||||
response (call handler
|
||||
{:path-params {:method-name "hdr-method"}})]
|
||||
(t/is (= "val" (get (::yres/headers response) "x-custom")))))
|
||||
|
||||
;; --- RESPONSE: CUSTOM STATUS AND HEADERS TOGETHER
|
||||
|
||||
(t/deftest custom-status-and-headers-from-metadata
|
||||
(let [result (with-meta {:ok true}
|
||||
{::http/status 202
|
||||
::http/headers {"x-request-id" "req-123"}})
|
||||
handler (make-handler {:both [{} (constantly result)]})
|
||||
response (call handler
|
||||
{:path-params {:method-name "both"}})]
|
||||
(t/is (= 202 (::yres/status response)))
|
||||
(t/is (= "req-123" (get (::yres/headers response) "x-request-id")))))
|
||||
|
||||
;; --- RESPONSE: FUNCTION
|
||||
|
||||
(t/deftest function-response-called-with-request
|
||||
(let [captured (atom nil)
|
||||
resp-fn (fn [req]
|
||||
(reset! captured req)
|
||||
{::yres/status 200
|
||||
::yres/body "from-fn"})
|
||||
handler (make-handler {:fn-method [{} (constantly resp-fn)]})
|
||||
request (make-request {:path-params {:method-name "fn-method"}})
|
||||
response (handler request)]
|
||||
(t/is (= request @captured))
|
||||
(t/is (= 200 (::yres/status response)))
|
||||
(t/is (= "from-fn" (::yres/body response)))))
|
||||
|
||||
(t/deftest function-response-returning-nil
|
||||
(let [handler (make-handler {:fn-nil [{} (constantly (fn [_] nil))]})
|
||||
response (call handler
|
||||
{:path-params {:method-name "fn-nil"}})]
|
||||
(t/is (nil? (::yres/body response)))))
|
||||
|
||||
(t/deftest function-response-uses-return-value-as-is
|
||||
(let [resp-fn (fn [_req]
|
||||
{:result "ok"})
|
||||
handler (make-handler {:fn-meta [{} (constantly resp-fn)]})
|
||||
response (call handler
|
||||
{:path-params {:method-name "fn-meta"}})]
|
||||
;; When handler returns a fn, handle-response calls (fn request)
|
||||
;; and uses the return value directly as the response.
|
||||
(t/is (= {:result "ok"} response))))
|
||||
|
||||
;; --- RESPONSE: METADATA WRAPPER
|
||||
|
||||
(t/deftest metadata-wrapper-unwrapped-in-response
|
||||
(let [wrapped (rph/wrap {:data "value"})
|
||||
handler (make-handler {:wrap-method [{} (constantly wrapped)]})
|
||||
response (call handler
|
||||
{:path-params {:method-name "wrap-method"}})]
|
||||
(t/is (= {:data "value"} (::yres/body response)))))
|
||||
|
||||
(t/deftest metadata-wrapper-preserves-metadata
|
||||
(let [wrapped (-> (rph/wrap {:data "value"})
|
||||
(vary-meta assoc ::http/status 201))
|
||||
handler (make-handler {:wrap-meta [{} (constantly wrapped)]})
|
||||
response (call handler
|
||||
{:path-params {:method-name "wrap-meta"}})]
|
||||
(t/is (= 201 (::yres/status response)))))
|
||||
|
||||
(t/deftest metadata-wrapper-with-custom-headers
|
||||
(let [wrapped (-> (rph/wrap {:data "value"})
|
||||
(vary-meta assoc ::http/headers {"x-wrap" "yes"}))
|
||||
handler (make-handler {:wrap-hdr [{} (constantly wrapped)]})
|
||||
response (call handler
|
||||
{:path-params {:method-name "wrap-hdr"}})]
|
||||
(t/is (= "yes" (get (::yres/headers response) "x-wrap")))))
|
||||
|
||||
;; --- RESPONSE: STREAM BODY
|
||||
|
||||
(t/deftest stream-response-default-content-type
|
||||
(let [stream (yres/stream-body (fn [_response _output] nil))
|
||||
handler (make-handler {:stream-method [{} (constantly stream)]})
|
||||
response (call handler
|
||||
{:path-params {:method-name "stream-method"}})]
|
||||
(t/is (= "application/octet-stream"
|
||||
(get (::yres/headers response) "content-type")))))
|
||||
|
||||
(t/deftest stream-response-preserves-existing-content-type
|
||||
(let [stream (with-meta
|
||||
(yres/stream-body (fn [_response _output] nil))
|
||||
{::http/headers {"content-type" "text/plain"}})
|
||||
handler (make-handler {:stream-ct [{} (constantly stream)]})
|
||||
response (call handler
|
||||
{:path-params {:method-name "stream-ct"}})]
|
||||
(t/is (= "text/plain"
|
||||
(get (::yres/headers response) "content-type")))))
|
||||
|
||||
;; --- RESPONSE TRANSFORMATION
|
||||
|
||||
(t/deftest response-transform-fns-applied
|
||||
(let [transform (fn [_req resp]
|
||||
(assoc-in resp [::yres/headers "x-transformed"] "yes"))
|
||||
result (with-meta {:ok true}
|
||||
{::rpc/response-transform-fns [transform]})
|
||||
handler (make-handler {:transform [{} (constantly result)]})
|
||||
response (call handler
|
||||
{:path-params {:method-name "transform"}})]
|
||||
(t/is (= "yes" (get (::yres/headers response) "x-transformed")))))
|
||||
|
||||
(t/deftest response-transform-fns-applied-in-order
|
||||
(let [t1 (fn [_req resp]
|
||||
(assoc-in resp [::yres/headers "x-step"] "1"))
|
||||
t2 (fn [_req resp]
|
||||
(update-in resp [::yres/headers "x-step"] str "-2"))
|
||||
result (with-meta {:ok true}
|
||||
{::rpc/response-transform-fns [t1 t2]})
|
||||
handler (make-handler {:multi-transform [{} (constantly result)]})
|
||||
response (call handler
|
||||
{:path-params {:method-name "multi-transform"}})]
|
||||
(t/is (= "1-2" (get (::yres/headers response) "x-step")))))
|
||||
|
||||
(t/deftest transform-fn-can-modify-status
|
||||
(let [transform (fn [_req _resp]
|
||||
{::yres/status 418
|
||||
::yres/body "teapot"
|
||||
::yres/headers {}})
|
||||
result (with-meta {:ok true}
|
||||
{::rpc/response-transform-fns [transform]})
|
||||
handler (make-handler {:teapot [{} (constantly result)]})
|
||||
response (call handler
|
||||
{:path-params {:method-name "teapot"}})]
|
||||
(t/is (= 418 (::yres/status response)))))
|
||||
|
||||
(t/deftest empty-transform-fns-no-op
|
||||
(let [result (with-meta {:ok true}
|
||||
{::rpc/response-transform-fns []})
|
||||
handler (make-handler {:empty-t [{} (constantly result)]})
|
||||
response (call handler
|
||||
{:path-params {:method-name "empty-t"}})]
|
||||
(t/is (= 200 (::yres/status response)))))
|
||||
|
||||
(t/deftest transform-receives-request-and-response
|
||||
(let [captured-req (atom nil)
|
||||
captured-resp (atom nil)
|
||||
transform (fn [req resp]
|
||||
(reset! captured-req req)
|
||||
(reset! captured-resp resp)
|
||||
resp)
|
||||
result (with-meta {:ok true}
|
||||
{::rpc/response-transform-fns [transform]})
|
||||
handler (make-handler {:cap-t [{} (constantly result)]})
|
||||
request (make-request {:path-params {:method-name "cap-t"}})
|
||||
response (handler request)]
|
||||
(t/is (= request @captured-req))
|
||||
(t/is (some? @captured-resp))
|
||||
(t/is (= 200 (::yres/status @captured-resp)))))
|
||||
|
||||
;; --- BEFORE-COMPLETE HOOKS
|
||||
|
||||
(t/deftest before-complete-hooks-called
|
||||
(let [called? (atom false)
|
||||
hook (fn [] (reset! called? true))
|
||||
result (with-meta {:ok true}
|
||||
{::rpc/before-complete-fns [hook]})
|
||||
handler (make-handler {:hook-method [{} (constantly result)]})]
|
||||
(call handler {:path-params {:method-name "hook-method"}})
|
||||
(t/is (true? @called?))))
|
||||
|
||||
(t/deftest multiple-hooks-all-called
|
||||
(let [called-a (atom false)
|
||||
called-b (atom false)
|
||||
hook-a (fn [] (reset! called-a true))
|
||||
hook-b (fn [] (reset! called-b true))
|
||||
result (with-meta {:ok true}
|
||||
{::rpc/before-complete-fns [hook-a hook-b]})
|
||||
handler (make-handler {:multi-hook [{} (constantly result)]})]
|
||||
(call handler {:path-params {:method-name "multi-hook"}})
|
||||
(t/is (true? @called-a))
|
||||
(t/is (true? @called-b))))
|
||||
|
||||
(t/deftest before-complete-hook-errors-ignored
|
||||
(let [hook (fn [] (throw (Exception. "hook error")))
|
||||
result (with-meta {:ok true}
|
||||
{::rpc/before-complete-fns [hook]})
|
||||
handler (make-handler {:hook-err [{} (constantly result)]})]
|
||||
(let [response (call handler
|
||||
{:path-params {:method-name "hook-err"}})]
|
||||
(t/is (= 200 (::yres/status response))))))
|
||||
|
||||
(t/deftest hook-error-does-not-prevent-other-hooks
|
||||
(let [called? (atom false)
|
||||
hook-a (fn [] (throw (Exception. "boom")))
|
||||
hook-b (fn [] (reset! called? true))
|
||||
result (with-meta {:ok true}
|
||||
{::rpc/before-complete-fns [hook-a hook-b]})
|
||||
handler (make-handler {:hook-chain [{} (constantly result)]})]
|
||||
(call handler {:path-params {:method-name "hook-chain"}})
|
||||
(t/is (true? @called?))))
|
||||
|
||||
(t/deftest empty-hooks-no-op
|
||||
(let [result (with-meta {:ok true}
|
||||
{::rpc/before-complete-fns []})
|
||||
handler (make-handler {:empty-hooks [{} (constantly result)]})
|
||||
response (call handler
|
||||
{:path-params {:method-name "empty-hooks"}})]
|
||||
(t/is (= 200 (::yres/status response)))))
|
||||
|
||||
;; --- METHODS MAP FORMAT
|
||||
|
||||
(t/deftest methods-map-uses-peek-to-extract-handler
|
||||
(with-redefs [ct/now (constantly fixed-time)
|
||||
uuid/next (constantly fixed-uuid)
|
||||
inet/parse-request (constantly "127.0.0.1")]
|
||||
(let [metadata {:some-meta "value"}
|
||||
methods {:peek-test [metadata echo-handler]}
|
||||
handler (rpc/make-rpc-handler methods)
|
||||
data (call-body handler
|
||||
{:path-params {:method-name "peek-test"}})]
|
||||
(t/is (= "peek-test" (::rpc/handler-name data))))))
|
||||
|
||||
;; --- COND/*ENABLED* BINDING
|
||||
|
||||
(t/deftest cond-enabled-bound-to-true-inside-handler
|
||||
(let [captured (atom nil)
|
||||
handler (make-handler
|
||||
{:cond-test [{} (fn [data]
|
||||
(reset! captured cond/*enabled*)
|
||||
data)]})]
|
||||
(call handler {:path-params {:method-name "cond-test"}})
|
||||
(t/is (true? @captured))))
|
||||
|
||||
(t/deftest cond-enabled-false-outside-handler
|
||||
(let [handler (make-handler)]
|
||||
(call handler)
|
||||
(t/is (false? cond/*enabled*))))
|
||||
|
||||
(t/deftest cond-enabled-restored-after-handler-throws
|
||||
(let [handler (make-handler
|
||||
{:throwing [{} (fn [_]
|
||||
(ex/raise :type :test :code :x))]})]
|
||||
(try (call handler {:path-params {:method-name "throwing"}})
|
||||
(catch Exception _))
|
||||
(t/is (false? cond/*enabled*))))
|
||||
|
||||
;; --- HANDLER EXCEPTION PROPAGATION
|
||||
|
||||
(t/deftest handler-exception-propagates-with-type-and-code
|
||||
(let [handler (make-handler
|
||||
{:fail [{} (fn [_]
|
||||
(ex/raise :type :test
|
||||
:code :boom))]})
|
||||
ex (call-ex handler {:path-params {:method-name "fail"}})]
|
||||
(t/is (some? ex))
|
||||
(t/is (th/ex-of-type? ex :test))
|
||||
(t/is (th/ex-of-code? ex :boom))))
|
||||
|
||||
;; --- DEFAULT HANDLER
|
||||
|
||||
(t/deftest default-handler-for-missing-method
|
||||
(let [handler (rpc/make-rpc-handler {})
|
||||
ex (try (handler (make-request
|
||||
{:path-params {:method-name "nonexistent"}}))
|
||||
(catch Exception e e))]
|
||||
(t/is (some? ex))
|
||||
(t/is (th/ex-of-type? ex :not-found))))
|
||||
|
||||
;; --- RESPONSE: NIL BODY WITH CUSTOM HEADERS
|
||||
|
||||
(t/deftest nil-body-with-custom-headers
|
||||
(let [wrapped (-> (rph/wrap nil)
|
||||
(vary-meta assoc ::http/headers {"x-custom" "val"}))
|
||||
handler (make-handler {:nil-hdr [{} (constantly wrapped)]})
|
||||
response (call handler
|
||||
{:path-params {:method-name "nil-hdr"}})]
|
||||
(t/is (= 204 (::yres/status response)))
|
||||
(t/is (= "val" (get (::yres/headers response) "x-custom")))))
|
||||
Loading…
x
Reference in New Issue
Block a user