Repository: ring-clojure/ring-mock Branch: master Commit: 143f5bc3beb3 Files: 9 Total size: 22.8 KB Directory structure: gitextract_rzk_m87a/ ├── .github/ │ └── workflows/ │ └── test.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── project.clj ├── src/ │ └── ring/ │ └── mock/ │ └── request.clj └── test/ └── ring/ └── mock/ ├── request_test.clj └── test.txt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/test.yml ================================================ name: Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Prepare java uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: '11' - name: Install clojure tools uses: DeLaGuardo/setup-clojure@13.4 with: lein: 2.11.2 - name: Cache clojure dependencies uses: actions/cache@v4 with: path: ~/.m2/repository key: cljdeps-${{ hashFiles('project.clj') }} restore-keys: cljdeps- - name: Run tests run: lein test-all ================================================ FILE: .gitignore ================================================ /target /lib /classes /checkouts /codox pom.xml pom.xml.asc *.jar *.class .lein-deps-sum .lein-failures .lein-plugins .lein-env .lein-repl-history /.cpcache /.clj-kondo ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Guidelines **Do** follow [the seven rules of a great Git commit message][1]. **Do** follow [the Clojure Style Guide][2]. **Do** include tests for your change when appropriate. **Do** ensure that the CI checks pass. **Do** squash the commits in your PR to remove corrections irrelevant to the code history, once the PR has been reviewed. **Do** feel free to pester the project maintainers about the PR if it hasn't been responded to. Sometimes notifications can be missed. **Don't** include more than one feature or fix in a single PR. **Don't** include changes unrelated to the purpose of the PR. This includes changing the project version number, adding lines to the `.gitignore` file, or changing the indentation or formatting. **Don't** open a new PR if changes are requested. Just push to the same branch and the PR will be updated. **Don't** overuse vertical whitespace; avoid multiple sequential blank lines. **Don't** docstring private vars or functions. [1]: https://chris.beams.io/posts/git-commit/#seven-rules [2]: https://github.com/bbatsov/clojure-style-guide ================================================ FILE: LICENSE ================================================ Copyright (c) 2009-2010 Mark McGranaghan Copyright (c) 2009-2018 James Reeves Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Ring-Mock [![Build Status](https://github.com/ring-clojure/ring-mock/actions/workflows/test.yml/badge.svg)](https://github.com/ring-clojure/ring-mock/actions/workflows/test.yml) Ring-Mock is a library for creating [Ring][] request maps for testing purposes. [ring]: https://github.com/ring-clojure/ring ## Installation Add the following dependency to your deps.edn file: ring/ring-mock {:mvn/version "0.6.2"} Or to your Leiningen project file: [ring/ring-mock "0.6.2"] ## Documentation * [API Documentation](https://ring-clojure.github.io/ring-mock/ring.mock.request.html) ## Example ```clojure (ns your-app.core-test (:require [clojure.test :refer :all] [your-app.core :refer :all] [ring.mock.request :as mock])) (deftest your-handler-test (is (= (your-handler (mock/request :get "/doc/10")) {:status 200 :headers {"content-type" "text/plain"} :body "Your expected result"}))) (deftest your-json-handler-test (is (= (your-handler (-> (mock/request :post "/api/endpoint") (mock/json-body {:foo "bar"}))) {:status 201 :headers {"content-type" "application/json"} :body {:key "your expected result"}}))) ``` ## License Copyright © 2025 James Reeves Distributed under the MIT License. ================================================ FILE: project.clj ================================================ (defproject ring/ring-mock "0.6.2" :description "A library for creating mock Ring request maps" :url "https://github.com/ring-clojure/ring-mock" :license {:name "The MIT License" :url "http://opensource.org/licenses/MIT"} :dependencies [[org.clojure/clojure "1.9.0"] [cheshire "6.0.0"] [org.apache.httpcomponents.client5/httpclient5 "5.5"] [ring/ring-codec "1.3.0"] [ring/ring-core "1.14.2"]] :plugins [[lein-codox "0.10.8"]] :codox {:project {:name "Ring-Mock"} :output-path "codox" :source-uri "https://github.com/ring-clojure/ring-mock/blob/{version}/{filepath}#L{line}"} :aliases {"test-all" ["with-profile" "default:+1.10:+1.11:+1.12" "test"]} :profiles {:1.10 {:dependencies [[org.clojure/clojure "1.10.0"] [ring/ring-spec "0.0.4"]]} :1.11 {:dependencies [[org.clojure/clojure "1.11.0"] [ring/ring-spec "0.0.4"]]} :1.12 {:dependencies [[org.clojure/clojure "1.12.0"] [ring/ring-spec "0.0.4"]]}}) ================================================ FILE: src/ring/mock/request.clj ================================================ (ns ring.mock.request "Functions to create mock request maps." (:require [cheshire.core :as json] [clojure.string :as string] [ring.util.codec :as codec] [ring.util.mime-type :as mime]) (:import [java.io ByteArrayInputStream ByteArrayOutputStream File InputStream] [java.net URI] [java.nio.charset Charset] [java.util Map] [org.apache.hc.core5.http ContentType HttpEntity] [org.apache.hc.client5.http.entity.mime MultipartEntityBuilder])) (defn- encode-params [params] (when params (codec/form-encode params))) (defn header "Add a HTTP header to the request map." [request header value] (let [header (string/lower-case (name header))] (assoc-in request [:headers header] (str value)))) (defn cookie "Add a cookie to the request headers map" [request cookie-name value] (let [cookie-name (name cookie-name) cookie-string (str cookie-name "=" value)] (update-in request [:headers "cookie"] (fn [cookie-header] (if cookie-header (str cookie-header "; " cookie-string) cookie-string))))) (defn content-type "Set the content type of the request map." [request mime-type] (-> request (assoc :content-type mime-type) (header :content-type mime-type))) (defn content-length "Set the content length of the request map." [request length] (-> request (assoc :content-length length) (header :content-length length))) (defn- combined-query [request params] (let [query (:query-string request)] (when (or query params) (string/join "&" (remove string/blank? [query (encode-params params)]))))) (defn- merge-query [request params] (if-let [qs (combined-query request params)] (assoc request :query-string qs) request)) (defn query-string "Set the query string of the request to a string or a map of parameters." [request params] (cond (nil? params) request (map? params) (assoc request :query-string (encode-params params)) :else (assoc request :query-string params))) (defmulti body "Set the body of the request. The supplied body value can be a string or a map of parameters to be url-encoded." {:arglists '([request body-value])} (fn [_request x] (type x))) (defmethod body String [request ^String string] (body request (.getBytes string))) (defmethod body (class (byte-array 0)) [request bytes] (-> request (content-length (count bytes)) (assoc :body (ByteArrayInputStream. bytes)))) (defmethod body Map [request params] (-> request (content-type "application/x-www-form-urlencoded") (body (encode-params params)))) (defmethod body nil [request _params] request) (defn json-body "Set the body of the request to a JSON structure. The supplied body value should be a map of parameters to be converted to JSON." [request body-value] (-> request (content-type "application/json") (body (json/generate-string body-value)))) (def ^:private default-charset (Charset/forName "UTF-8")) (defn- file? [f] (instance? File f)) (defn- str->bytes ^bytes [^String s] (.getBytes s ^Charset default-charset)) (defn- add-binary-body [^MultipartEntityBuilder builder key value ^ContentType mimetype ^String filename] (let [k (name key)] (cond (string? value) (.addBinaryBody builder k (str->bytes value) mimetype filename) (bytes? value) (.addBinaryBody builder k ^bytes value mimetype filename) (file? value) (.addBinaryBody builder k ^File value mimetype filename) (instance? InputStream value) (.addBinaryBody builder k ^InputStream value mimetype filename) :else (throw (IllegalArgumentException. (str "Cannot encode a value of type " (type value) " as a multipart body.")))))) (defn- add-multipart-part [^MultipartEntityBuilder builder k v] (let [param (if (map? v) v {:value v}) value (if (map? v) (:value v) v) mimetype (ContentType/parse (or (:content-type param) (when (file? value) (mime/ext-mime-type (.getName ^File value))) (if (string? (:value param)) "text/plain; charset=UTF-8" "application/octet-stream"))) filename (or (:filename param) (when (file? value) (.getName ^File value)))] (add-binary-body builder (name k) value mimetype filename))) (defn- multipart-entity ^HttpEntity [params] (let [builder (MultipartEntityBuilder/create)] (.setCharset builder ^Charset default-charset) (doseq [[k v] params] (add-multipart-part builder k v)) (.build builder))) (defn multipart-body "Set the body of the request to a map of parameters encoded as a multipart form. The parameters are supplied as a map. The keys should be keywords or strings. The values should be maps that contain the following keys: :value - a string, byte array, File or InputStream :filename - the name of the file the value came from (optional) :content-type - the content type of the value (optional) The value may also be a string, byte array, File or InputStream instead of a map. In that case, it will be treated as if it were a map with a single :value key." [request params] (let [entity (multipart-entity params) out (ByteArrayOutputStream.)] (.writeTo entity out) (.close out) (-> request (content-length (.getContentLength entity)) (content-type (.getContentType entity)) (assoc :body (ByteArrayInputStream. (.toByteArray out)))))) (def default-port "A map of the default ports for a scheme." {:http 80 :https 443}) (defn request "Create a minimal valid request map from a HTTP method keyword, a string containing a URI, and an optional map of parameters that will be added to the query string of the URI. The URI can be relative or absolute. Relative URIs are assumed to go to http://localhost." ([method uri] (request method uri nil)) ([method uri params] (let [uri (URI. uri) scheme (keyword (or (.getScheme uri) "http")) host (or (.getHost uri) "localhost") port (when (not= (.getPort uri) -1) (.getPort uri)) path (.getRawPath uri) query (.getRawQuery uri) request {:protocol "HTTP/1.1" :server-port (or port (default-port scheme)) :server-name host :remote-addr "127.0.0.1" :uri (if (string/blank? path) "/" path) :scheme scheme :request-method method :headers {"host" (if port (str host ":" port) host)}} request (if query (assoc request :query-string query) request)] (if (#{:get :head :delete} method) (merge-query request params) (body request params))))) ================================================ FILE: test/ring/mock/request_test.clj ================================================ (ns ring.mock.request-test (:require [clojure.java.io :as io] [clojure.spec.alpha :as s] [clojure.test :refer [deftest is testing]] [ring.mock.request :refer [body content-length content-type cookie header json-body multipart-body query-string request]]) (:import [java.io InputStream])) (deftest test-request (testing "relative uri" (is (= (request :get "/foo") {:protocol "HTTP/1.1" :server-port 80 :server-name "localhost" :remote-addr "127.0.0.1" :uri "/foo" :scheme :http :request-method :get :headers {"host" "localhost"}}))) (testing "absolute uri" (let [request (request :post "https://example.com:8443/foo?bar=baz" {"quux" "zot"}) literal-request (dissoc request :body) body (:body request)] (is (= literal-request {:protocol "HTTP/1.1" :server-port 8443 :server-name "example.com" :remote-addr "127.0.0.1" :uri "/foo" :query-string "bar=baz" :scheme :https :request-method :post :content-type "application/x-www-form-urlencoded" :content-length 8 :headers {"host" "example.com:8443" "content-type" "application/x-www-form-urlencoded" "content-length" "8"}})) (is (= (slurp body) "quux=zot")))) (testing "absolute http uri with implicit port" (let [{:keys [headers server-port]} (request :get "http://example.test")] (is (= server-port 80)) (is (= (get headers "host") "example.test")))) (testing "absolute https uri with implicit port" (let [{:keys [headers server-port]} (request :get "https://example.test")] (is (= server-port 443)) (is (= (get headers "host") "example.test")))) (testing "nil path" (is (= (:uri (request :get "http://example.com")) "/"))) (testing "only params in :get" (is (= (:query-string (request :get "/?a=b")) "a=b"))) (testing "added params in :get" (is (= (:query-string (request :get "/" (array-map :x "y" :z "n"))) "x=y&z=n")) (is (= (:query-string (request :get "/?a=b" {:x "y"})) "a=b&x=y")) (is (= (:query-string (request :get "/?" {:x "y"})) "x=y")) (is (= (:query-string (request :get "/" {:x "a b"})) "x=a+b"))) (testing "added params in :delete" (is (= (:query-string (request :delete "/" (array-map :x "y" :z "n"))) "x=y&z=n"))) (testing "added params in :post" (let [req (request :post "/" (array-map :x "y" :z "n"))] (is (= (slurp (:body req)) "x=y&z=n")) (is (not (contains? req :query-string)))) (let [req (request :post "/?a=b" {:x "y"})] (is (= (slurp (:body req)) "x=y")) (is (= (:query-string req) "a=b"))) (let [req (request :post "/?" {:x "y"})] (is (= (slurp (:body req)) "x=y")) (is (= (:query-string req) ""))) (let [req (request :post "/" {:x "a b"})] (is (= (slurp (:body req)) "x=a+b")) (is (nil? (:query-string req)))) (let [req (request :post "/?a=b")] (is (nil? (:body req))) (is (= (:query-string req) "a=b")))) (testing "added params in :put" (let [req (request :put "/" (array-map :x "y" :z "n"))] (is (= (slurp (:body req)) "x=y&z=n"))))) (deftest test-header (is (= (header {} "X-Foo" "Bar") {:headers {"x-foo" "Bar"}})) (is (= (header {} :x-foo "Bar") {:headers {"x-foo" "Bar"}}))) (deftest test-cookie (is (= (cookie {} "Foo" "Bar") {:headers {"cookie" "Foo=Bar"}})) (is (= (cookie {:headers {"cookie" "a=b"}} "c" "d") {:headers {"cookie" "a=b; c=d"}})) (is (= (cookie {} :foo "bar") {:headers {"cookie" "foo=bar"}}))) (deftest test-content-type (is (= (content-type {} "text/html") {:content-type "text/html" :headers {"content-type" "text/html"}}))) (deftest test-content-length (is (= (content-length {} 10) {:content-length 10 :headers {"content-length" "10"}}))) (deftest test-query-string (testing "nil" (is (= (query-string {} nil) {}))) (testing "string" (is (= (query-string {} "a=b") {:query-string "a=b"}))) (testing "map of params" (is (= (query-string {} {:a "b"}) {:query-string "a=b"}))) (testing "overwriting" (is (= (-> {} (query-string {:a "b"}) (query-string {:c "d"})) {:query-string "c=d"})))) (deftest test-body (testing "string body" (let [resp (body {} "Hello World")] (is (instance? InputStream (:body resp))) (is (= (slurp (:body resp)) "Hello World")) (is (= (:content-length resp) 11)))) (testing "map body" (let [resp (body {} (array-map :foo "bar" :fi ["fi" "fo" "fum"]))] (is (instance? InputStream (:body resp))) (is (= (slurp (:body resp)) "foo=bar&fi=fi&fi=fo&fi=fum")) (is (= (:content-length resp) 26)) (is (= (:content-type resp) "application/x-www-form-urlencoded")))) (testing "bytes body" (let [resp (body {} (.getBytes "foo"))] (is (instance? InputStream (:body resp))) (is (= (slurp (:body resp)) "foo")) (is (= (:content-length resp) 3))))) (deftest test-json-body (testing "json body" (let [resp (json-body {} {:baz ["qu" "qi" "qo"]})] (is (= (:content-type resp) "application/json")) (is (instance? InputStream (:body resp))) (is (= (slurp (:body resp)) "{\"baz\":[\"qu\",\"qi\",\"qo\"]}")) (is (= (:content-length resp) 24))))) (defn- get-boundary [{:keys [content-type]}] (re-find #"(?<=boundary=)[^;]*" content-type)) (deftest test-multipart-body (testing "string values" (let [response (multipart-body {} {:foo "a" :bar {:value "b"} :baz {:value "" :content-type "text/html"}}) boundary (get-boundary response)] (is (= (:content-length response) (+ 284 (* 4 (count boundary))))) (is (= (:content-type response) (str "multipart/form-data; charset=ISO-8859-1; " "boundary=" boundary))) (is (= (slurp (:body response)) (str "--" boundary "\r\n" "Content-Disposition: form-data; name=\"foo\"\r\n" "Content-Type: text/plain; charset=UTF-8\r\n\r\n" "a\r\n" "--" boundary "\r\n" "Content-Disposition: form-data; name=\"bar\"\r\n" "Content-Type: text/plain; charset=UTF-8\r\n\r\n" "b\r\n" "--" boundary "\r\n" "Content-Disposition: form-data; name=\"baz\"\r\n" "Content-Type: text/html\r\n\r\n" "\r\n" "--" boundary "--\r\n"))))) (testing "byte array values" (let [response (multipart-body {} {:foo (.getBytes "a" "UTF-8") :bar {:value (.getBytes "b" "UTF-8") :content-type "image/png" :filename "bee.png"}}) boundary (get-boundary response)] (is (= (:content-length response) (+ 197 (* 3 (count boundary))))) (is (= (:content-type response) (str "multipart/form-data; charset=ISO-8859-1; " "boundary=" boundary))) (is (= (slurp (:body response)) (str "--" boundary "\r\n" "Content-Disposition: form-data; name=\"foo\"\r\n" "Content-Type: application/octet-stream\r\n\r\n" "a\r\n" "--" boundary "\r\n" "Content-Disposition: form-data; name=\"bar\"; " "filename=\"bee.png\"\r\n" "Content-Type: image/png\r\n\r\n" "b\r\n" "--" boundary "--\r\n"))))) (testing "file values" (let [test-file (io/file (io/resource "ring/mock/test.txt")) response (multipart-body {} {:foo test-file :bar {:value test-file :content-type "text/html" :filename "test.html"}}) boundary (get-boundary response)] (is (= (:content-length response) (+ 208 (* 3 (count boundary))))) (is (= (:content-type response) (str "multipart/form-data; charset=ISO-8859-1; " "boundary=" boundary))) (is (= (slurp (:body response)) (str "--" boundary "\r\n" "Content-Disposition: form-data; name=\"foo\"; " "filename=\"test.txt\"\r\n" "Content-Type: text/plain\r\n\r\n" "a\n\r\n" "--" boundary "\r\n" "Content-Disposition: form-data; name=\"bar\"; " "filename=\"test.html\"\r\n" "Content-Type: text/html\r\n\r\n" "a\n\r\n" "--" boundary "--\r\n"))))) (testing "not supported type" (try (multipart-body {} {:foo :keyword :bar {:value :keyword :content-type "text/html" :filename "test.html"}}) (catch Exception e (let [expected-message "Cannot encode a value of type class clojure.lang.Keyword as a multipart body."] (is (= expected-message (.getMessage ^Exception e)))))))) (defmacro when-clojure-spec [& body] (when (try (require '[clojure.spec :as s]) true (catch Exception _ (binding [*out* *err*] (println "ring-mock: Skipping ring-spec tests.")) false)) `(do (require 'ring.core.spec) ~@body))) (deftest test-specs (when-clojure-spec (doseq [req [(-> (request :get "/foo/bar") (query-string nil)) (request :put "/foo/bar") (request :something "/foo" {:params true})]] (is (s/valid? :ring/request req) (s/explain-str :ring/request req))))) ================================================ FILE: test/ring/mock/test.txt ================================================ a