From 7fb9c27e46da33e087c4bb0062f1896712da2632 Mon Sep 17 00:00:00 2001 From: Joel Kaasinen Date: Wed, 29 Oct 2025 09:20:54 +0200 Subject: [PATCH 1/4] feat: use request Content-Type or :muuntaja/content-type to coerce Previously, `extract-response-format-default` was only looking at (-> request :muuntaja/response :format). This led to wrong behaviour when there were separate schemas for separate response content types and an explicitly picked content-type for the response. --- doc/ring/coercion.md | 19 +- modules/reitit-core/src/reitit/coercion.cljc | 6 +- test/cljc/reitit/ring_coercion_test.cljc | 307 +++++++++++++------ 3 files changed, 239 insertions(+), 93 deletions(-) diff --git a/doc/ring/coercion.md b/doc/ring/coercion.md index c450b43c..48c2debe 100644 --- a/doc/ring/coercion.md +++ b/doc/ring/coercion.md @@ -202,9 +202,11 @@ is: "application/edn" {:schema {:x s/Int}} :default {:schema {:ww s/Int}}}}} :handler ...}}]] - {:data {:middleware [rrc/coerce-exceptions-middleware - rrc/coerce-request-middleware - rrc/coerce-response-middleware]}}))) + {:data {:muuntaja muuntaja.core/instance + :middleware [reitit.ring.middleware.muuntaja/format-middleware + reitit.ring.coercion/coerce-exceptions-middleware + reitit.ring.coercion/coerce-request-middleware + reitit.ring.coercion/coerce-response-middleware]}}))) ``` The resolution logic for response coercers is: @@ -215,6 +217,17 @@ The resolution logic for response coercers is: 3. `:body` 3. If nothing was found, do not coerce +To select the response content-type, you can either: +1. Let muuntaja pick the content-type based on things like the request Accept header + - This is what most users want +2. Set `:muuntaja/content-type` in the response to pick an explicit content type +3. Set the `"Content-Type"` header in the response + - This disables muuntaja, so you need to encode your response body in some other way! + - This is not compatible with response schema checking, since coercion won't know what to do with the already-encoded response body. +4. Use the `:extract-response-format` option to inject your own logic. See `reitit.coercion/extract-response-format-default` for the default. + +See also the [muuntaja content negotiation](./content_negotiation.md) docs. + ## Pretty printing spec errors Spec problems are exposed as is in request & response coercion errors. Pretty-printers like [expound](https://github.com/bhb/expound) can be enabled like this: diff --git a/modules/reitit-core/src/reitit/coercion.cljc b/modules/reitit-core/src/reitit/coercion.cljc index 147b652c..4ecdd5a4 100644 --- a/modules/reitit-core/src/reitit/coercion.cljc +++ b/modules/reitit-core/src/reitit/coercion.cljc @@ -152,8 +152,10 @@ rcs (request-coercers coercion parameters (cond-> opts route-request (assoc ::skip #{:body})))] (if (and crc rcs) (into crc (vec rcs)) (or crc rcs))))) -(defn extract-response-format-default [request _] - (-> request :muuntaja/response :format)) +(defn extract-response-format-default [request response] + (or (get-in response [:headers "Content-Type"]) + (:muuntaja/content-type response) + (-> request :muuntaja/response :format))) (defn -format->coercer [coercion {:keys [content body]} _opts] (->> (concat (when body diff --git a/test/cljc/reitit/ring_coercion_test.cljc b/test/cljc/reitit/ring_coercion_test.cljc index aae8d69a..93af98a9 100644 --- a/test/cljc/reitit/ring_coercion_test.cljc +++ b/test/cljc/reitit/ring_coercion_test.cljc @@ -1,8 +1,10 @@ (ns reitit.ring-coercion-test (:require [clojure.test :refer [deftest is testing]] [malli.experimental.lite :as l] - #?@(:clj [[muuntaja.middleware] - [jsonista.core :as j]]) + #?@(:clj [[muuntaja.core] + [muuntaja.middleware] + [jsonista.core :as j] + [reitit.ring.middleware.muuntaja]]) [malli.core :as m] [malli.util :as mu] [meta-merge.core :refer [meta-merge]] @@ -585,99 +587,138 @@ #?(:clj (deftest per-content-type-test - (doseq [[coercion json-request edn-request default-request json-response edn-response default-response] - [[malli/coercion - [:map [:request [:enum :json]] [:response any?]] - [:map [:request [:enum :edn]] [:response any?]] - [:map [:request [:enum :default]] [:response any?]] - [:map [:request any?] [:response [:enum :json]]] - [:map [:request any?] [:response [:enum :edn]]] - [:map [:request any?] [:response [:enum :default]]]] - [schema/coercion - {:request (s/eq :json) :response s/Any} - {:request (s/eq :edn) :response s/Any} - {:request (s/eq :default) :response s/Any} - {:request s/Any :response (s/eq :json)} - {:request s/Any :response (s/eq :edn)} - {:request s/Any :response (s/eq :default)}] - [spec/coercion - {:request (clojure.spec.alpha/spec #{:json}) :response any?} - {:request (clojure.spec.alpha/spec #{:edn}) :response any?} - {:request (clojure.spec.alpha/spec #{:default}) :response any?} - {:request any? :response (clojure.spec.alpha/spec #{:json})} - {:request any? :response (clojure.spec.alpha/spec #{:end})} - {:request any? :response (clojure.spec.alpha/spec #{:default})}]]] - (testing (str coercion) - (doseq [{:keys [name app]} - [{:name "using top-level :body" - :app (ring/ring-handler - (ring/router - ["/foo" {:post {:request {:content {"application/json" {:schema json-request} - "application/edn" {:schema edn-request}} - :body default-request} - :responses {200 {:content {"application/json" {:schema json-response} - "application/edn" {:schema edn-response}} - :body default-response}} - :handler (fn [req] - {:status 200 - :body (-> req :parameters :request)})}}] - {:validate reitit.ring.spec/validate - :data {:middleware [rrc/coerce-request-middleware - rrc/coerce-response-middleware] - :coercion coercion}}))} - {:name "using :default content" - :app (ring/ring-handler - (ring/router - ["/foo" {:post {:request {:content {"application/json" {:schema json-request} - "application/edn" {:schema edn-request} - :default {:schema default-request}} - :body json-request} ;; not applied as :default exists - :responses {200 {:content {"application/json" {:schema json-response} - "application/edn" {:schema edn-response} - :default {:schema default-response}} - :body json-response}} ;; not applied as :default exists - :handler (fn [req] - {:status 200 - :body (-> req :parameters :request)})}}] - {:validate reitit.ring.spec/validate - :data {:middleware [rrc/coerce-request-middleware - rrc/coerce-response-middleware] - :coercion coercion}}))}]] - (testing name - (let [call (fn [request] + (let [normalize-json (fn [resp] + (update resp :body #(-> % j/write-value-as-string (j/read-value j/keyword-keys-object-mapper))))] + (doseq [[coercion json-request edn-request default-request json-response edn-response default-response] + [[malli/coercion + [:map [:request [:enum :json]] [:response any?]] + [:map [:request [:enum :edn]] [:response any?]] + [:map [:request [:enum :default]] [:response any?]] + [:map [:request any?] [:response [:enum :json]]] + [:map [:request any?] [:response [:enum :edn]]] + [:map [:request any?] [:response [:enum :default]]]] + [schema/coercion + {:request (s/eq :json) :response s/Any} + {:request (s/eq :edn) :response s/Any} + {:request (s/eq :default) :response s/Any} + {:request s/Any :response (s/eq :json)} + {:request s/Any :response (s/eq :edn)} + {:request s/Any :response (s/eq :default)}] + [spec/coercion + {:request (clojure.spec.alpha/spec #{:json}) :response any?} + {:request (clojure.spec.alpha/spec #{:edn}) :response any?} + {:request (clojure.spec.alpha/spec #{:default}) :response any?} + {:request any? :response (clojure.spec.alpha/spec #{:json})} + {:request any? :response (clojure.spec.alpha/spec #{:end})} + {:request any? :response (clojure.spec.alpha/spec #{:default})}]]] + (testing (str coercion) + (doseq [{:keys [name app]} + [{:name "using top-level :body" + :app (ring/ring-handler + (ring/router + ["/foo" {:post {:request {:content {"application/json" {:schema json-request} + "application/edn" {:schema edn-request}} + :body default-request} + :responses {200 {:content {"application/json" {:schema json-response} + "application/edn" {:schema edn-response}} + :body default-response}} + :handler (fn [req] + {:status 200 + :body (-> req :parameters :request)})}}] + {:validate reitit.ring.spec/validate + :data {:middleware [rrc/coerce-request-middleware + rrc/coerce-response-middleware] + :coercion coercion}}))} + {:name "using :default content" + :app (ring/ring-handler + (ring/router + ["/foo" {:post {:request {:content {"application/json" {:schema json-request} + "application/edn" {:schema edn-request} + :default {:schema default-request}} + :body json-request} ;; not applied as :default exists + :responses {200 {:content {"application/json" {:schema json-response} + "application/edn" {:schema edn-response} + :default {:schema default-response}} + :body json-response}} ;; not applied as :default exists + :handler (fn [req] + {:status 200 + :body (-> req :parameters :request)})}}] + {:validate reitit.ring.spec/validate + :data {:middleware [rrc/coerce-request-middleware + rrc/coerce-response-middleware] + :coercion coercion}}))}]] + (testing name + (let [call (fn [request] + (try + (app request) + (catch ExceptionInfo e + (select-keys (ex-data e) [:type :in])))) + request (fn [request-format response-format body] + {:request-method :post + :uri "/foo" + :muuntaja/request {:format request-format} + :muuntaja/response {:format response-format} + :body-params body})] + (testing "succesful call" + (is (= {:status 200 :body {:request "json", :response "json"}} + (normalize-json (call (request "application/json" "application/json" {:request :json :response :json}))))) + (is (= {:status 200 :body {:request "edn", :response "json"}} + (normalize-json (call (request "application/edn" "application/json" {:request :edn :response :json}))))) + (is (= {:status 200 :body {:request :default, :response :default}} + (call (request "application/transit" "application/transit" {:request :default :response :default}))))) + (testing "request validation fails" + (is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]} + (call (request "application/edn" "application/json" {:request :json :response :json})))) + (is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]} + (call (request "application/json" "application/json" {:request :edn :response :json})))) + (is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]} + (call (request "application/transit" "application/json" {:request :edn :response :json}))))) + (testing "response validation fails" + (is (= {:type :reitit.coercion/response-coercion :in [:response :body]} + (call (request "application/json" "application/json" {:request :json :response :edn})))) + (is (= {:type :reitit.coercion/response-coercion :in [:response :body]} + (call (request "application/json" "application/edn" {:request :json :response :json})))) + (is (= {:type :reitit.coercion/response-coercion :in [:response :body]} + (call (request "application/json" "application/transit" {:request :json :response :json})))))))) + (testing "explicit response content type" + (let [response (atom nil) + app (ring/ring-handler + (ring/router + ["/foo" {:post {:responses {200 {:content {"application/json" {:schema json-response} + "application/edn" {:schema edn-response} + :default {:schema default-response}}}} + :handler (fn [req] + @response)}}] + {:validate reitit.ring.spec/validate + :data {:middleware [rrc/coerce-request-middleware + rrc/coerce-response-middleware] + :coercion coercion}})) + call (fn [request] (try (app request) (catch ExceptionInfo e + #_(ex-data e) (select-keys (ex-data e) [:type :in])))) - request (fn [request-format response-format body] + request (fn [request-format body resp] + (reset! response resp) {:request-method :post :uri "/foo" :muuntaja/request {:format request-format} - :muuntaja/response {:format response-format} - :body-params body}) - normalize-json (fn[body] - (-> body j/write-value-as-string (j/read-value j/keyword-keys-object-mapper)))] - (testing "succesful call" - (is (= {:status 200 :body {:request "json", :response "json"}} - (normalize-json (call (request "application/json" "application/json" {:request :json :response :json}))))) - (is (= {:status 200 :body {:request "edn", :response "json"}} - (normalize-json (call (request "application/edn" "application/json" {:request :edn :response :json}))))) - (is (= {:status 200 :body {:request :default, :response :default}} - (call (request "application/transit" "application/transit" {:request :default :response :default}))))) - (testing "request validation fails" - (is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]} - (call (request "application/edn" "application/json" {:request :json :response :json})))) - (is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]} - (call (request "application/json" "application/json" {:request :edn :response :json})))) - (is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]} - (call (request "application/transit" "application/json" {:request :edn :response :json}))))) - (testing "response validation fails" - (is (= {:type :reitit.coercion/response-coercion :in [:response :body]} - (call (request "application/json" "application/json" {:request :json :response :edn})))) - (is (= {:type :reitit.coercion/response-coercion :in [:response :body]} - (call (request "application/json" "application/edn" {:request :json :response :json})))) - (is (= {:type :reitit.coercion/response-coercion :in [:response :body]} - (call (request "application/json" "application/transit" {:request :json :response :json})))))))))))) + :body-params body})] + (testing "via :headers \"Content-Type\"" + (is (= {:status 200 :body {:request "json" :response "json"} :headers {"Content-Type" "application/json"}} + (normalize-json (call (request "application/json" {:request :json :response :json} {:status 200 :body {:request :json :response :json} :headers {"Content-Type" "application/json"}})))) + "valid reponse") + (is (= {:in [:response :body] :type :reitit.coercion/response-coercion} + (call (request "application/json" {:request :json :response :json} {:status 200 :body {:request :json :response :invalid} :headers {"Content-Type" "application/json"}}))) + "invalid reponse")) + (testing "via :muuntaja/content-type" + (is (= {:status 200 :body {:request "json" :response "json"} :muuntaja/content-type "application/json"} + (normalize-json (call (request "application/json" {:request :json :response :json} {:status 200 :body {:request :json :response :json} :muuntaja/content-type "application/json"})))) + "valid reponse") + (is (= {:in [:response :body] :type :reitit.coercion/response-coercion} + (call (request "application/json" {:request :json :response :json} {:status 200 :body {:request :json :response :invalid} :muuntaja/content-type "application/json"}))) + "invalid reponse"))))))))) #?(:clj @@ -801,3 +842,93 @@ (app) :body slurp (read-string))] (is (= data-edn (e2e (assoc data-edn :EXTRA "VALUE")))) (is (thrown? ExceptionInfo (e2e data-json)))))))) + +#?(:clj + (deftest muuntaja-per-content-type-coercion-test + ;; Test integration between per-content-type coercion and muuntaja. + ;; Malli-only for now. + (let [response (atom nil) + app (ring/ring-handler + (ring/router + ["/foo" {:post {:request {:content {"application/json" {:schema [:map [:request [:enum :json]]]} + "application/edn" {:schema [:map [:request [:enum :edn]]]} + :default {:schema [:map [:request [:enum :default]]]}}} + :responses {200 {:content {"application/json" {:schema [:map [:response [:enum :json]]]} + "application/edn" {:schema [:map [:response [:enum :edn]]]} + :default {}}}} + :handler (fn [req] @response)}}] + {:data {:middleware [reitit.ring.middleware.muuntaja/format-middleware + rrc/coerce-request-middleware + rrc/coerce-response-middleware] + :muuntaja muuntaja.core/instance + :coercion malli/coercion}})) + maybe-slurp #(if (instance? java.io.InputStream %) + (slurp %) + %) + call (fn [request resp] + (reset! response resp) + (try + (-> (merge {:request-method :post :uri "/foo"} request) + (update :body #(ByteArrayInputStream. (.getBytes % "UTF-8"))) + (app) :body (maybe-slurp)) + (catch ExceptionInfo e + #_(ex-data e) + (select-keys (ex-data e) [:in :type])))) + read-json #(j/read-value % (j/object-mapper {:decode-key-fn true}))] + (testing "response content-type defaults to json" + (is (= {:response "json"} + (read-json + (call {:headers {"content-type" "application/json"} + :body (j/write-value-as-string {:request :json})} + {:status 200 + :body {:response :json}})))) + (is (= {:response "json"} + (read-json + (call {:headers {"content-type" "application/edn"} + :body (pr-str {:request :edn})} + {:status 200 + :body {:response :json}})))) + (is (= {:in [:response :body] :type :reitit.coercion/response-coercion} + (call {:headers {"content-type" "application/json"} + :body (j/write-value-as-string {:request :json})} + {:status 200 + :body {:response :invalid}})))) + (testing "response content-type negotiated via accept header" + (is (= {:response "json"} + (read-json + (call {:headers {"content-type" "application/json" "accept" "application/json"} + :body (j/write-value-as-string {:request :json})} + {:status 200 + :body {:response :json}})))) + (is (= {:response :edn} + (read-string + (call {:headers {"content-type" "application/json" "accept" "application/edn"} + :body (j/write-value-as-string {:request :json})} + {:status 200 + :body {:response :edn}})))) + (is (= {:in [:response :body] :type :reitit.coercion/response-coercion} + (call {:headers {"content-type" "application/json" "accept" "application/edn"} + :body (j/write-value-as-string {:request :json})} + {:status 200 + :body {:response :invalid}})))) + (testing "response content-type set via :muuntaja/content-type" + (is (= {:response :edn} + (read-string + (call {:headers {"content-type" "application/json" "accept" "application/json"} + :body (j/write-value-as-string {:request :json})} + {:status 200 + :muuntaja/content-type "application/edn" + :body {:response :edn}})))) + (is (= {:in [:response :body] :type :reitit.coercion/response-coercion} + (call {:headers {"content-type" "application/json" "accept" "application/json"} + :body (j/write-value-as-string {:request :json})} + {:status 200 + :muuntaja/content-type "application/edn" + :body {:response :invalid}})))) + (testing "response content-type set via Content-Type header. muuntaja disabled for response." + (is (= "custom data" + (call {:headers {"content-type" "application/json" "accept" "application/json"} + :body (j/write-value-as-string {:request :json})} + {:status 200 + :headers {"Content-Type" "application/custom"} + :body "custom data"}))))))) From 39c5ae86a4047e7e6b3a4cf927f609a8103e6eb5 Mon Sep 17 00:00:00 2001 From: Joel Kaasinen Date: Wed, 29 Oct 2025 10:45:47 +0200 Subject: [PATCH 2/4] doc: return random content-type from openapi example /pizza --- examples/openapi/src/example/server.clj | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/examples/openapi/src/example/server.clj b/examples/openapi/src/example/server.clj index b2419cf0..bfd65b3a 100644 --- a/examples/openapi/src/example/server.clj +++ b/examples/openapi/src/example/server.clj @@ -52,23 +52,34 @@ {:get {:summary "Fetch a pizza | Multiple content-types, multiple examples" :responses {200 {:description "Fetch a pizza as json or EDN" :content {"application/json" {:schema [:map + [:format [:enum :json]] [:color :keyword] [:pineapple :boolean]] :examples {:white {:description "White pizza with pineapple" - :value {:color :white + :value {:format :json + :color :white :pineapple true}} :red {:description "Red pizza" - :value {:color :red + :value {:format :json + :color :red :pineapple false}}}} "application/edn" {:schema [:map + [:format [:enum :edn]] [:color :keyword] [:pineapple :boolean]] :examples {:red {:description "Red pizza with pineapple" - :value (pr-str {:color :red :pineapple true})}}}}}} + :value (pr-str {:format :edn :color :red :pineapple true})}}}}}} :handler (fn [_request] - {:status 200 - :body {:color :red - :pineapple true}})} + (rand-nth [{:status 200 + :muuntaja/content-type "application/json" + :body {:format :json + :color :red + :pineapple true}} + {:status 200 + :muuntaja/content-type "application/edn" + :body {:format :edn + :color :red + :pineapple true}}]))} :post {:summary "Create a pizza | Multiple content-types, multiple examples | Default response schema" :request {:description "Create a pizza using json or EDN" :content {"application/json" {:schema [:map From ae52000b299f50e87678cd3eef47e1a5e498a345 Mon Sep 17 00:00:00 2001 From: Joel Kaasinen Date: Wed, 29 Oct 2025 10:57:59 +0200 Subject: [PATCH 3/4] doc: update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76fef901..c32d7dce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ We use [Break Versioning][breakver]. The version numbers follow a `. Date: Fri, 31 Oct 2025 09:38:56 +0200 Subject: [PATCH 4/4] test: improve per-content-type coercion tests - The :headers "Content-Type" case in per-content-type-test was unrealistic. Ring would've thrown an exception at the non-string :body. - Test response Content-Type in muuntaja-per-content-type-coercion-test --- test/cljc/reitit/ring_coercion_test.cljc | 102 +++++++++++------------ 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/test/cljc/reitit/ring_coercion_test.cljc b/test/cljc/reitit/ring_coercion_test.cljc index 93af98a9..f74f65b9 100644 --- a/test/cljc/reitit/ring_coercion_test.cljc +++ b/test/cljc/reitit/ring_coercion_test.cljc @@ -705,13 +705,6 @@ :uri "/foo" :muuntaja/request {:format request-format} :body-params body})] - (testing "via :headers \"Content-Type\"" - (is (= {:status 200 :body {:request "json" :response "json"} :headers {"Content-Type" "application/json"}} - (normalize-json (call (request "application/json" {:request :json :response :json} {:status 200 :body {:request :json :response :json} :headers {"Content-Type" "application/json"}})))) - "valid reponse") - (is (= {:in [:response :body] :type :reitit.coercion/response-coercion} - (call (request "application/json" {:request :json :response :json} {:status 200 :body {:request :json :response :invalid} :headers {"Content-Type" "application/json"}}))) - "invalid reponse")) (testing "via :muuntaja/content-type" (is (= {:status 200 :body {:request "json" :response "json"} :muuntaja/content-type "application/json"} (normalize-json (call (request "application/json" {:request :json :response :json} {:status 200 :body {:request :json :response :json} :muuntaja/content-type "application/json"})))) @@ -870,65 +863,72 @@ (try (-> (merge {:request-method :post :uri "/foo"} request) (update :body #(ByteArrayInputStream. (.getBytes % "UTF-8"))) - (app) :body (maybe-slurp)) + (app)) (catch ExceptionInfo e #_(ex-data e) (select-keys (ex-data e) [:in :type])))) - read-json #(j/read-value % (j/object-mapper {:decode-key-fn true}))] + read-json #(j/read-value % (j/object-mapper {:decode-key-fn true})) + json-response? (fn [resp] + (and (.startsWith (get-in resp [:headers "Content-Type"]) "application/json") ;; ignore the ;charset=utf-8 part + (= {:response "json"} (read-json (maybe-slurp (:body resp)))))) + edn-response? (fn [resp] + (and (.startsWith (get-in resp [:headers "Content-Type"]) "application/edn") ;; ignore the ;charset=utf-8 part + (= {:response :edn} (read-string (maybe-slurp (:body resp)))))) + custom-response? (fn [resp] + (and (= (get-in resp [:headers "Content-Type"]) "application/custom") + (= "custom data" (maybe-slurp (:body resp)))))] (testing "response content-type defaults to json" - (is (= {:response "json"} - (read-json - (call {:headers {"content-type" "application/json"} - :body (j/write-value-as-string {:request :json})} - {:status 200 - :body {:response :json}})))) - (is (= {:response "json"} - (read-json - (call {:headers {"content-type" "application/edn"} - :body (pr-str {:request :edn})} - {:status 200 - :body {:response :json}})))) + (is (json-response? + (call {:headers {"content-type" "application/json"} + :body (j/write-value-as-string {:request :json})} + {:status 200 + :body {:response :json}}))) + (is (json-response? + (call {:headers {"content-type" "application/edn"} + :body (pr-str {:request :edn})} + {:status 200 + :body {:response :json}}))) (is (= {:in [:response :body] :type :reitit.coercion/response-coercion} (call {:headers {"content-type" "application/json"} - :body (j/write-value-as-string {:request :json})} - {:status 200 - :body {:response :invalid}})))) + :body (j/write-value-as-string {:request :json})} + {:status 200 + :body {:response :invalid}})) + "invalid response")) (testing "response content-type negotiated via accept header" - (is (= {:response "json"} - (read-json - (call {:headers {"content-type" "application/json" "accept" "application/json"} - :body (j/write-value-as-string {:request :json})} - {:status 200 - :body {:response :json}})))) - (is (= {:response :edn} - (read-string - (call {:headers {"content-type" "application/json" "accept" "application/edn"} - :body (j/write-value-as-string {:request :json})} - {:status 200 - :body {:response :edn}})))) + (is (json-response? + (call {:headers {"content-type" "application/json" "accept" "application/json"} + :body (j/write-value-as-string {:request :json})} + {:status 200 + :body {:response :json}}))) + (is (edn-response? + (call {:headers {"content-type" "application/json" "accept" "application/edn"} + :body (j/write-value-as-string {:request :json})} + {:status 200 + :body {:response :edn}}))) (is (= {:in [:response :body] :type :reitit.coercion/response-coercion} (call {:headers {"content-type" "application/json" "accept" "application/edn"} :body (j/write-value-as-string {:request :json})} {:status 200 - :body {:response :invalid}})))) + :body {:response :invalid}})) + "invalid response")) (testing "response content-type set via :muuntaja/content-type" - (is (= {:response :edn} - (read-string - (call {:headers {"content-type" "application/json" "accept" "application/json"} - :body (j/write-value-as-string {:request :json})} - {:status 200 - :muuntaja/content-type "application/edn" - :body {:response :edn}})))) + (is (edn-response? + (call {:headers {"content-type" "application/json" "accept" "application/json"} + :body (j/write-value-as-string {:request :json})} + {:status 200 + :muuntaja/content-type "application/edn" + :body {:response :edn}}))) (is (= {:in [:response :body] :type :reitit.coercion/response-coercion} (call {:headers {"content-type" "application/json" "accept" "application/json"} :body (j/write-value-as-string {:request :json})} {:status 200 :muuntaja/content-type "application/edn" - :body {:response :invalid}})))) + :body {:response :invalid}})) + "invalid response")) (testing "response content-type set via Content-Type header. muuntaja disabled for response." - (is (= "custom data" - (call {:headers {"content-type" "application/json" "accept" "application/json"} - :body (j/write-value-as-string {:request :json})} - {:status 200 - :headers {"Content-Type" "application/custom"} - :body "custom data"}))))))) + (is (custom-response? + (call {:headers {"content-type" "application/json" "accept" "application/json"} + :body (j/write-value-as-string {:request :json})} + {:status 200 + :headers {"Content-Type" "application/custom"} + :body "custom data"})))))))