(ns reitit.openapi-test (:require [clojure.java.shell :as shell] [clojure.test :refer [deftest is testing]] [jsonista.core :as j] [malli.core :as mc] [matcher-combinators.test :refer [match?]] [matcher-combinators.matchers :as matchers] [muuntaja.core :as m] [reitit.coercion.malli :as malli] [reitit.coercion.schema :as schema] [reitit.coercion.spec :as spec] [reitit.http.interceptors.multipart] [reitit.openapi :as openapi] [reitit.ring :as ring] [reitit.ring.malli] [reitit.ring.spec] [reitit.ring.coercion :as rrc] [reitit.swagger-ui :as swagger-ui] [schema.core :as s] [schema-tools.core] [spec-tools.core :as st] [spec-tools.data-spec :as ds])) (defn validate "Returns nil if data is a valid openapi spec, otherwise validation result" [data] (let [file (java.io.File/createTempFile "reitit-openapi" ".json")] (.deleteOnExit file) (spit file (j/write-value-as-string data)) (let [result (shell/sh "npx" "-p" "@seriousme/openapi-schema-validator" "validate-api" (.getPath file))] (when-not (zero? (:exit result)) (j/read-value (:out result)))))) (def app (ring/ring-handler (ring/router ["/api" {:openapi {:id ::math}} ["/openapi.json" {:get {:no-doc true :openapi {:info {:title "my-api" :version "0.0.1"}} :handler (openapi/create-openapi-handler)}}] ["/spec" {:coercion spec/coercion} ["/plus/:z" {:get {:summary "plus" :tags [:plus :spec] :parameters {:query {:x int?, :y int?} :path {:z int?}} :openapi {:operationId "spec-plus" :deprecated true :responses {400 {:description "kosh" :content {"application/json" {:schema {:type "string"}}}}}} :responses {200 {:description "success" :body {:total int?}} 500 {:description "fail"}} :handler (fn [{{{:keys [x y]} :query {:keys [z]} :path} :parameters}] {:status 200, :body {:total (+ x y z)}})} :post {:summary "plus with body" :parameters {:body (ds/maybe [int?]) :path {:z int?}} :openapi {:responses {400 {:content {"application/json" {:schema {:type "string"}}} :description "kosh"}}} :responses {200 {:description "success" :body {:total int?}} 500 {:description "fail"} 504 {:description "default" :content {:default {:schema {:error string?}}} :body {:masked string?}}} :handler (fn [{{{:keys [z]} :path xs :body} :parameters}] {:status 200, :body {:total (+ (reduce + xs) z)}})}}]] ["/malli" {:coercion malli/coercion} ["/plus/*z" {:get {:summary "plus" :tags [:plus :malli] :parameters {:query [:map [:x int?] [:y int?]] :path [:map [:z int?]]} :openapi {:responses {400 {:description "kosh" :content {"application/json" {:schema {:type "string"}}}}}} :responses {200 {:description "success" :body [:map [:total int?]]} 500 {:description "fail"}} :handler (fn [{{{:keys [x y]} :query {:keys [z]} :path} :parameters}] {:status 200, :body {:total (+ x y z)}})} :post {:summary "plus with body" :parameters {:body [:maybe [:vector int?]] :path [:map [:z int?]]} :openapi {:responses {400 {:description "kosh" :content {"application/json" {:schema {:type "string"}}}}}} :responses {200 {:description "success" :body [:map [:total int?]]} 500 {:description "fail"} 504 {:description "default" :content {:default {:schema {:error string?}}} :body {:masked string?}}} :handler (fn [{{{:keys [z]} :path xs :body} :parameters}] {:status 200, :body {:total (+ (reduce + xs) z)}})}}]] ["/schema" {:coercion schema/coercion} ["/plus/*z" {:get {:summary "plus" :tags [:plus :schema] :parameters {:query {:x s/Int, :y s/Int} :path {:z s/Int}} :openapi {:responses {400 {:content {"application/json" {:schema {:type "string"}}} :description "kosh"}}} :responses {200 {:description "success" :body {:total s/Int}} 500 {:description "fail"}} :handler (fn [{{{:keys [x y]} :query {:keys [z]} :path} :parameters}] {:status 200, :body {:total (+ x y z)}})} :post {:summary "plus with body" :parameters {:body (s/maybe [s/Int]) :path {:z s/Int}} :openapi {:responses {400 {:content {"application/json" {:schema {:type "string"}}} :description "kosh"}}} :responses {200 {:description "success" :body {:total s/Int}} 500 {:description "fail"} 504 {:description "default" :content {:default {:schema {:error s/Str}}} :body {:masked s/Str}}} :handler (fn [{{{:keys [z]} :path xs :body} :parameters}] {:status 200, :body {:total (+ (reduce + xs) z)}})}}]]] {:validate reitit.ring.spec/validate :data {:middleware [openapi/openapi-feature rrc/coerce-exceptions-middleware rrc/coerce-request-middleware rrc/coerce-response-middleware]}}))) (deftest openapi-test (testing "endpoints work" (testing "malli" (is (= {:body {:total 6}, :status 200} (app {:request-method :get :uri "/api/malli/plus/3" :query-params {:x "2", :y "1"}}))) (is (= {:body {:total 7}, :status 200} (app {:request-method :post :uri "/api/malli/plus/3" :body-params [1 3]}))))) (testing "openapi-spec" (let [spec (:body (app {:request-method :get :uri "/api/openapi.json"})) expected {:x-id #{::math} :openapi "3.1.0" :info {:title "my-api" :version "0.0.1"} :paths {"/api/spec/plus/{z}" {:get {:parameters [{:in "query" :name "x" :required true :schema {:type "integer" :format "int64"}} {:in "query" :name "y" :required true :schema {:type "integer" :format "int64"}} {:in "path" :name "z" :required true :schema {:type "integer" :format "int64"}}] :responses {200 {:description "success" :content {"application/json" {:schema {:type "object" :properties {"total" {:format "int64" :type "integer"}} :required ["total"]}}}} 400 {:description "kosh" :content {"application/json" {:schema {:type "string"}}}} 500 {:description "fail"}} :operationId "spec-plus" :deprecated true :tags [:plus :spec] :summary "plus"} :post {:parameters [{:in "path" :name "z" :required true :schema {:type "integer" :format "int64"}}] :requestBody {:content {"application/json" {:schema {:oneOf [{:items {:type "integer" :format "int64"} :type "array"} {:type "null"}]}}}} :responses {200 {:description "success" :content {"application/json" {:schema {:properties {"total" {:format "int64" :type "integer"}} :required ["total"] :type "object"}}}} 400 {:content {"application/json" {:schema {:type "string"}}} :description "kosh"} 500 {:description "fail"} 504 {:description "default" :content {"application/json" {:schema {:properties {"error" {:type "string"}} :required ["error"] :type "object"}}}}} :summary "plus with body"}} "/api/malli/plus/{z}" {:get {:parameters [{:in "query" :name :x :required true :schema {:type "integer"}} {:in "query" :name :y :required true :schema {:type "integer"}} {:in "path" :name :z :required true :schema {:type "integer"}}] :responses {200 {:description "success" :content {"application/json" {:schema {:type "object" :properties {:total {:type "integer"}} :additionalProperties false :required [:total]}}}} 400 {:description "kosh" :content {"application/json" {:schema {:type "string"}}}} 500 {:description "fail"}} :tags [:plus :malli] :summary "plus"} :post {:parameters [{:in "path" :name :z :schema {:type "integer"} :required true}] :requestBody {:content {"application/json" {:schema {:oneOf [{:items {:type "integer"} :type "array"} {:type "null"}]}}}} :responses {200 {:description "success" :content {"application/json" {:schema {:properties {:total {:type "integer"}} :required [:total] :additionalProperties false :type "object"}}}} 400 {:description "kosh" :content {"application/json" {:schema {:type "string"}}}} 500 {:description "fail"} 504 {:description "default" :content {"application/json" {:schema {:additionalProperties false :properties {:error {:type "string"}} :required [:error] :type "object"}}}}} :summary "plus with body"}} "/api/schema/plus/{z}" {:get {:parameters [{:in "query" :name "x" :required true :schema {:format "int32" :type "integer"}} {:in "query" :name "y" :required true :schema {:type "integer" :format "int32"}} {:in "path" :name "z" :required true :schema {:type "integer" :format "int32"}}] :responses {200 {:description "success" :content {"application/json" {:schema {:additionalProperties false :properties {"total" {:format "int32" :type "integer"}} :required ["total"] :type "object"}}}} 400 {:description "kosh" :content {"application/json" {:schema {:type "string"}}}} 500 {:description "fail"}} :tags [:plus :schema] :summary "plus"} :post {:parameters [{:in "path" :name "z" :required true :schema {:type "integer" :format "int32"}}] :requestBody {:content {"application/json" {:schema {:oneOf [{:type "array" :items {:type "integer" :format "int32"}} {:type "null"}]}}}} :responses {200 {:description "success" :content {"application/json" {:schema {:properties {"total" {:format "int32" :type "integer"}} :additionalProperties false :required ["total"] :type "object"}}}} 400 {:description "kosh" :content {"application/json" {:schema {:type "string"}}}} 500 {:description "fail"} 504 {:description "default" :content {"application/json" {:schema {:additionalProperties false :properties {"error" {:type "string"}} :required ["error"] :type "object"}}}}} :summary "plus with body"}}}}] (is (= expected spec)) (is (= nil (validate spec)))))) (defn spec-paths [app uri] (-> {:request-method :get, :uri uri} app :body :paths keys)) (deftest multiple-openapi-apis-test (let [ping-route ["/ping" {:get (constantly "ping")}] spec-route ["/openapi.json" {:get {:no-doc true :handler (openapi/create-openapi-handler)}}] app (ring/ring-handler (ring/router [["/common" {:openapi {:id #{::one ::two}}} ping-route] ["/one" {:openapi {:id ::one}} ping-route spec-route] ["/two" {:openapi {:id ::two}} ping-route spec-route ["/deep" {:openapi {:id ::one}} ping-route]] ["/one-two" {:openapi {:id #{::one ::two}}} spec-route]]))] (is (= ["/common/ping" "/one/ping" "/two/deep/ping"] (spec-paths app "/one/openapi.json"))) (is (= ["/common/ping" "/two/ping"] (spec-paths app "/two/openapi.json"))) (is (= ["/common/ping" "/one/ping" "/two/ping" "/two/deep/ping"] (spec-paths app "/one-two/openapi.json"))))) (deftest openapi-ui-config-test (let [app (swagger-ui/create-swagger-ui-handler {:path "/" :url "/openapi.json" :config {:jsonEditor true}})] (is (= 302 (:status (app {:request-method :get, :uri "/"})))) (is (= 200 (:status (app {:request-method :get, :uri "/index.html"})))) (is (= {:jsonEditor true, :url "/openapi.json"} (->> {:request-method :get, :uri "/config.json"} (app) :body (m/decode m/instance "application/json")))))) (deftest without-openapi-id-test (let [app (ring/ring-handler (ring/router [["/ping" {:get (constantly "ping")}] ["/openapi.json" {:get {:no-doc true :handler (openapi/create-openapi-handler)}}]]))] (is (= ["/ping"] (spec-paths app "/openapi.json"))) (is (= #{::openapi/default} (-> {:request-method :get :uri "/openapi.json"} (app) :body :x-id))))) (deftest with-options-endpoint-test (let [app (ring/ring-handler (ring/router [["/ping" {:options (constantly "options")}] ["/pong" (constantly "options")] ["/openapi.json" {:get {:no-doc true :handler (openapi/create-openapi-handler)}}]]))] (is (= ["/ping" "/pong"] (spec-paths app "/openapi.json"))) (is (= #{::openapi/default} (-> {:request-method :get :uri "/openapi.json"} (app) :body :x-id))))) (defn- normalize "Normalize format of openapi spec by converting it to json and back. Handles differences like :q vs \"q\" in openapi generation." [data] (-> data j/write-value-as-string (j/read-value j/keyword-keys-object-mapper))) (deftest all-parameter-types-test (doseq [[coercion ->schema] [[#'malli/coercion (fn [nom] [:map [nom [:string {:description (str "description " nom)}]]])] [#'schema/coercion (fn [nom] {nom (schema-tools.core/schema s/Str {:description (str "description " nom)})})] [#'spec/coercion (fn [nom] {nom (st/spec {:spec string? :description (str "description " nom)})})]]] (testing (str coercion) (let [app (ring/ring-handler (ring/router [["/parameters" {:post {:decription "parameters" :coercion @coercion :parameters {:query (->schema :q) :body (->schema :b) :header (->schema :h) :cookie (->schema :c) :path (->schema :p)} :responses {200 {:description "success" :body (->schema :ok)}} :handler identity}}] ["/openapi.json" {:get {:handler (openapi/create-openapi-handler) :openapi {:info {:title "" :version "0.0.1"}} :no-doc true}}]] {:data {:middleware [openapi/openapi-feature]}})) spec (-> {:request-method :get :uri "/openapi.json"} app :body)] (testing "all non-body parameters" (is (match? [{:in "query" :name "q" :required true :description "description :q" :schema {:type "string"}} {:in "header" :name "h" :required true :description "description :h" :schema {:type "string"}} {:in "cookie" :name "c" :required true :description "description :c" :schema {:type "string"}} {:in "path" :name "p" :required true :description "description :p" :schema {:type "string"}}] (-> spec (get-in [:paths "/parameters" :post :parameters]) normalize)))) (testing "body parameter" (is (match? (merge {:type "object" :properties {:b {:type "string"}} :required ["b"]} ;; spec outputs open schemas (when-not (#{#'spec/coercion} coercion) {:additionalProperties false})) (-> spec (get-in [:paths "/parameters" :post :requestBody :content "application/json" :schema]) normalize)))) (testing "body response" (is (match? (merge {:type "object" :properties {:ok {:type "string"}} :required ["ok"]} (when-not (#{#'spec/coercion} coercion) {:additionalProperties false})) (-> spec (get-in [:paths "/parameters" :post :responses 200 :content "application/json" :schema]) normalize)))) (testing "spec is valid" (is (nil? (validate spec)))))))) (deftest examples-test (doseq [[coercion ->schema] [[#'malli/coercion (fn [nom] [:map {:json-schema/example {nom "EXAMPLE2"}} [nom [:string {:json-schema/example "EXAMPLE"}]]])] [#'schema/coercion (fn [nom] (schema-tools.core/schema {nom (schema-tools.core/schema s/Str {:openapi/example "EXAMPLE"})} {:openapi/example {nom "EXAMPLE2"}}))] [#'spec/coercion (fn [nom] (assoc (ds/spec ::foo {nom (st/spec string? {:openapi/example "EXAMPLE"})}) :openapi/example {nom "EXAMPLE2"}))]]] (testing (str coercion) (let [app (ring/ring-handler (ring/router [["/examples" {:post {:decription "examples" :openapi/request-content-types ["application/json" "application/edn"] :openapi/response-content-types ["application/json" "application/edn"] :coercion @coercion :request {:content {"application/json" {:schema (->schema :b) :examples {"named-example" {:description "a named example" :value {:b "named"}}}} :default {:schema (->schema :b2) :examples {"default-example" {:description "default example" :value {:b2 "named"}}}}}} :parameters {:query (->schema :q)} :responses {200 {:description "success" :content {"application/json" {:schema (->schema :ok) :examples {"response-example" {:value {:ok "response"}}}} :default {:schema (->schema :ok) :examples {"default-response-example" {:value {:ok "default"}}}}}}} :handler identity}}] ["/openapi.json" {:get {:handler (openapi/create-openapi-handler) :openapi {:info {:title "" :version "0.0.1"}} :no-doc true}}]] {:data {:middleware [openapi/openapi-feature]}})) spec (-> {:request-method :get :uri "/openapi.json"} app :body)] (testing "query parameter" (is (match? [{:in "query" :name "q" :required true :schema {:type "string" :example "EXAMPLE"}}] (-> spec (get-in [:paths "/examples" :post :parameters]) normalize)))) (testing "body parameter" (is (match? {:schema {:type "object" :properties {:b {:type "string" :example "EXAMPLE"}} :required ["b"] :example {:b "EXAMPLE2"}} :examples {:named-example {:description "a named example" :value {:b "named"}}}} (-> spec (get-in [:paths "/examples" :post :requestBody :content "application/json"]) normalize))) (testing "default" (is (match? {:schema {:type "object" :properties {:b2 {:type "string" :example "EXAMPLE"}} :required ["b2"] :example {:b2 "EXAMPLE2"}} :examples {:default-example {:description "default example" :value {:b2 "named"}}}} (-> spec (get-in [:paths "/examples" :post :requestBody :content "application/edn"]) normalize))))) (testing "body response" (is (match? {:schema {:type "object" :properties {:ok {:type "string" :example "EXAMPLE"}} :required ["ok"] :example {:ok "EXAMPLE2"}} :examples {:response-example {:value {:ok "response"}}}} (-> spec (get-in [:paths "/examples" :post :responses 200 :content "application/json"]) normalize))) (testing "default" (is (match? {:schema {:type "object" :properties {:ok {:type "string" :example "EXAMPLE"}} :required ["ok"] :example {:ok "EXAMPLE2"}} :examples {:default-response-example {:value {:ok "default"}}}} (-> spec (get-in [:paths "/examples" :post :responses 200 :content "application/edn"]) normalize))))) (testing "spec is valid" (is (nil? (validate spec)))))))) (deftest multipart-test (doseq [[coercion file-schema string-schema] [[#'malli/coercion reitit.ring.malli/bytes-part :string] [#'schema/coercion (schema-tools.core/schema {:filename s/Str :content-type s/Str :bytes s/Num} {:openapi {:type "string" :format "binary"}}) s/Str] [#'spec/coercion reitit.http.interceptors.multipart/bytes-part string?]]] (testing (str coercion) (let [app (ring/ring-handler (ring/router [["/upload" {:post {:decription "upload" :coercion @coercion :parameters {:multipart {:file file-schema :more string-schema}} :handler identity}}] ["/openapi.json" {:get {:handler (openapi/create-openapi-handler) :openapi {:info {:title "" :version "0.0.1"}} :no-doc true}}]] {:data {:middleware [openapi/openapi-feature]}})) spec (-> {:request-method :get :uri "/openapi.json"} app :body)] (testing "multipart body" (is (nil? (get-in spec [:paths "/upload" :post :parameters]))) (is (= (merge {:type "object" :properties {:file {:type "string" :format "binary"} :more {:type "string"}} :required ["file" "more"]} (when-not (= #'spec/coercion coercion) {:additionalProperties false})) (-> spec (get-in [:paths "/upload" :post :requestBody :content "multipart/form-data" :schema]) normalize)))) (testing "spec is valid" (is (nil? (validate spec)))))))) (deftest per-content-type-test (doseq [[coercion ->schema] [[malli/coercion (fn [nom] [:map [nom :string]])] [schema/coercion (fn [nom] {nom s/Str})] [spec/coercion (fn [nom] {nom string?})]]] (testing (str coercion) (let [app (ring/ring-handler (ring/router [["/parameters" {:post {:description "parameters" :coercion coercion :request {:content {"application/json" {:schema (->schema :b)} "application/edn" {:schema (->schema :c)}}} :responses {200 {:description "success" :content {"application/json" {:schema (->schema :ok)} "application/edn" {:schema (->schema :edn)}}}} :handler (fn [req] {:status 200 :body (-> req :parameters :request)})}}] ["/openapi.json" {:get {:handler (openapi/create-openapi-handler) :openapi {:info {:title "" :version "0.0.1"}} :no-doc true}}]] {:validate reitit.ring.spec/validate :data {:middleware [openapi/openapi-feature rrc/coerce-request-middleware rrc/coerce-response-middleware]}})) spec (-> {:request-method :get :uri "/openapi.json"} app :body) spec-coercion (= coercion spec/coercion)] (testing "body parameter" (is (= (merge {:type "object" :properties {:b {:type "string"}} :required ["b"]} (when-not spec-coercion {:additionalProperties false})) (-> spec (get-in [:paths "/parameters" :post :requestBody :content "application/json" :schema]) normalize))) (is (= (merge {:type "object" :properties {:c {:type "string"}} :required ["c"]} (when-not spec-coercion {:additionalProperties false})) (-> spec (get-in [:paths "/parameters" :post :requestBody :content "application/edn" :schema]) normalize)))) (testing "body response" (is (= (merge {:type "object" :properties {:ok {:type "string"}} :required ["ok"]} (when-not spec-coercion {:additionalProperties false})) (-> spec (get-in [:paths "/parameters" :post :responses 200 :content "application/json" :schema]) normalize))) (is (= (merge {:type "object" :properties {:edn {:type "string"}} :required ["edn"]} (when-not spec-coercion {:additionalProperties false})) (-> spec (get-in [:paths "/parameters" :post :responses 200 :content "application/edn" :schema]) normalize)))) (testing "validation" (let [query {:request-method :post :uri "/parameters" :muuntaja/request {:format "application/json"} :muuntaja/response {:format "application/json"} :body-params {:b "x"}}] (testing "of output" (is (= {:type :reitit.coercion/response-coercion :in [:response :body]} (try (app query) (catch clojure.lang.ExceptionInfo e (select-keys (ex-data e) [:type :in])))))) (testing "of input" (is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]} (try (app (assoc query :body-params {:z 1})) (catch clojure.lang.ExceptionInfo e (select-keys (ex-data e) [:type :in])))))))) (testing "spec is valid" (is (nil? (validate spec)))))))) (deftest default-content-type-test (doseq [[coercion ->schema] [[malli/coercion (fn [nom] [:map [nom :string]])] [schema/coercion (fn [nom] {nom s/Str})] [spec/coercion (fn [nom] {nom string?})]]] (testing (str coercion) (let [app (ring/ring-handler (ring/router [["/explicit-content-type" {:post {:description "parameters" :coercion coercion :request {:content {"application/json" {:schema (->schema :b)} "application/edn" {:schema (->schema :c)}}} :responses {200 {:description "success" :content {"application/json" {:schema (->schema :ok)} "application/edn" {:schema (->schema :edn)}}}} :handler (fn [req] {:status 200 :body (-> req :parameters :request)})}}] ["/muuntaja" {:post {:description "default content types from muuntaja" :coercion coercion ;;; TODO: test the :parameters syntax :request {:content {:default {:schema (->schema :b)} "application/reitit-request" {:schema (->schema :ok)}}} :responses {200 {:description "success" :content {:default {:schema (->schema :ok)} "application/reitit-response" {:schema (->schema :ok)}}}} :handler (fn [req] {:status 200 :body (-> req :parameters :request)})}}] ["/override-default-content-type" {:post {:description "override default content types from muuntaja" :coercion coercion :openapi/request-content-types ["application/request"] :openapi/response-content-types ["application/response"] ;;; TODO: test the :parameters syntax :request {:content {:default {:schema (->schema :b)}}} :responses {200 {:description "success" :content {:default {:schema (->schema :ok)}}}} :handler (fn [req] {:status 200 :body (-> req :parameters :request)})}}] ["/legacy" {:post {:description "default content types from muuntaja, legacy syntax" :coercion coercion ;;; TODO: test the :parameters syntax :request {:body {:schema (->schema :b)}} :responses {200 {:description "success" :body {:schema (->schema :ok)}}} :handler (fn [req] {:status 200 :body (-> req :parameters :request)})}}] ["/openapi.json" {:get {:handler (openapi/create-openapi-handler) :openapi {:info {:title "" :version "0.0.1"}} :no-doc true}}]] {:validate reitit.ring.spec/validate :data {:muuntaja (m/create (-> m/default-options (update-in [:formats] select-keys ["application/transit+json"]) (assoc :default-format "application/transit+json"))) :middleware [openapi/openapi-feature rrc/coerce-request-middleware rrc/coerce-response-middleware]}})) spec (-> {:request-method :get :uri "/openapi.json"} app :body) spec-coercion (= coercion spec/coercion)] (testing "explicit content types" (testing "body parameter" (is (= ["application/edn" "application/json"] (-> spec (get-in [:paths "/explicit-content-type" :post :requestBody :content]) keys sort)))) (testing "body response" (is (= ["application/edn" "application/json"] (-> spec (get-in [:paths "/explicit-content-type" :post :responses 200 :content]) keys sort))))) (testing "muuntaja content types" (testing "body parameter" (is (= ["application/transit+json" "application/reitit-request"] (-> spec (get-in [:paths "/muuntaja" :post :requestBody :content]) keys)))) (testing "body response" (is (= ["application/transit+json" "application/reitit-response"] (-> spec (get-in [:paths "/muuntaja" :post :responses 200 :content]) keys))))) (testing "overridden muuntaja content types" (testing "body parameter" (is (= ["application/request"] (-> spec (get-in [:paths "/override-default-content-type" :post :requestBody :content]) keys)))) (testing "body response" (is (= ["application/response"] (-> spec (get-in [:paths "/override-default-content-type" :post :responses 200 :content]) keys))))) (testing "legacy syntax muuntaja content types" (testing "body parameter" (is (= ["application/transit+json"] (-> spec (get-in [:paths "/legacy" :post :requestBody :content]) keys)))) (testing "body response" (is (= ["application/transit+json"] (-> spec (get-in [:paths "/legacy" :post :responses 200 :content]) keys))))) (testing "spec is valid" (is (nil? (validate spec)))))))) (deftest recursive-test ;; Recursive schemas only properly supported for malli ;; See https://github.com/metosin/schema-tools/issues/41 (let [app (ring/ring-handler (ring/router [["/parameters" {:post {:description "parameters" :coercion malli/coercion :request {:body [:schema {:registry {"friend" [:map [:age int?] [:pet [:ref "pet"]]] "pet" [:map [:name :string] [:friends [:vector [:ref "friend"]]]]}} "friend"]} :handler (fn [req] {:status 200 :body (-> req :parameters :request)})}}] ["/openapi.json" {:get {:handler (openapi/create-openapi-handler) :openapi {:info {:title "" :version "0.0.1"}} :no-doc true}}]] {:validate reitit.ring.spec/validate :data {:middleware [openapi/openapi-feature rrc/coerce-request-middleware rrc/coerce-response-middleware]}})) spec (-> {:request-method :get :uri "/openapi.json"} app :body)] (is (= {:info {:title "" :version "0.0.1"} :openapi "3.1.0" :x-id #{:reitit.openapi/default} :paths {"/parameters" {:post {:description "parameters" :requestBody {:content {"application/json" {:schema {:$ref "#/components/schemas/friend"}}}}}}} :components {:schemas {"friend" {:properties {:age {:type "integer"} :pet {:$ref "#/components/schemas/pet"}} :required [:age :pet] :type "object"} "pet" {:properties {:friends {:items {:$ref "#/components/schemas/friend"} :type "array"} :name {:type "string"}} :required [:name :friends] :type "object"}}}} spec)) (testing "spec is valid" (is (nil? (validate spec)))))) (def Y :int) (def Plus [:map [:x :int] [:y #'Y]]) (deftest openapi-malli-tests (let [app (ring/ring-handler (ring/router [["/openapi.json" {:get {:no-doc true :openapi {:info {:title "" :version "0.0.1"}} :handler (openapi/create-openapi-handler)}}] ["/malli" {:coercion malli/coercion} ["/plus" {:post {:summary "plus with body" :request {:description "body description" :content {"application/json" {:schema {:x int?, :y int?} :examples {"1+1" {:value {:x 1, :y 1}} "1+2" {:value {:x 1, :y 2}}} :openapi {:example {:x 2, :y 2}}}}} :responses {200 {:description "success" :content {"application/json" {:schema {:total int?} :examples {"2" {:value {:total 2}} "3" {:value {:total 3}}} :openapi {:example {:total 4}}}}}} :handler (fn [request] (let [{:keys [x y]} (-> request :parameters :body)] {:status 200, :body {:total (+ x y)}}))}}]]] {:validate reitit.ring.spec/validate :data {:middleware [openapi/openapi-feature rrc/coerce-exceptions-middleware rrc/coerce-request-middleware rrc/coerce-response-middleware]}})) spec (:body (app {:request-method :get :uri "/openapi.json"}))] (is (= {"/malli/plus" {:post {:requestBody {:description "body description", :content {"application/json" {:schema {:type "object", :properties {:x {:type "integer"}, :y {:type "integer"}}, :required [:x :y], :additionalProperties false}, :examples {"1+1" {:value {:x 1, :y 1}} "1+2" {:value {:x 1, :y 2}}}, :example {:x 2, :y 2}}}}, :responses {200 {:description "success", :content {"application/json" {:schema {:type "object", :properties {:total {:type "integer"}}, :required [:total], :additionalProperties false}, :examples {"2" {:value {:total 2}}, "3" {:value {:total 3}}}, :example {:total 4}}}}}, :summary "plus with body"}}} (:paths spec))) (is (nil? (validate spec)))) (testing "ref schemas" (let [registry (merge (mc/base-schemas) (mc/type-schemas) {"plus" [:map [:x :int] [:y "y"]] "y" :int}) app (ring/ring-handler (ring/router [["/openapi.json" {:get {:no-doc true :openapi {:info {:title "" :version "0.0.1"}} :handler (openapi/create-openapi-handler)}}] ["/post" {:post {:coercion malli/coercion :parameters {:body (mc/schema "plus" {:registry registry})} :handler identity}}] ["/get" {:get {:coercion malli/coercion :parameters {:query (mc/schema "plus" {:registry registry})} :handler identity}}]])) spec (:body (app {:request-method :get :uri "/openapi.json"}))] (is (= {:openapi "3.1.0" :x-id #{:reitit.openapi/default} :info {:title "" :version "0.0.1"} :paths {"/get" {:get {:parameters [{:in "query" :name :x :required true :schema {:type "integer"}} {:in "query" :name :y :required true :schema {:$ref "#/components/schemas/y"}}]}} "/post" {:post {:requestBody {:content {"application/json" {:schema {:$ref "#/components/schemas/plus"}}}}}}} :components {:schemas {"y" {:type "integer"} "plus" {:type "object" :properties {:x {:type "integer"} :y {:$ref "#/components/schemas/y"}} :required [:x :y]}}}} spec)) (is (nil? (validate spec))))) (testing "var schemas" (let [app (ring/ring-handler (ring/router [["/openapi.json" {:get {:no-doc true :openapi {:info {:title "" :version "0.0.1"}} :handler (openapi/create-openapi-handler)}}] ["/post" {:post {:coercion malli/coercion :parameters {:body #'Plus} :handler identity}}] ["/get" {:get {:coercion malli/coercion :parameters {:query #'Plus} :handler identity}}]])) spec (:body (app {:request-method :get :uri "/openapi.json"}))] (is (= {:openapi "3.1.0" :x-id #{:reitit.openapi/default} :info {:title "" :version "0.0.1"} :paths {"/post" {:post {:requestBody {:content {"application/json" {:schema {:$ref "#/components/schemas/reitit.openapi-test~1Plus"}}}}}} "/get" {:get {:parameters [{:in "query" :name :x :required true :schema {:type "integer"}} {:in "query" :name :y :required true :schema {:$ref "#/components/schemas/reitit.openapi-test~1Y"}}]}}} :components {:schemas {"reitit.openapi-test/Plus" {:type "object" :properties {:x {:type "integer"} :y {:$ref "#/components/schemas/reitit.openapi-test~1Y"}} :required [:x :y]} "reitit.openapi-test/Y" {:type "integer"}}}} spec)) ;; TODO: the OAS 3.1 json schema disallows "/" in :components :schemas keys, ;; even though the text of the spec allows it. See: ;; https://github.com/seriousme/openapi-schema-validator/blob/772375bf4895f0e641d103c27140cdd1d2afc34e/schemas/v3.1/schema.json#L282 #_ (is (nil? (validate spec))))))