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