2026-08-07 18:19:09 +02:00

914 lines
37 KiB
Clojure

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