diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index c7e33312ca..8d5007d571 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -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]] diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj index e68210aae3..78ba08e5fd 100644 --- a/backend/src/app/rpc/commands/binfile.clj +++ b/backend/src/app/rpc/commands/binfile.clj @@ -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] diff --git a/backend/test/backend_tests/helpers.clj b/backend/test/backend_tests/helpers.clj index 0f582497d9..669a3fc1f7 100644 --- a/backend/test/backend_tests/helpers.clj +++ b/backend/test/backend_tests/helpers.clj @@ -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] diff --git a/backend/test/backend_tests/http_middleware_test.clj b/backend/test/backend_tests/http_middleware_test.clj index bd986fc031..c7c2970b8a 100644 --- a/backend/test/backend_tests/http_middleware_test.clj +++ b/backend/test/backend_tests/http_middleware_test.clj @@ -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")}) diff --git a/backend/test/backend_tests/rpc_binfile_test.clj b/backend/test/backend_tests/rpc_binfile_test.clj index 536a980339..b84a608e7f 100644 --- a/backend/test/backend_tests/rpc_binfile_test.clj +++ b/backend/test/backend_tests/rpc_binfile_test.clj @@ -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"))) diff --git a/backend/test/backend_tests/rpc_nitrate_test.clj b/backend/test/backend_tests/rpc_nitrate_test.clj index 5d973d94bd..884af96319 100644 --- a/backend/test/backend_tests/rpc_nitrate_test.clj +++ b/backend/test/backend_tests/rpc_nitrate_test.clj @@ -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"))) diff --git a/backend/test/backend_tests/rpc_test.clj b/backend/test/backend_tests/rpc_test.clj new file mode 100644 index 0000000000..a4d877473c --- /dev/null +++ b/backend/test/backend_tests/rpc_test.clj @@ -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")))))