From 26d9b47865c21ddd26e2f72eb8d1681f4e41ac11 Mon Sep 17 00:00:00 2001 From: Kirill Artamonov Date: Thu, 7 Mar 2019 11:06:47 +0100 Subject: [PATCH] feat: file upload --- README.md | 20 ++- pom.xml | 21 ++-- .../com/tomtom/http/HttpClientIT.groovy | 119 +++++++++--------- .../com/tomtom/http/RequestBuilder.groovy | 51 ++++---- .../com/tomtom/http/RequestBuilderSpec.groovy | 42 +++++-- 5 files changed, 145 insertions(+), 108 deletions(-) diff --git a/README.md b/README.md index f90cdf6..caca7c0 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ limitations under the License. * [Base url](#base-url) * [Request headers](#request-headers) * [Request body](#request-body) + * [File upload](#file-upload) * [Handling responses](#responses) * [Response status code](#status) * [Response headers](#response-headers) @@ -69,8 +70,10 @@ The library is initially intended for writing easily readable unit-tests but can * (doc) maven usage and javadocs * [1.2.0](http://mvnrepository.com/artifact/com.tomtom.http/goji-http-client/1.2.0) * (feature) support for TRACE, OPTIONS and PATCH methods - * [1.2.3](https://search.maven.org/artifact/com.tomtom.http/goji-http-client/1.2.3/jar) - * (chore) updated dependencies, including jackson-databind version with vulnerabilities +* [1.2.3](https://search.maven.org/artifact/com.tomtom.http/goji-http-client/1.2.3/jar) + * (chore) updated dependencies, including jackson-databind version with vulnerabilities +* [1.3.0](https://search.maven.org/artifact/com.tomtom.http/goji-http-client/1.3.0/jar) + * (feat) file upload ## Usage @@ -109,8 +112,7 @@ http.options() ### A request to an arbitrary url: ```groovy -http.get( - url: 'http://pizza-delivery.org/margheritas') +http.get(url: 'http://pizza-delivery.org/margheritas') ``` @@ -152,6 +154,16 @@ http.put( body: [key: 'value']) ``` + +#### Uploading a file + +If an instance of `java.io.File` is provided as `body` argument, it will be wrapped into a `MultipartFile`: +```groovy +http.put( + path: '/post', + body: '/tmp/input.json' as File) +``` + ## Handling responses diff --git a/pom.xml b/pom.xml index 78e2644..08b8418 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ 4.0.0 com.tomtom.http goji-http-client - 1.2.4 + 1.3.0 GOJI HTTP Client A wrapper around Apache HttpClient and Jackson Databind libraries with lean Groovy syntax. @@ -49,25 +49,25 @@ - 2.5.5 - 4.5.6 + 2.5.6 + 4.5.7 2.9.8 1.6.2 - 3.0.0-M1 + 3.0.0-M3 1.6 2.1 3.0.0 3.0.1 3.7.1 - 3.0.0-M1 + 3.0.0-M3 1.6.8 3.2.10 - 2.11.1 - 1.7.25 + 2.11.2 + 1.7.26 1.2-groovy-2.5 - 2.20.0 + 2.21.0 @@ -112,6 +112,11 @@ httpclient ${httpclient.version} + + org.apache.httpcomponents + httpmime + ${httpclient.version} + org.codehaus.groovy diff --git a/src/integration-test/groovy/com/tomtom/http/HttpClientIT.groovy b/src/integration-test/groovy/com/tomtom/http/HttpClientIT.groovy index 5a71232..9daf796 100644 --- a/src/integration-test/groovy/com/tomtom/http/HttpClientIT.groovy +++ b/src/integration-test/groovy/com/tomtom/http/HttpClientIT.groovy @@ -17,34 +17,32 @@ package com.tomtom.http import com.github.tomakehurst.wiremock.WireMockServer +import org.junit.Rule +import org.junit.rules.TemporaryFolder import spock.lang.Specification import static com.github.tomakehurst.wiremock.client.WireMock.* +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig import static com.tomtom.http.response.ResponseCode.OK class HttpClientIT extends Specification { - static service = new WireMockServer() - + def service = new WireMockServer(wireMockConfig().dynamicPort()) def http = new HttpClient() - - def setupSpec() { - service.start() - } + @Rule + TemporaryFolder tmp def setup() { - service.resetAll() + service.start() } - def 'Executes a get'() { + def 'executes a get'() { given: service.givenThat( get(urlEqualTo('/path')) - .willReturn( - aResponse() - .withStatus(OK) - .withHeader('header', 'value') - .withBody('body'))) + .willReturn(ok() + .withHeader('header', 'value') + .withBody('body'))) when: def response = http.get( @@ -58,33 +56,27 @@ class HttpClientIT extends Specification { } } - def 'Executes a get with path and base url'() { + def 'executes a get with path and base url'() { given: service.givenThat( get(urlEqualTo('/path')) - .willReturn( - aResponse() - .withStatus(OK))) + .willReturn(ok())) and: - def http = new HttpClient( - baseUrl: "http://localhost:${service.port()}") + def http = new HttpClient(baseUrl: "http://localhost:${service.port()}") when: - def response = http.get( - path: '/path') + def response = http.get(path: '/path') then: response.statusCode == OK } - def 'Executes a post with a body'() { + def 'executes a post with a body'() { given: service.givenThat( post(urlEqualTo('/path')) .withRequestBody(equalTo('body')) - .willReturn( - aResponse() - .withStatus(OK))) + .willReturn(ok())) when: def response = http.post( @@ -95,14 +87,30 @@ class HttpClientIT extends Specification { response.statusCode == OK } - def 'Executes a put with a json body'() { + def 'executes a post with a file body'() { + given: + def file = tmp.newFile() << 'foo' + service.givenThat( + post(urlEqualTo('/path')) + .withMultipartRequestBody( + aMultipart().withBody(equalTo('foo'))) + .willReturn(ok())) + + when: + def response = http.post( + url: "http://localhost:${service.port()}/path", + body: file) + + then: + response.statusCode == OK + } + + def 'executes a put with a json body'() { given: service.givenThat( put(urlEqualTo('/path')) .withRequestBody(equalTo('{"a":"b"}')) - .willReturn( - aResponse() - .withStatus(OK))) + .willReturn(ok())) when: def response = http.put( @@ -113,14 +121,12 @@ class HttpClientIT extends Specification { response.statusCode == OK } - def 'Executes a delete with headers'() { + def 'executes a delete with headers'() { given: service.givenThat( delete(urlEqualTo('/path')) .withHeader('header', equalTo('value')) - .willReturn( - aResponse() - .withStatus(OK))) + .willReturn(ok())) when: def response = http.delete( @@ -131,58 +137,50 @@ class HttpClientIT extends Specification { response.statusCode == OK } - def 'Executes an options'() { + def 'executes options'() { given: service.givenThat( options(urlEqualTo('/path')) - .willReturn( - aResponse().withStatus(OK))) + .willReturn(ok())) when: - def response = http.options( - url: "http://localhost:${service.port()}/path") + def response = http.options(url: "http://localhost:${service.port()}/path") then: response.statusCode == OK } - def 'Executes a trace'() { + def 'executes a trace'() { given: service.givenThat( trace(urlEqualTo('/path')) - .willReturn( - aResponse().withStatus(OK))) + .willReturn(ok())) when: - def response = http.trace( - url: "http://localhost:${service.port()}/path") + def response = http.trace(url: "http://localhost:${service.port()}/path") then: response.statusCode == OK } - def 'Executes a patch'() { + def 'executes a patch'() { given: service.givenThat( patch(urlEqualTo('/path')) - .willReturn( - aResponse().withStatus(OK))) + .willReturn(ok())) when: - def response = http.patch( - url: "http://localhost:${service.port()}/path") + def response = http.patch(url: "http://localhost:${service.port()}/path") then: response.statusCode == OK } - def 'Parses response'() { + def 'parses responses'() { given: service.givenThat( get(urlEqualTo('/path')) - .willReturn( - aResponse() - .withStatus(OK) + .willReturn(ok() .withBody('{"a": "b"}'))) when: @@ -197,13 +195,11 @@ class HttpClientIT extends Specification { } } - def 'Parses complex response'() { + def 'parses generic responses'() { given: service.givenThat( get(urlEqualTo('/path')) - .willReturn( - aResponse() - .withStatus(OK) + .willReturn(ok() .withBody('[{"name": "John Doe"}]'))) when: @@ -214,21 +210,18 @@ class HttpClientIT extends Specification { then: with(response) { statusCode == OK - body == [ - new Person( - name: 'John Doe')] + body == [new Person(name: 'John Doe')] } } - def cleanupSpec() { + def cleanup() { service?.stop() } - def 'Executes an https get'() { + def 'executes an https get'() { when: - def response = http.get( - url: 'https://httpbin.org/html') + def response = http.get(url: 'https://httpbin.org/html') then: response.statusCode == OK diff --git a/src/main/groovy/com/tomtom/http/RequestBuilder.groovy b/src/main/groovy/com/tomtom/http/RequestBuilder.groovy index a6be4d4..9f8683c 100644 --- a/src/main/groovy/com/tomtom/http/RequestBuilder.groovy +++ b/src/main/groovy/com/tomtom/http/RequestBuilder.groovy @@ -18,8 +18,10 @@ package com.tomtom.http import com.fasterxml.jackson.databind.ObjectMapper import groovy.transform.PackageScope +import org.apache.http.HttpEntity import org.apache.http.client.methods.* import org.apache.http.entity.StringEntity +import org.apache.http.entity.mime.MultipartEntityBuilder import org.apache.http.message.BasicHeader @PackageScope @@ -28,8 +30,7 @@ class RequestBuilder { private ObjectMapper mapper = new ObjectMapper() private String baseUrl - HttpRequestBase request( - Map properties) { + HttpRequestBase request(Map properties) { def method = properties['method'] def url = urlFrom properties def request = requestFor method, url @@ -38,16 +39,18 @@ class RequestBuilder { if (headers) addHeaders request, headers def body = properties['body'] - if (body) { - def serialized = serialize body - addBody request, serialized - } + if (body) + if (body instanceof File) + addFile request, body + else { + def serialized = serialize body + addBody request, serialized + } request } - private urlFrom( - Map properties) { + private urlFrom(Map properties) { def url = properties['url'] as String if (url) return url def path = properties['path'] @@ -55,28 +58,32 @@ class RequestBuilder { throw new NoUrl() } - private static def addHeaders( - request, - Map headers) { - headers - .collect { new BasicHeader(it.key as String, it.value as String) } + private static addHeaders(request, Map headers) { + headers.collect { new BasicHeader(it.key as String, it.value as String) } .forEach { request.addHeader it } } private String serialize(body) { - (body instanceof String) ? - body : mapper.writeValueAsString(body) + (body instanceof String) ? body : mapper.writeValueAsString(body) + } + + private static addBody(request, String body) { + addBody request, new StringEntity(body) + } + + private static addFile(request, File file) { + def body = MultipartEntityBuilder + .create() + .addBinaryBody("file", file) + .build() + addBody request, body } - private static addBody( - request, - String body) { - (request as HttpEntityEnclosingRequestBase) - .setEntity new StringEntity(body) + private static addBody(request, HttpEntity body) { + (request as HttpEntityEnclosingRequestBase).setEntity body } - private static HttpRequestBase requestFor( - method, String url) { + private static HttpRequestBase requestFor(method, String url) { switch (method) { case 'head': return new HttpHead(url) diff --git a/src/test/groovy/com/tomtom/http/RequestBuilderSpec.groovy b/src/test/groovy/com/tomtom/http/RequestBuilderSpec.groovy index 573d210..11158ee 100644 --- a/src/test/groovy/com/tomtom/http/RequestBuilderSpec.groovy +++ b/src/test/groovy/com/tomtom/http/RequestBuilderSpec.groovy @@ -18,13 +18,17 @@ package com.tomtom.http import org.apache.http.client.methods.HttpPost import org.apache.http.client.methods.HttpPut +import org.junit.Rule +import org.junit.rules.TemporaryFolder import spock.lang.Specification class RequestBuilderSpec extends Specification { def builder = new RequestBuilder() + @Rule + TemporaryFolder tmp - def 'Builds get'() { + def 'builds get'() { given: def properties = [ method: 'get', @@ -40,7 +44,7 @@ class RequestBuilderSpec extends Specification { } } - def 'Builds post with a body'() { + def 'builds post with a body'() { given: def properties = [ method: 'post', @@ -55,7 +59,23 @@ class RequestBuilderSpec extends Specification { (request as HttpPost).entity.content.text == 'bar' } - def 'Builds head'() { + def 'builds post with a file'() { + given: + def file = tmp.newFile() << 'bar' + def properties = [ + method: 'post', + url : 'foo', + body : file] + + when: + def request = builder.request properties + + then: + request.method == 'POST' + (request as HttpPost).entity.multipart.bodyParts.body*.inputStream.text == ['bar'] + } + + def 'builds head'() { given: def properties = [ method: 'head', @@ -68,7 +88,7 @@ class RequestBuilderSpec extends Specification { request.method == 'HEAD' } - def 'Builds put with a json body'() { + def 'builds put with a json body'() { given: def properties = [ method: 'put', @@ -83,7 +103,7 @@ class RequestBuilderSpec extends Specification { (request as HttpPut).entity.content.text == '{"a":"b"}' } - def 'Builds delete with headers'() { + def 'builds delete with headers'() { given: def properties = [ method : 'delete', @@ -101,7 +121,7 @@ class RequestBuilderSpec extends Specification { } } - def 'Builds options'() { + def 'builds options'() { given: def properties = [ method : 'options', @@ -116,7 +136,7 @@ class RequestBuilderSpec extends Specification { } } - def 'Builds patch'() { + def 'builds patch'() { given: def properties = [ method : 'patch', @@ -131,7 +151,7 @@ class RequestBuilderSpec extends Specification { } } - def 'Builds trace'() { + def 'builds trace'() { given: def properties = [ method : 'trace', @@ -146,7 +166,7 @@ class RequestBuilderSpec extends Specification { } } - def 'Specifies url by base url and path'() { + def 'specifies url by base url and path'() { given: def builder = new RequestBuilder(baseUrl: 'foo') @@ -159,7 +179,7 @@ class RequestBuilderSpec extends Specification { request.getURI() == 'foo/bar'.toURI() } - def 'Url property is preferred over path'() { + def 'URL property is preferred over path'() { given: def builder = new RequestBuilder(baseUrl: 'foo') @@ -173,7 +193,7 @@ class RequestBuilderSpec extends Specification { request.getURI() == 'bar'.toURI() } - def 'Either url or base url and path is required'() { + def 'either url or base url and path is required'() { when: builder.request([:])