Skip to content

Commit 662e268

Browse files
committed
Add support for multipart encoded forms
Closes #26.
1 parent 2161dbe commit 662e268

File tree

4 files changed

+137
-2
lines changed

4 files changed

+137
-2
lines changed

project.clj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
:url "http://opensource.org/licenses/MIT"}
66
:dependencies [[org.clojure/clojure "1.9.0"]
77
[cheshire "6.0.0"]
8-
[ring/ring-codec "1.3.0"]]
8+
[org.apache.httpcomponents.client5/httpclient5 "5.4.4"]
9+
[ring/ring-codec "1.3.0"]
10+
[ring/ring-core "1.14.1"]]
911
:plugins [[lein-codox "0.10.8"]]
1012
:codox
1113
{:project {:name "Ring-Mock"}

src/ring/mock/request.clj

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
"Functions to create mock request maps."
33
(:require [cheshire.core :as json]
44
[clojure.string :as string]
5-
[ring.util.codec :as codec]))
5+
[ring.util.codec :as codec]
6+
[ring.util.mime-type :as mime])
7+
(:import [java.io ByteArrayInputStream ByteArrayOutputStream File]
8+
[java.nio.charset Charset]
9+
[org.apache.hc.core5.http ContentType HttpEntity]
10+
[org.apache.hc.client5.http.entity.mime MultipartEntityBuilder]))
611

712
(defn- encode-params
813
"Turn a map of parameters into a urlencoded string."
@@ -95,6 +100,57 @@
95100
(content-type "application/json")
96101
(body (json/generate-string body-value))))
97102

103+
(def ^:private default-charset
104+
(Charset/forName "UTF-8"))
105+
106+
(defn- file? [f]
107+
(instance? File f))
108+
109+
(defn add-multipart-part [builder k v]
110+
(let [param (if (map? v) v {:value v})
111+
value (if (string? (:value param))
112+
(.getBytes (:value param) default-charset)
113+
(:value param))
114+
mimetype (ContentType/parse
115+
(or (:content-type param)
116+
(when (file? value)
117+
(mime/ext-mime-type (.getName ^File value)))
118+
(if (string? (:value param))
119+
"text/plain; charset=UTF-8"
120+
"application/octet-stream")))
121+
filename (or (:filename param)
122+
(when (file? value) (.getName ^File value)))]
123+
(.addBinaryBody builder (name k) value mimetype filename)))
124+
125+
(defn multipart-entity ^HttpEntity [params]
126+
(let [builder (MultipartEntityBuilder/create)]
127+
(.setCharset builder default-charset)
128+
(doseq [[k v] params]
129+
(add-multipart-part builder k v))
130+
(.build builder)))
131+
132+
(defn multipart-body
133+
"Set the body of the request to a map of parameters encoded as a multipart
134+
form. The parameters are supplied as a map. The keys should be keywords or
135+
strings. The values should be maps that contain the following keys:
136+
137+
:value - a string, byte array, File or InputStream
138+
:filename - the name of the file the value came from (optional)
139+
:content-type - the content type of the value (optional)
140+
141+
The value may also be a string, byte array, File or InputStream instead of a
142+
map. In that case, it will be treated as if it were a map with a single :value
143+
key."
144+
[request params]
145+
(let [entity (multipart-entity params)
146+
out (ByteArrayOutputStream.)]
147+
(.writeTo entity out)
148+
(.close out)
149+
(-> request
150+
(content-length (.getContentLength entity))
151+
(content-type (.getContentType entity))
152+
(assoc :body (ByteArrayInputStream. (.toByteArray out))))))
153+
98154
(def default-port
99155
"A map of the default ports for a scheme."
100156
{:http 80

test/ring/mock/request_test.clj

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,82 @@
153153
"{\"baz\":[\"qu\",\"qi\",\"qo\"]}"))
154154
(is (= (:content-length resp) 24)))))
155155

156+
(defn- get-boundary [{:keys [content-type]}]
157+
(re-find #"(?<=boundary=)[^;]*" content-type))
158+
159+
(deftest test-multipart-body
160+
(testing "string values"
161+
(let [response (multipart-body {} {:foo "a"
162+
:bar {:value "b"}
163+
:baz {:value "<html></html>"
164+
:content-type "text/html"}})
165+
boundary (get-boundary response)]
166+
(is (= (:content-length response)
167+
(+ 284 (* 4 (count boundary)))))
168+
(is (= (:content-type response)
169+
(str "multipart/form-data; charset=ISO-8859-1; "
170+
"boundary=" boundary)))
171+
(is (= (slurp (:body response))
172+
(str "--" boundary "\r\n"
173+
"Content-Disposition: form-data; name=\"foo\"\r\n"
174+
"Content-Type: text/plain; charset=UTF-8\r\n\r\n"
175+
"a\r\n"
176+
"--" boundary "\r\n"
177+
"Content-Disposition: form-data; name=\"bar\"\r\n"
178+
"Content-Type: text/plain; charset=UTF-8\r\n\r\n"
179+
"b\r\n"
180+
"--" boundary "\r\n"
181+
"Content-Disposition: form-data; name=\"baz\"\r\n"
182+
"Content-Type: text/html\r\n\r\n"
183+
"<html></html>\r\n"
184+
"--" boundary "--\r\n")))))
185+
(testing "byte array values"
186+
(let [response (multipart-body {} {:foo (.getBytes "a" "UTF-8")
187+
:bar {:value (.getBytes "b" "UTF-8")
188+
:content-type "image/png"
189+
:filename "bee.png"}})
190+
boundary (get-boundary response)]
191+
(is (= (:content-length response)
192+
(+ 197 (* 3 (count boundary)))))
193+
(is (= (:content-type response)
194+
(str "multipart/form-data; charset=ISO-8859-1; "
195+
"boundary=" boundary)))
196+
(is (= (slurp (:body response))
197+
(str "--" boundary "\r\n"
198+
"Content-Disposition: form-data; name=\"foo\"\r\n"
199+
"Content-Type: application/octet-stream\r\n\r\n"
200+
"a\r\n"
201+
"--" boundary "\r\n"
202+
"Content-Disposition: form-data; name=\"bar\"; "
203+
"filename=\"bee.png\"\r\n"
204+
"Content-Type: image/png\r\n\r\n"
205+
"b\r\n"
206+
"--" boundary "--\r\n")))))
207+
(testing "file values"
208+
(let [test-file (io/file (io/resource "ring/mock/test.txt"))
209+
response (multipart-body {} {:foo test-file
210+
:bar {:value test-file
211+
:content-type "text/html"
212+
:filename "test.html"}})
213+
boundary (get-boundary response)]
214+
(is (= (:content-length response)
215+
(+ 208 (* 3 (count boundary)))))
216+
(is (= (:content-type response)
217+
(str "multipart/form-data; charset=ISO-8859-1; "
218+
"boundary=" boundary)))
219+
(is (= (slurp (:body response))
220+
(str "--" boundary "\r\n"
221+
"Content-Disposition: form-data; name=\"foo\"; "
222+
"filename=\"test.txt\"\r\n"
223+
"Content-Type: text/plain\r\n\r\n"
224+
"a\n\r\n"
225+
"--" boundary "\r\n"
226+
"Content-Disposition: form-data; name=\"bar\"; "
227+
"filename=\"test.html\"\r\n"
228+
"Content-Type: text/html\r\n\r\n"
229+
"a\n\r\n"
230+
"--" boundary "--\r\n"))))))
231+
156232
(defmacro when-clojure-spec
157233
[& body]
158234
(when (try

test/ring/mock/test.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
a

0 commit comments

Comments
 (0)