Browse Source

Donate 'feign-mock' to 'feign' (#692)

* Copy of feign-mock from git@github.com:velo/feign-mock.git

* Fix license headers, remove .tool files and make code java 6 compatible

* Remove all badges

* Update feign GitHub link

* Remove link to orignal project
pull/698/head
Marvin Froeder 6 years ago committed by GitHub
parent
commit
ecd928a2e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 47
      mock/README.asciidoc
  2. 61
      mock/pom.xml
  3. 20
      mock/src/main/java/feign/mock/HttpMethod.java
  4. 276
      mock/src/main/java/feign/mock/MockClient.java
  5. 49
      mock/src/main/java/feign/mock/MockTarget.java
  6. 173
      mock/src/main/java/feign/mock/RequestKey.java
  7. 24
      mock/src/main/java/feign/mock/VerificationAssertionError.java
  8. 168
      mock/src/test/java/feign/mock/MockClientSequentialTest.java
  9. 261
      mock/src/test/java/feign/mock/MockClientTest.java
  10. 36
      mock/src/test/java/feign/mock/MockTargetTest.java
  11. 159
      mock/src/test/java/feign/mock/RequestKeyTest.java
  12. 602
      mock/src/test/resources/fixtures/contributors.json
  13. 7
      pom.xml

47
mock/README.asciidoc

@ -0,0 +1,47 @@ @@ -0,0 +1,47 @@
# feign-mock
An easy way to test https://github.com/OpenFeign/feign. Since feign stores most of the logic in annotations, this helps to check if the annotations are correct.
The original article is available https://velo.github.io/2016/06/05/Testing-feign-clients.html[here]
If mocking feign clients is easy, testing the logic written in annotations is not!
To check if you are parsing the request/response properly, the only way is firing a real request. Well, that doesn't seem to be a good path to unit (or even integration) test remote services. Any IO change will affect test stability.
With feign-mock you can use pre-loaded JSON strings or streams as content for your responses. It also allows you to verify mocked invocations and feign-mock will hit your annotations to make sure everything works.
##### Example
```
private GitHub github;
private MockClient mockClient;
@Before
public void setup() throws IOException {
mockClient = new MockClient()
.noContent(HttpMethod.PATCH, "/repos/velo/feign-mock/contributors");
github = Feign.builder()
.decoder(new GsonDecoder())
.client(mockClient)
.target(new MockTarget<>(GitHub.class));
}
@After
public void tearDown() {
mockClient.verifyStatus();
}
@Test
public void missHttpMethod() {
List<Contributor> result = github.patchContributors("velo", "feign-mock");
assertThat(result, nullValue());
mockClient.verifyOne(HttpMethod.PATCH, "/repos/velo/feign-mock/contributors");
}
```
This simple test returns no content and verifies that the URL was truly invoked.
On the mocked client, you can include all URLs and methods you want to mock.
For more comprehensive examples take a look at https://github.com/OpenFeign/feign/blob/master/mock/src/test/java/feign/mock/MockClientTest.java[MockClientTest].

61
mock/pom.xml

@ -0,0 +1,61 @@ @@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2012-2018 The Feign Authors
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied. See the License for the specific language governing permissions and limitations under
the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.github.openfeign</groupId>
<artifactId>parent</artifactId>
<version>9.7.0-SNAPSHOT</version>
</parent>
<artifactId>feign-mock</artifactId>
<name>Feign Mock</name>
<description>Feign Mock</description>
<properties>
<main.basedir>${project.basedir}/..</main.basedir>
<hamcrest.version>1.3</hamcrest.version>
</properties>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>feign-core</artifactId>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>feign-gson</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<version>${hamcrest.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<version>${hamcrest.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

20
mock/src/main/java/feign/mock/HttpMethod.java

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
/**
* Copyright 2012-2018 The Feign Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package feign.mock;
public enum HttpMethod {
GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, CONNECT, PATCH
}

276
mock/src/main/java/feign/mock/MockClient.java

@ -0,0 +1,276 @@ @@ -0,0 +1,276 @@
/**
* Copyright 2012-2018 The Feign Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package feign.mock;
import static feign.Util.UTF_8;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import feign.Client;
import feign.Request;
import feign.Response;
import feign.Util;
public class MockClient implements Client {
class RequestResponse {
private final RequestKey requestKey;
private final Response.Builder responseBuilder;
public RequestResponse(RequestKey requestKey, Response.Builder responseBuilder) {
this.requestKey = requestKey;
this.responseBuilder = responseBuilder;
}
}
public static final Map<String, Collection<String>> EMPTY_HEADERS = Collections.emptyMap();
private final List<RequestResponse> responses = new ArrayList<RequestResponse>();
private final Map<RequestKey, List<Request>> requests = new HashMap<RequestKey, List<Request>>();
private boolean sequential;
private Iterator<RequestResponse> responseIterator;
public MockClient() {
}
public MockClient(boolean sequential) {
this.sequential = sequential;
}
@Override
public synchronized Response execute(Request request, Request.Options options) throws IOException {
RequestKey requestKey = RequestKey.create(request);
Response.Builder responseBuilder;
if (sequential) {
responseBuilder = executeSequential(requestKey);
} else {
responseBuilder = executeAny(request, requestKey);
}
return responseBuilder.request(request).build();
}
private Response.Builder executeSequential(RequestKey requestKey) {
Response.Builder responseBuilder;
if (responseIterator == null) {
responseIterator = responses.iterator();
}
if (!responseIterator.hasNext()) {
throw new VerificationAssertionError("Received excessive request %s", requestKey);
}
RequestResponse expectedRequestResponse = responseIterator.next();
if (!expectedRequestResponse.requestKey.equalsExtended(requestKey)) {
throw new VerificationAssertionError("Expected %s, but was %s", expectedRequestResponse.requestKey,
requestKey);
}
responseBuilder = expectedRequestResponse.responseBuilder;
return responseBuilder;
}
private Response.Builder executeAny(Request request, RequestKey requestKey) {
Response.Builder responseBuilder;
if (requests.containsKey(requestKey)) {
requests.get(requestKey).add(request);
} else {
requests.put(requestKey, new ArrayList<Request>(Arrays.asList(request)));
}
responseBuilder = getResponseBuilder(request, requestKey);
return responseBuilder;
}
private Response.Builder getResponseBuilder(Request request, RequestKey requestKey) {
Response.Builder responseBuilder = null;
for (RequestResponse requestResponse : responses) {
if (requestResponse.requestKey.equalsExtended(requestKey)) {
responseBuilder = requestResponse.responseBuilder;
// Don't break here, last one should win to be compatible with
// previous
// releases of this library!
}
}
if (responseBuilder == null) {
responseBuilder = Response.builder().status(HttpURLConnection.HTTP_NOT_FOUND).reason("Not mocker")
.headers(request.headers());
}
return responseBuilder;
}
public MockClient ok(HttpMethod method, String url, InputStream responseBody) throws IOException {
return ok(RequestKey.builder(method, url).build(), responseBody);
}
public MockClient ok(HttpMethod method, String url, String responseBody) {
return ok(RequestKey.builder(method, url).build(), responseBody);
}
public MockClient ok(HttpMethod method, String url, byte[] responseBody) {
return ok(RequestKey.builder(method, url).build(), responseBody);
}
public MockClient ok(HttpMethod method, String url) {
return ok(RequestKey.builder(method, url).build());
}
public MockClient ok(RequestKey requestKey, InputStream responseBody) throws IOException {
return ok(requestKey, Util.toByteArray(responseBody));
}
public MockClient ok(RequestKey requestKey, String responseBody) {
return ok(requestKey, responseBody.getBytes(UTF_8));
}
public MockClient ok(RequestKey requestKey, byte[] responseBody) {
return add(requestKey, HttpURLConnection.HTTP_OK, responseBody);
}
public MockClient ok(RequestKey requestKey) {
return ok(requestKey, (byte[]) null);
}
public MockClient add(HttpMethod method, String url, int status, InputStream responseBody) throws IOException {
return add(RequestKey.builder(method, url).build(), status, responseBody);
}
public MockClient add(HttpMethod method, String url, int status, String responseBody) {
return add(RequestKey.builder(method, url).build(), status, responseBody);
}
public MockClient add(HttpMethod method, String url, int status, byte[] responseBody) {
return add(RequestKey.builder(method, url).build(), status, responseBody);
}
public MockClient add(HttpMethod method, String url, int status) {
return add(RequestKey.builder(method, url).build(), status);
}
/**
* @param response
* <ul>
* <li>the status defaults to 0, not 200!</li>
* <li>the internal feign-code requires the headers to be
* set</li>
* </ul>
*/
public MockClient add(HttpMethod method, String url, Response.Builder response) {
return add(RequestKey.builder(method, url).build(), response);
}
public MockClient add(RequestKey requestKey, int status, InputStream responseBody) throws IOException {
return add(requestKey, status, Util.toByteArray(responseBody));
}
public MockClient add(RequestKey requestKey, int status, String responseBody) {
return add(requestKey, status, responseBody.getBytes(UTF_8));
}
public MockClient add(RequestKey requestKey, int status, byte[] responseBody) {
return add(requestKey,
Response.builder().status(status).reason("Mocked").headers(EMPTY_HEADERS).body(responseBody));
}
public MockClient add(RequestKey requestKey, int status) {
return add(requestKey, status, (byte[]) null);
}
public MockClient add(RequestKey requestKey, Response.Builder response) {
responses.add(new RequestResponse(requestKey, response));
return this;
}
public MockClient add(HttpMethod method, String url, Response response) {
return this.add(method, url, response.toBuilder());
}
public MockClient noContent(HttpMethod method, String url) {
return add(method, url, HttpURLConnection.HTTP_NO_CONTENT);
}
public Request verifyOne(HttpMethod method, String url) {
return verifyTimes(method, url, 1).get(0);
}
public List<Request> verifyTimes(final HttpMethod method, final String url, final int times) {
if (times < 0) {
throw new IllegalArgumentException("times must be a non negative number");
}
if (times == 0) {
verifyNever(method, url);
return Collections.emptyList();
}
RequestKey requestKey = RequestKey.builder(method, url).build();
if (!requests.containsKey(requestKey)) {
throw new VerificationAssertionError("Wanted: '%s' but never invoked!", requestKey);
}
List<Request> result = requests.get(requestKey);
if (result.size() != times) {
throw new VerificationAssertionError("Wanted: '%s' to be invoked: '%s' times but got: '%s'!", requestKey,
times, result.size());
}
return result;
}
public void verifyNever(HttpMethod method, String url) {
RequestKey requestKey = RequestKey.builder(method, url).build();
if (requests.containsKey(requestKey)) {
throw new VerificationAssertionError("Do not wanted: '%s' but was invoked!", requestKey);
}
}
/**
* To be called in an &#64;After method:
*
* <pre>
* &#64;After
* public void tearDown() {
* mockClient.verifyStatus();
* }
* </pre>
*/
public void verifyStatus() {
if (sequential) {
boolean unopenedIterator = responseIterator == null && !responses.isEmpty();
if (unopenedIterator || responseIterator.hasNext()) {
throw new VerificationAssertionError("More executions were expected");
}
}
}
public void resetRequests() {
requests.clear();
}
}

49
mock/src/main/java/feign/mock/MockTarget.java

@ -0,0 +1,49 @@ @@ -0,0 +1,49 @@
/**
* Copyright 2012-2018 The Feign Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package feign.mock;
import feign.Request;
import feign.RequestTemplate;
import feign.Target;
public class MockTarget<E> implements Target<E> {
private final Class<E> type;
public MockTarget(Class<E> type) {
this.type = type;
}
@Override
public Class<E> type() {
return type;
}
@Override
public String name() {
return type.getSimpleName();
}
@Override
public String url() {
return "";
}
@Override
public Request apply(RequestTemplate input) {
input.insert(0, url());
return input.request();
}
}

173
mock/src/main/java/feign/mock/RequestKey.java

@ -0,0 +1,173 @@ @@ -0,0 +1,173 @@
/**
* Copyright 2012-2018 The Feign Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package feign.mock;
import static feign.Util.UTF_8;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import feign.Request;
import feign.Util;
public class RequestKey {
public static class Builder {
private final HttpMethod method;
private final String url;
private Map<String, Collection<String>> headers;
private Charset charset;
private byte[] body;
private Builder(HttpMethod method, String url) {
this.method = method;
this.url = url;
}
public Builder headers(Map<String, Collection<String>> headers) {
this.headers = headers;
return this;
}
public Builder charset(Charset charset) {
this.charset = charset;
return this;
}
public Builder body(String body) {
return body(body.getBytes(UTF_8));
}
public Builder body(byte[] body) {
this.body = body;
return this;
}
public RequestKey build() {
return new RequestKey(this);
}
}
public static Builder builder(HttpMethod method, String url) {
return new Builder(method, url);
}
public static RequestKey create(Request request) {
return new RequestKey(request);
}
private static String buildUrl(Request request) {
try {
return URLDecoder.decode(request.url(), Util.UTF_8.name());
} catch (final UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
private final HttpMethod method;
private final String url;
private final Map<String, Collection<String>> headers;
private final Charset charset;
private final byte[] body;
private RequestKey(Builder builder) {
this.method = builder.method;
this.url = builder.url;
this.headers = builder.headers;
this.charset = builder.charset;
this.body = builder.body;
}
private RequestKey(Request request) {
this.method = HttpMethod.valueOf(request.method());
this.url = buildUrl(request);
this.headers = request.headers();
this.charset = request.charset();
this.body = request.body();
}
public HttpMethod getMethod() {
return method;
}
public String getUrl() {
return url;
}
public Map<String, Collection<String>> getHeaders() {
return headers;
}
public Charset getCharset() {
return charset;
}
public byte[] getBody() {
return body;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((method == null) ? 0 : method.hashCode());
result = prime * result + ((url == null) ? 0 : url.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
final RequestKey other = (RequestKey) obj;
if (method != other.method) return false;
if (url == null) {
if (other.url != null) return false;
} else if (!url.equals(other.url)) return false;
return true;
}
public boolean equalsExtended(Object obj) {
if (equals(obj)) {
RequestKey other = (RequestKey) obj;
boolean headersEqual = other.headers == null || headers == null || headers.equals(other.headers);
boolean charsetEqual = other.charset == null || charset == null || charset.equals(other.charset);
boolean bodyEqual = other.body == null || body == null || Arrays.equals(other.body, body);
return headersEqual && charsetEqual && bodyEqual;
}
return false;
}
@Override
public String toString() {
return String.format("Request [%s %s: %s headers and %s]", method, url,
headers == null ? "without" : "with " + headers.size(),
charset == null ? "no charset" : "charset " + charset);
}
}

24
mock/src/main/java/feign/mock/VerificationAssertionError.java

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
/**
* Copyright 2012-2018 The Feign Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package feign.mock;
public class VerificationAssertionError extends AssertionError {
private static final long serialVersionUID = -3302777023656958993L;
public VerificationAssertionError(String message, Object... arguments) {
super(String.format(message, arguments));
}
}

168
mock/src/test/java/feign/mock/MockClientSequentialTest.java

@ -0,0 +1,168 @@ @@ -0,0 +1,168 @@
/**
* Copyright 2012-2018 The Feign Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package feign.mock;
import static feign.Util.toByteArray;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.fail;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.List;
import javax.net.ssl.HttpsURLConnection;
import org.junit.Before;
import org.junit.Test;
import feign.Body;
import feign.Feign;
import feign.FeignException;
import feign.Param;
import feign.RequestLine;
import feign.Response;
import feign.codec.DecodeException;
import feign.codec.Decoder;
import feign.gson.GsonDecoder;
public class MockClientSequentialTest {
interface GitHub {
@RequestLine("GET /repos/{owner}/{repo}/contributors")
List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
@RequestLine("GET /repos/{owner}/{repo}/contributors?client_id={client_id}")
List<Contributor> contributors(@Param("client_id") String clientId, @Param("owner") String owner,
@Param("repo") String repo);
@RequestLine("PATCH /repos/{owner}/{repo}/contributors")
List<Contributor> patchContributors(@Param("owner") String owner, @Param("repo") String repo);
@RequestLine("POST /repos/{owner}/{repo}/contributors")
@Body("%7B\"login\":\"{login}\",\"type\":\"{type}\"%7D")
Contributor create(@Param("owner") String owner, @Param("repo") String repo, @Param("login") String login,
@Param("type") String type);
}
static class Contributor {
String login;
int contributions;
}
class AssertionDecoder implements Decoder {
private final Decoder delegate;
public AssertionDecoder(Decoder delegate) {
this.delegate = delegate;
}
@Override
public Object decode(Response response, Type type) throws IOException, DecodeException, FeignException {
assertThat(response.request(), notNullValue());
return delegate.decode(response, type);
}
}
private GitHub githubSequential;
private MockClient mockClientSequential;
@Before
public void setup() throws IOException {
try (InputStream input = getClass().getResourceAsStream("/fixtures/contributors.json")) {
byte[] data = toByteArray(input);
mockClientSequential = new MockClient(true);
githubSequential = Feign.builder().decoder(new AssertionDecoder(new GsonDecoder()))
.client(mockClientSequential
.add(HttpMethod.GET, "/repos/netflix/feign/contributors", HttpsURLConnection.HTTP_OK, data)
.add(HttpMethod.GET, "/repos/netflix/feign/contributors?client_id=55",
HttpsURLConnection.HTTP_NOT_FOUND)
.add(HttpMethod.GET, "/repos/netflix/feign/contributors?client_id=7 7",
HttpsURLConnection.HTTP_INTERNAL_ERROR, new ByteArrayInputStream(data))
.add(HttpMethod.GET, "/repos/netflix/feign/contributors",
Response.builder().status(HttpsURLConnection.HTTP_OK)
.headers(MockClient.EMPTY_HEADERS).body(data)))
.target(new MockTarget<>(GitHub.class));
}
}
@Test
public void sequentialRequests() throws Exception {
githubSequential.contributors("netflix", "feign");
try {
githubSequential.contributors("55", "netflix", "feign");
fail();
} catch (FeignException e) {
assertThat(e.status(), equalTo(HttpsURLConnection.HTTP_NOT_FOUND));
}
try {
githubSequential.contributors("7 7", "netflix", "feign");
fail();
} catch (FeignException e) {
assertThat(e.status(), equalTo(HttpsURLConnection.HTTP_INTERNAL_ERROR));
}
githubSequential.contributors("netflix", "feign");
mockClientSequential.verifyStatus();
}
@Test
public void sequentialRequestsCalledTooLess() throws Exception {
githubSequential.contributors("netflix", "feign");
try {
mockClientSequential.verifyStatus();
fail();
} catch (VerificationAssertionError e) {
assertThat(e.getMessage(), startsWith("More executions"));
}
}
@Test
public void sequentialRequestsCalledTooMany() throws Exception {
sequentialRequests();
try {
githubSequential.contributors("netflix", "feign");
fail();
} catch (VerificationAssertionError e) {
assertThat(e.getMessage(), containsString("excessive"));
}
}
@Test
public void sequentialRequestsInWrongOrder() throws Exception {
try {
githubSequential.contributors("7 7", "netflix", "feign");
fail();
} catch (VerificationAssertionError e) {
assertThat(e.getMessage(), startsWith("Expected Request ["));
}
}
}

261
mock/src/test/java/feign/mock/MockClientTest.java

@ -0,0 +1,261 @@ @@ -0,0 +1,261 @@
/**
* Copyright 2012-2018 The Feign Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package feign.mock;
import static feign.Util.toByteArray;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.fail;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.List;
import javax.net.ssl.HttpsURLConnection;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Test;
import feign.Body;
import feign.Feign;
import feign.FeignException;
import feign.Param;
import feign.Request;
import feign.RequestLine;
import feign.Response;
import feign.codec.DecodeException;
import feign.codec.Decoder;
import feign.gson.GsonDecoder;
public class MockClientTest {
interface GitHub {
@RequestLine("GET /repos/{owner}/{repo}/contributors")
List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
@RequestLine("GET /repos/{owner}/{repo}/contributors?client_id={client_id}")
List<Contributor> contributors(@Param("client_id") String clientId, @Param("owner") String owner,
@Param("repo") String repo);
@RequestLine("PATCH /repos/{owner}/{repo}/contributors")
List<Contributor> patchContributors(@Param("owner") String owner, @Param("repo") String repo);
@RequestLine("POST /repos/{owner}/{repo}/contributors")
@Body("%7B\"login\":\"{login}\",\"type\":\"{type}\"%7D")
Contributor create(@Param("owner") String owner, @Param("repo") String repo, @Param("login") String login,
@Param("type") String type);
}
static class Contributor {
String login;
int contributions;
}
class AssertionDecoder implements Decoder {
private final Decoder delegate;
public AssertionDecoder(Decoder delegate) {
this.delegate = delegate;
}
@Override
public Object decode(Response response, Type type) throws IOException, DecodeException, FeignException {
assertThat(response.request(), notNullValue());
return delegate.decode(response, type);
}
}
private GitHub github;
private MockClient mockClient;
@Before
public void setup() throws IOException {
try (InputStream input = getClass().getResourceAsStream("/fixtures/contributors.json")) {
byte[] data = toByteArray(input);
mockClient = new MockClient();
github = Feign.builder().decoder(new AssertionDecoder(new GsonDecoder()))
.client(mockClient.ok(HttpMethod.GET, "/repos/netflix/feign/contributors", data)
.ok(HttpMethod.GET, "/repos/netflix/feign/contributors?client_id=55")
.ok(HttpMethod.GET, "/repos/netflix/feign/contributors?client_id=7 7",
new ByteArrayInputStream(data))
.ok(HttpMethod.POST, "/repos/netflix/feign/contributors",
"{\"login\":\"velo\",\"contributions\":0}")
.noContent(HttpMethod.PATCH, "/repos/velo/feign-mock/contributors")
.add(HttpMethod.GET, "/repos/netflix/feign/contributors?client_id=1234567890",
HttpsURLConnection.HTTP_NOT_FOUND)
.add(HttpMethod.GET, "/repos/netflix/feign/contributors?client_id=123456789",
HttpsURLConnection.HTTP_INTERNAL_ERROR, new ByteArrayInputStream(data))
.add(HttpMethod.GET, "/repos/netflix/feign/contributors?client_id=123456789",
HttpsURLConnection.HTTP_INTERNAL_ERROR, "")
.add(HttpMethod.GET, "/repos/netflix/feign/contributors?client_id=123456789",
HttpsURLConnection.HTTP_INTERNAL_ERROR, data))
.target(new MockTarget<>(GitHub.class));
}
}
@Test
public void hitMock() {
List<Contributor> contributors = github.contributors("netflix", "feign");
assertThat(contributors, hasSize(30));
mockClient.verifyStatus();
}
@Test
public void missMock() {
try {
github.contributors("velo", "feign-mock");
fail();
} catch (FeignException e) {
assertThat(e.getMessage(), Matchers.containsString("404"));
}
}
@Test
public void missHttpMethod() {
try {
github.patchContributors("netflix", "feign");
fail();
} catch (FeignException e) {
assertThat(e.getMessage(), Matchers.containsString("404"));
}
}
@Test
public void paramsEncoding() {
List<Contributor> contributors = github.contributors("7 7", "netflix", "feign");
assertThat(contributors, hasSize(30));
mockClient.verifyStatus();
}
@Test
public void verifyInvocation() {
Contributor contribution = github.create("netflix", "feign", "velo_at_github", "preposterous hacker");
// making sure it received a proper response
assertThat(contribution, notNullValue());
assertThat(contribution.login, equalTo("velo"));
assertThat(contribution.contributions, equalTo(0));
List<Request> results = mockClient.verifyTimes(HttpMethod.POST, "/repos/netflix/feign/contributors", 1);
assertThat(results, hasSize(1));
byte[] body = mockClient.verifyOne(HttpMethod.POST, "/repos/netflix/feign/contributors").body();
assertThat(body, notNullValue());
String message = new String(body);
assertThat(message, containsString("velo_at_github"));
assertThat(message, containsString("preposterous hacker"));
mockClient.verifyStatus();
}
@Test
public void verifyNone() {
github.create("netflix", "feign", "velo_at_github", "preposterous hacker");
mockClient.verifyTimes(HttpMethod.POST, "/repos/netflix/feign/contributors", 1);
try {
mockClient.verifyTimes(HttpMethod.POST, "/repos/netflix/feign/contributors", 0);
fail();
} catch (VerificationAssertionError e) {
assertThat(e.getMessage(), containsString("Do not wanted"));
assertThat(e.getMessage(), containsString("POST"));
assertThat(e.getMessage(), containsString("/repos/netflix/feign/contributors"));
}
try {
mockClient.verifyTimes(HttpMethod.POST, "/repos/netflix/feign/contributors", 3);
fail();
} catch (VerificationAssertionError e) {
assertThat(e.getMessage(), containsString("Wanted"));
assertThat(e.getMessage(), containsString("POST"));
assertThat(e.getMessage(), containsString("/repos/netflix/feign/contributors"));
assertThat(e.getMessage(), containsString("'3'"));
assertThat(e.getMessage(), containsString("'1'"));
}
}
@Test
public void verifyNotInvoked() {
mockClient.verifyNever(HttpMethod.POST, "/repos/netflix/feign/contributors");
List<Request> results = mockClient.verifyTimes(HttpMethod.POST, "/repos/netflix/feign/contributors", 0);
assertThat(results, hasSize(0));
try {
mockClient.verifyOne(HttpMethod.POST, "/repos/netflix/feign/contributors");
fail();
} catch (VerificationAssertionError e) {
assertThat(e.getMessage(), containsString("Wanted"));
assertThat(e.getMessage(), containsString("POST"));
assertThat(e.getMessage(), containsString("/repos/netflix/feign/contributors"));
assertThat(e.getMessage(), containsString("never invoked"));
}
}
@Test
public void verifyNegative() {
try {
mockClient.verifyTimes(HttpMethod.POST, "/repos/netflix/feign/contributors", -1);
fail();
} catch (IllegalArgumentException e) {
assertThat(e.getMessage(), containsString("non negative"));
}
}
@Test
public void verifyMultipleRequests() {
mockClient.verifyNever(HttpMethod.POST, "/repos/netflix/feign/contributors");
github.create("netflix", "feign", "velo_at_github", "preposterous hacker");
Request result = mockClient.verifyOne(HttpMethod.POST, "/repos/netflix/feign/contributors");
assertThat(result, notNullValue());
github.create("netflix", "feign", "velo_at_github", "preposterous hacker");
List<Request> results = mockClient.verifyTimes(HttpMethod.POST, "/repos/netflix/feign/contributors", 2);
assertThat(results, hasSize(2));
github.create("netflix", "feign", "velo_at_github", "preposterous hacker");
results = mockClient.verifyTimes(HttpMethod.POST, "/repos/netflix/feign/contributors", 3);
assertThat(results, hasSize(3));
mockClient.verifyStatus();
}
@Test
public void resetRequests() {
mockClient.verifyNever(HttpMethod.POST, "/repos/netflix/feign/contributors");
github.create("netflix", "feign", "velo_at_github", "preposterous hacker");
Request result = mockClient.verifyOne(HttpMethod.POST, "/repos/netflix/feign/contributors");
assertThat(result, notNullValue());
mockClient.resetRequests();
mockClient.verifyNever(HttpMethod.POST, "/repos/netflix/feign/contributors");
}
}

36
mock/src/test/java/feign/mock/MockTargetTest.java

@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
/**
* Copyright 2012-2018 The Feign Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package feign.mock;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;
public class MockTargetTest {
private MockTarget<MockTargetTest> target;
@Before
public void setup() {
target = new MockTarget<>(MockTargetTest.class);
}
@Test
public void test() {
assertThat(target.name(), equalTo("MockTargetTest"));
}
}

159
mock/src/test/java/feign/mock/RequestKeyTest.java

@ -0,0 +1,159 @@ @@ -0,0 +1,159 @@
/**
* Copyright 2012-2018 The Feign Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package feign.mock;
import static org.hamcrest.Matchers.both;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertThat;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import feign.Request;
public class RequestKeyTest {
private RequestKey requestKey;
@Before
public void setUp() {
Map<String, Collection<String>> map = new HashMap<>();
map.put("my-header", Arrays.asList("val"));
requestKey = RequestKey.builder(HttpMethod.GET, "a").headers(map).charset(StandardCharsets.UTF_16)
.body("content").build();
}
@Test
public void builder() throws Exception {
assertThat(requestKey.getMethod(), equalTo(HttpMethod.GET));
assertThat(requestKey.getUrl(), equalTo("a"));
assertThat(requestKey.getHeaders().entrySet(), hasSize(1));
assertThat(requestKey.getHeaders().get("my-header"), equalTo((Collection<String>) Arrays.asList("val")));
assertThat(requestKey.getCharset(), equalTo(StandardCharsets.UTF_16));
}
@Test
public void create() throws Exception {
Map<String, Collection<String>> map = new HashMap<>();
map.put("my-header", Arrays.asList("val"));
Request request = Request.create("GET", "a", map, "content".getBytes(StandardCharsets.UTF_8),
StandardCharsets.UTF_16);
requestKey = RequestKey.create(request);
assertThat(requestKey.getMethod(), equalTo(HttpMethod.GET));
assertThat(requestKey.getUrl(), equalTo("a"));
assertThat(requestKey.getHeaders().entrySet(), hasSize(1));
assertThat(requestKey.getHeaders().get("my-header"), equalTo((Collection<String>) Arrays.asList("val")));
assertThat(requestKey.getCharset(), equalTo(StandardCharsets.UTF_16));
assertThat(requestKey.getBody(), equalTo("content".getBytes(StandardCharsets.UTF_8)));
}
@Test
public void checkHashes() {
RequestKey requestKey1 = RequestKey.builder(HttpMethod.GET, "a").build();
RequestKey requestKey2 = RequestKey.builder(HttpMethod.GET, "b").build();
assertThat(requestKey1.hashCode(), not(equalTo(requestKey2.hashCode())));
assertThat(requestKey1, not(equalTo(requestKey2)));
}
@Test
public void equalObject() {
assertThat(requestKey, not(equalTo(new Object())));
}
@Test
public void equalNull() {
assertThat(requestKey, not(equalTo(null)));
}
@Test
public void equalPost() {
RequestKey requestKey1 = RequestKey.builder(HttpMethod.GET, "a").build();
RequestKey requestKey2 = RequestKey.builder(HttpMethod.POST, "a").build();
assertThat(requestKey1.hashCode(), not(equalTo(requestKey2.hashCode())));
assertThat(requestKey1, not(equalTo(requestKey2)));
}
@Test
public void equalSelf() {
assertThat(requestKey.hashCode(), equalTo(requestKey.hashCode()));
assertThat(requestKey, equalTo(requestKey));
}
@Test
public void equalMinimum() {
RequestKey requestKey2 = RequestKey.builder(HttpMethod.GET, "a").build();
assertThat(requestKey.hashCode(), equalTo(requestKey2.hashCode()));
assertThat(requestKey, equalTo(requestKey2));
}
@Test
public void equalExtra() {
Map<String, Collection<String>> map = new HashMap<>();
map.put("my-other-header", Arrays.asList("other value"));
RequestKey requestKey2 = RequestKey.builder(HttpMethod.GET, "a").headers(map)
.charset(StandardCharsets.ISO_8859_1).build();
assertThat(requestKey.hashCode(), equalTo(requestKey2.hashCode()));
assertThat(requestKey, equalTo(requestKey2));
}
@Test
public void equalsExtended() {
RequestKey requestKey2 = RequestKey.builder(HttpMethod.GET, "a").build();
assertThat(requestKey.hashCode(), equalTo(requestKey2.hashCode()));
assertThat(requestKey.equalsExtended(requestKey2), equalTo(true));
}
@Test
public void equalsExtendedExtra() {
Map<String, Collection<String>> map = new HashMap<>();
map.put("my-other-header", Arrays.asList("other value"));
RequestKey requestKey2 = RequestKey.builder(HttpMethod.GET, "a").headers(map)
.charset(StandardCharsets.ISO_8859_1).build();
assertThat(requestKey.hashCode(), equalTo(requestKey2.hashCode()));
assertThat(requestKey.equalsExtended(requestKey2), equalTo(false));
}
@Test
public void testToString() throws Exception {
assertThat(requestKey.toString(), startsWith("Request [GET a: "));
assertThat(requestKey.toString(), both(containsString(" with 1 ")).and(containsString(" UTF-16]")));
}
@Test
public void testToStringSimple() throws Exception {
requestKey = RequestKey.builder(HttpMethod.GET, "a").build();
assertThat(requestKey.toString(), startsWith("Request [GET a: "));
assertThat(requestKey.toString(), both(containsString(" without ")).and(containsString(" no charset")));
}
}
//

602
mock/src/test/resources/fixtures/contributors.json

@ -0,0 +1,602 @@ @@ -0,0 +1,602 @@
[
{
"login": "adriancole",
"id": 64215,
"avatar_url": "https://avatars.githubusercontent.com/u/64215?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/adriancole",
"html_url": "https://github.com/adriancole",
"followers_url": "https://api.github.com/users/adriancole/followers",
"following_url": "https://api.github.com/users/adriancole/following{/other_user}",
"gists_url": "https://api.github.com/users/adriancole/gists{/gist_id}",
"starred_url": "https://api.github.com/users/adriancole/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/adriancole/subscriptions",
"organizations_url": "https://api.github.com/users/adriancole/orgs",
"repos_url": "https://api.github.com/users/adriancole/repos",
"events_url": "https://api.github.com/users/adriancole/events{/privacy}",
"received_events_url": "https://api.github.com/users/adriancole/received_events",
"type": "User",
"site_admin": false,
"contributions": 297
},
{
"login": "quidryan",
"id": 360255,
"avatar_url": "https://avatars.githubusercontent.com/u/360255?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/quidryan",
"html_url": "https://github.com/quidryan",
"followers_url": "https://api.github.com/users/quidryan/followers",
"following_url": "https://api.github.com/users/quidryan/following{/other_user}",
"gists_url": "https://api.github.com/users/quidryan/gists{/gist_id}",
"starred_url": "https://api.github.com/users/quidryan/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/quidryan/subscriptions",
"organizations_url": "https://api.github.com/users/quidryan/orgs",
"repos_url": "https://api.github.com/users/quidryan/repos",
"events_url": "https://api.github.com/users/quidryan/events{/privacy}",
"received_events_url": "https://api.github.com/users/quidryan/received_events",
"type": "User",
"site_admin": false,
"contributions": 43
},
{
"login": "rspieldenner",
"id": 782102,
"avatar_url": "https://avatars.githubusercontent.com/u/782102?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/rspieldenner",
"html_url": "https://github.com/rspieldenner",
"followers_url": "https://api.github.com/users/rspieldenner/followers",
"following_url": "https://api.github.com/users/rspieldenner/following{/other_user}",
"gists_url": "https://api.github.com/users/rspieldenner/gists{/gist_id}",
"starred_url": "https://api.github.com/users/rspieldenner/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/rspieldenner/subscriptions",
"organizations_url": "https://api.github.com/users/rspieldenner/orgs",
"repos_url": "https://api.github.com/users/rspieldenner/repos",
"events_url": "https://api.github.com/users/rspieldenner/events{/privacy}",
"received_events_url": "https://api.github.com/users/rspieldenner/received_events",
"type": "User",
"site_admin": false,
"contributions": 14
},
{
"login": "davidmc24",
"id": 447825,
"avatar_url": "https://avatars.githubusercontent.com/u/447825?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/davidmc24",
"html_url": "https://github.com/davidmc24",
"followers_url": "https://api.github.com/users/davidmc24/followers",
"following_url": "https://api.github.com/users/davidmc24/following{/other_user}",
"gists_url": "https://api.github.com/users/davidmc24/gists{/gist_id}",
"starred_url": "https://api.github.com/users/davidmc24/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/davidmc24/subscriptions",
"organizations_url": "https://api.github.com/users/davidmc24/orgs",
"repos_url": "https://api.github.com/users/davidmc24/repos",
"events_url": "https://api.github.com/users/davidmc24/events{/privacy}",
"received_events_url": "https://api.github.com/users/davidmc24/received_events",
"type": "User",
"site_admin": false,
"contributions": 12
},
{
"login": "ahus1",
"id": 3957921,
"avatar_url": "https://avatars.githubusercontent.com/u/3957921?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/ahus1",
"html_url": "https://github.com/ahus1",
"followers_url": "https://api.github.com/users/ahus1/followers",
"following_url": "https://api.github.com/users/ahus1/following{/other_user}",
"gists_url": "https://api.github.com/users/ahus1/gists{/gist_id}",
"starred_url": "https://api.github.com/users/ahus1/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/ahus1/subscriptions",
"organizations_url": "https://api.github.com/users/ahus1/orgs",
"repos_url": "https://api.github.com/users/ahus1/repos",
"events_url": "https://api.github.com/users/ahus1/events{/privacy}",
"received_events_url": "https://api.github.com/users/ahus1/received_events",
"type": "User",
"site_admin": false,
"contributions": 6
},
{
"login": "allenxwang",
"id": 1728105,
"avatar_url": "https://avatars.githubusercontent.com/u/1728105?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/allenxwang",
"html_url": "https://github.com/allenxwang",
"followers_url": "https://api.github.com/users/allenxwang/followers",
"following_url": "https://api.github.com/users/allenxwang/following{/other_user}",
"gists_url": "https://api.github.com/users/allenxwang/gists{/gist_id}",
"starred_url": "https://api.github.com/users/allenxwang/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/allenxwang/subscriptions",
"organizations_url": "https://api.github.com/users/allenxwang/orgs",
"repos_url": "https://api.github.com/users/allenxwang/repos",
"events_url": "https://api.github.com/users/allenxwang/events{/privacy}",
"received_events_url": "https://api.github.com/users/allenxwang/received_events",
"type": "User",
"site_admin": false,
"contributions": 5
},
{
"login": "nmiyake",
"id": 4267425,
"avatar_url": "https://avatars.githubusercontent.com/u/4267425?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/nmiyake",
"html_url": "https://github.com/nmiyake",
"followers_url": "https://api.github.com/users/nmiyake/followers",
"following_url": "https://api.github.com/users/nmiyake/following{/other_user}",
"gists_url": "https://api.github.com/users/nmiyake/gists{/gist_id}",
"starred_url": "https://api.github.com/users/nmiyake/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/nmiyake/subscriptions",
"organizations_url": "https://api.github.com/users/nmiyake/orgs",
"repos_url": "https://api.github.com/users/nmiyake/repos",
"events_url": "https://api.github.com/users/nmiyake/events{/privacy}",
"received_events_url": "https://api.github.com/users/nmiyake/received_events",
"type": "User",
"site_admin": false,
"contributions": 4
},
{
"login": "Drdoteam",
"id": 4572139,
"avatar_url": "https://avatars.githubusercontent.com/u/4572139?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/Drdoteam",
"html_url": "https://github.com/Drdoteam",
"followers_url": "https://api.github.com/users/Drdoteam/followers",
"following_url": "https://api.github.com/users/Drdoteam/following{/other_user}",
"gists_url": "https://api.github.com/users/Drdoteam/gists{/gist_id}",
"starred_url": "https://api.github.com/users/Drdoteam/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/Drdoteam/subscriptions",
"organizations_url": "https://api.github.com/users/Drdoteam/orgs",
"repos_url": "https://api.github.com/users/Drdoteam/repos",
"events_url": "https://api.github.com/users/Drdoteam/events{/privacy}",
"received_events_url": "https://api.github.com/users/Drdoteam/received_events",
"type": "User",
"site_admin": false,
"contributions": 4
},
{
"login": "spencergibb",
"id": 594085,
"avatar_url": "https://avatars.githubusercontent.com/u/594085?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/spencergibb",
"html_url": "https://github.com/spencergibb",
"followers_url": "https://api.github.com/users/spencergibb/followers",
"following_url": "https://api.github.com/users/spencergibb/following{/other_user}",
"gists_url": "https://api.github.com/users/spencergibb/gists{/gist_id}",
"starred_url": "https://api.github.com/users/spencergibb/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/spencergibb/subscriptions",
"organizations_url": "https://api.github.com/users/spencergibb/orgs",
"repos_url": "https://api.github.com/users/spencergibb/repos",
"events_url": "https://api.github.com/users/spencergibb/events{/privacy}",
"received_events_url": "https://api.github.com/users/spencergibb/received_events",
"type": "User",
"site_admin": false,
"contributions": 3
},
{
"login": "jacob-meacham",
"id": 1624811,
"avatar_url": "https://avatars.githubusercontent.com/u/1624811?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/jacob-meacham",
"html_url": "https://github.com/jacob-meacham",
"followers_url": "https://api.github.com/users/jacob-meacham/followers",
"following_url": "https://api.github.com/users/jacob-meacham/following{/other_user}",
"gists_url": "https://api.github.com/users/jacob-meacham/gists{/gist_id}",
"starred_url": "https://api.github.com/users/jacob-meacham/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/jacob-meacham/subscriptions",
"organizations_url": "https://api.github.com/users/jacob-meacham/orgs",
"repos_url": "https://api.github.com/users/jacob-meacham/repos",
"events_url": "https://api.github.com/users/jacob-meacham/events{/privacy}",
"received_events_url": "https://api.github.com/users/jacob-meacham/received_events",
"type": "User",
"site_admin": false,
"contributions": 3
},
{
"login": "pnepywoda",
"id": 13909400,
"avatar_url": "https://avatars.githubusercontent.com/u/13909400?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/pnepywoda",
"html_url": "https://github.com/pnepywoda",
"followers_url": "https://api.github.com/users/pnepywoda/followers",
"following_url": "https://api.github.com/users/pnepywoda/following{/other_user}",
"gists_url": "https://api.github.com/users/pnepywoda/gists{/gist_id}",
"starred_url": "https://api.github.com/users/pnepywoda/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/pnepywoda/subscriptions",
"organizations_url": "https://api.github.com/users/pnepywoda/orgs",
"repos_url": "https://api.github.com/users/pnepywoda/repos",
"events_url": "https://api.github.com/users/pnepywoda/events{/privacy}",
"received_events_url": "https://api.github.com/users/pnepywoda/received_events",
"type": "User",
"site_admin": false,
"contributions": 3
},
{
"login": "santhosh-tekuri",
"id": 1112271,
"avatar_url": "https://avatars.githubusercontent.com/u/1112271?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/santhosh-tekuri",
"html_url": "https://github.com/santhosh-tekuri",
"followers_url": "https://api.github.com/users/santhosh-tekuri/followers",
"following_url": "https://api.github.com/users/santhosh-tekuri/following{/other_user}",
"gists_url": "https://api.github.com/users/santhosh-tekuri/gists{/gist_id}",
"starred_url": "https://api.github.com/users/santhosh-tekuri/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/santhosh-tekuri/subscriptions",
"organizations_url": "https://api.github.com/users/santhosh-tekuri/orgs",
"repos_url": "https://api.github.com/users/santhosh-tekuri/repos",
"events_url": "https://api.github.com/users/santhosh-tekuri/events{/privacy}",
"received_events_url": "https://api.github.com/users/santhosh-tekuri/received_events",
"type": "User",
"site_admin": false,
"contributions": 3
},
{
"login": "bstick12",
"id": 1146861,
"avatar_url": "https://avatars.githubusercontent.com/u/1146861?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/bstick12",
"html_url": "https://github.com/bstick12",
"followers_url": "https://api.github.com/users/bstick12/followers",
"following_url": "https://api.github.com/users/bstick12/following{/other_user}",
"gists_url": "https://api.github.com/users/bstick12/gists{/gist_id}",
"starred_url": "https://api.github.com/users/bstick12/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/bstick12/subscriptions",
"organizations_url": "https://api.github.com/users/bstick12/orgs",
"repos_url": "https://api.github.com/users/bstick12/repos",
"events_url": "https://api.github.com/users/bstick12/events{/privacy}",
"received_events_url": "https://api.github.com/users/bstick12/received_events",
"type": "User",
"site_admin": false,
"contributions": 3
},
{
"login": "oillio",
"id": 205051,
"avatar_url": "https://avatars.githubusercontent.com/u/205051?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/oillio",
"html_url": "https://github.com/oillio",
"followers_url": "https://api.github.com/users/oillio/followers",
"following_url": "https://api.github.com/users/oillio/following{/other_user}",
"gists_url": "https://api.github.com/users/oillio/gists{/gist_id}",
"starred_url": "https://api.github.com/users/oillio/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/oillio/subscriptions",
"organizations_url": "https://api.github.com/users/oillio/orgs",
"repos_url": "https://api.github.com/users/oillio/repos",
"events_url": "https://api.github.com/users/oillio/events{/privacy}",
"received_events_url": "https://api.github.com/users/oillio/received_events",
"type": "User",
"site_admin": false,
"contributions": 2
},
{
"login": "stromnet",
"id": 668449,
"avatar_url": "https://avatars.githubusercontent.com/u/668449?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/stromnet",
"html_url": "https://github.com/stromnet",
"followers_url": "https://api.github.com/users/stromnet/followers",
"following_url": "https://api.github.com/users/stromnet/following{/other_user}",
"gists_url": "https://api.github.com/users/stromnet/gists{/gist_id}",
"starred_url": "https://api.github.com/users/stromnet/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/stromnet/subscriptions",
"organizations_url": "https://api.github.com/users/stromnet/orgs",
"repos_url": "https://api.github.com/users/stromnet/repos",
"events_url": "https://api.github.com/users/stromnet/events{/privacy}",
"received_events_url": "https://api.github.com/users/stromnet/received_events",
"type": "User",
"site_admin": false,
"contributions": 2
},
{
"login": "qualidafial",
"id": 38629,
"avatar_url": "https://avatars.githubusercontent.com/u/38629?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/qualidafial",
"html_url": "https://github.com/qualidafial",
"followers_url": "https://api.github.com/users/qualidafial/followers",
"following_url": "https://api.github.com/users/qualidafial/following{/other_user}",
"gists_url": "https://api.github.com/users/qualidafial/gists{/gist_id}",
"starred_url": "https://api.github.com/users/qualidafial/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/qualidafial/subscriptions",
"organizations_url": "https://api.github.com/users/qualidafial/orgs",
"repos_url": "https://api.github.com/users/qualidafial/repos",
"events_url": "https://api.github.com/users/qualidafial/events{/privacy}",
"received_events_url": "https://api.github.com/users/qualidafial/received_events",
"type": "User",
"site_admin": false,
"contributions": 2
},
{
"login": "amit-git",
"id": 2767034,
"avatar_url": "https://avatars.githubusercontent.com/u/2767034?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/amit-git",
"html_url": "https://github.com/amit-git",
"followers_url": "https://api.github.com/users/amit-git/followers",
"following_url": "https://api.github.com/users/amit-git/following{/other_user}",
"gists_url": "https://api.github.com/users/amit-git/gists{/gist_id}",
"starred_url": "https://api.github.com/users/amit-git/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/amit-git/subscriptions",
"organizations_url": "https://api.github.com/users/amit-git/orgs",
"repos_url": "https://api.github.com/users/amit-git/repos",
"events_url": "https://api.github.com/users/amit-git/events{/privacy}",
"received_events_url": "https://api.github.com/users/amit-git/received_events",
"type": "User",
"site_admin": false,
"contributions": 2
},
{
"login": "dstepanov",
"id": 666879,
"avatar_url": "https://avatars.githubusercontent.com/u/666879?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/dstepanov",
"html_url": "https://github.com/dstepanov",
"followers_url": "https://api.github.com/users/dstepanov/followers",
"following_url": "https://api.github.com/users/dstepanov/following{/other_user}",
"gists_url": "https://api.github.com/users/dstepanov/gists{/gist_id}",
"starred_url": "https://api.github.com/users/dstepanov/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/dstepanov/subscriptions",
"organizations_url": "https://api.github.com/users/dstepanov/orgs",
"repos_url": "https://api.github.com/users/dstepanov/repos",
"events_url": "https://api.github.com/users/dstepanov/events{/privacy}",
"received_events_url": "https://api.github.com/users/dstepanov/received_events",
"type": "User",
"site_admin": false,
"contributions": 1
},
{
"login": "asukhyy",
"id": 891597,
"avatar_url": "https://avatars.githubusercontent.com/u/891597?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/asukhyy",
"html_url": "https://github.com/asukhyy",
"followers_url": "https://api.github.com/users/asukhyy/followers",
"following_url": "https://api.github.com/users/asukhyy/following{/other_user}",
"gists_url": "https://api.github.com/users/asukhyy/gists{/gist_id}",
"starred_url": "https://api.github.com/users/asukhyy/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/asukhyy/subscriptions",
"organizations_url": "https://api.github.com/users/asukhyy/orgs",
"repos_url": "https://api.github.com/users/asukhyy/repos",
"events_url": "https://api.github.com/users/asukhyy/events{/privacy}",
"received_events_url": "https://api.github.com/users/asukhyy/received_events",
"type": "User",
"site_admin": false,
"contributions": 1
},
{
"login": "carlossg",
"id": 23651,
"avatar_url": "https://avatars.githubusercontent.com/u/23651?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/carlossg",
"html_url": "https://github.com/carlossg",
"followers_url": "https://api.github.com/users/carlossg/followers",
"following_url": "https://api.github.com/users/carlossg/following{/other_user}",
"gists_url": "https://api.github.com/users/carlossg/gists{/gist_id}",
"starred_url": "https://api.github.com/users/carlossg/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/carlossg/subscriptions",
"organizations_url": "https://api.github.com/users/carlossg/orgs",
"repos_url": "https://api.github.com/users/carlossg/repos",
"events_url": "https://api.github.com/users/carlossg/events{/privacy}",
"received_events_url": "https://api.github.com/users/carlossg/received_events",
"type": "User",
"site_admin": false,
"contributions": 1
},
{
"login": "christopherlakey",
"id": 1859690,
"avatar_url": "https://avatars.githubusercontent.com/u/1859690?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/christopherlakey",
"html_url": "https://github.com/christopherlakey",
"followers_url": "https://api.github.com/users/christopherlakey/followers",
"following_url": "https://api.github.com/users/christopherlakey/following{/other_user}",
"gists_url": "https://api.github.com/users/christopherlakey/gists{/gist_id}",
"starred_url": "https://api.github.com/users/christopherlakey/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/christopherlakey/subscriptions",
"organizations_url": "https://api.github.com/users/christopherlakey/orgs",
"repos_url": "https://api.github.com/users/christopherlakey/repos",
"events_url": "https://api.github.com/users/christopherlakey/events{/privacy}",
"received_events_url": "https://api.github.com/users/christopherlakey/received_events",
"type": "User",
"site_admin": false,
"contributions": 1
},
{
"login": "dsyer",
"id": 124075,
"avatar_url": "https://avatars.githubusercontent.com/u/124075?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/dsyer",
"html_url": "https://github.com/dsyer",
"followers_url": "https://api.github.com/users/dsyer/followers",
"following_url": "https://api.github.com/users/dsyer/following{/other_user}",
"gists_url": "https://api.github.com/users/dsyer/gists{/gist_id}",
"starred_url": "https://api.github.com/users/dsyer/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/dsyer/subscriptions",
"organizations_url": "https://api.github.com/users/dsyer/orgs",
"repos_url": "https://api.github.com/users/dsyer/repos",
"events_url": "https://api.github.com/users/dsyer/events{/privacy}",
"received_events_url": "https://api.github.com/users/dsyer/received_events",
"type": "User",
"site_admin": false,
"contributions": 1
},
{
"login": "aspyker",
"id": 260750,
"avatar_url": "https://avatars.githubusercontent.com/u/260750?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/aspyker",
"html_url": "https://github.com/aspyker",
"followers_url": "https://api.github.com/users/aspyker/followers",
"following_url": "https://api.github.com/users/aspyker/following{/other_user}",
"gists_url": "https://api.github.com/users/aspyker/gists{/gist_id}",
"starred_url": "https://api.github.com/users/aspyker/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/aspyker/subscriptions",
"organizations_url": "https://api.github.com/users/aspyker/orgs",
"repos_url": "https://api.github.com/users/aspyker/repos",
"events_url": "https://api.github.com/users/aspyker/events{/privacy}",
"received_events_url": "https://api.github.com/users/aspyker/received_events",
"type": "User",
"site_admin": false,
"contributions": 1
},
{
"login": "FrEaKmAn",
"id": 232901,
"avatar_url": "https://avatars.githubusercontent.com/u/232901?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/FrEaKmAn",
"html_url": "https://github.com/FrEaKmAn",
"followers_url": "https://api.github.com/users/FrEaKmAn/followers",
"following_url": "https://api.github.com/users/FrEaKmAn/following{/other_user}",
"gists_url": "https://api.github.com/users/FrEaKmAn/gists{/gist_id}",
"starred_url": "https://api.github.com/users/FrEaKmAn/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/FrEaKmAn/subscriptions",
"organizations_url": "https://api.github.com/users/FrEaKmAn/orgs",
"repos_url": "https://api.github.com/users/FrEaKmAn/repos",
"events_url": "https://api.github.com/users/FrEaKmAn/events{/privacy}",
"received_events_url": "https://api.github.com/users/FrEaKmAn/received_events",
"type": "User",
"site_admin": false,
"contributions": 1
},
{
"login": "htynkn",
"id": 659135,
"avatar_url": "https://avatars.githubusercontent.com/u/659135?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/htynkn",
"html_url": "https://github.com/htynkn",
"followers_url": "https://api.github.com/users/htynkn/followers",
"following_url": "https://api.github.com/users/htynkn/following{/other_user}",
"gists_url": "https://api.github.com/users/htynkn/gists{/gist_id}",
"starred_url": "https://api.github.com/users/htynkn/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/htynkn/subscriptions",
"organizations_url": "https://api.github.com/users/htynkn/orgs",
"repos_url": "https://api.github.com/users/htynkn/repos",
"events_url": "https://api.github.com/users/htynkn/events{/privacy}",
"received_events_url": "https://api.github.com/users/htynkn/received_events",
"type": "User",
"site_admin": false,
"contributions": 1
},
{
"login": "jebeaudet",
"id": 3722096,
"avatar_url": "https://avatars.githubusercontent.com/u/3722096?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/jebeaudet",
"html_url": "https://github.com/jebeaudet",
"followers_url": "https://api.github.com/users/jebeaudet/followers",
"following_url": "https://api.github.com/users/jebeaudet/following{/other_user}",
"gists_url": "https://api.github.com/users/jebeaudet/gists{/gist_id}",
"starred_url": "https://api.github.com/users/jebeaudet/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/jebeaudet/subscriptions",
"organizations_url": "https://api.github.com/users/jebeaudet/orgs",
"repos_url": "https://api.github.com/users/jebeaudet/repos",
"events_url": "https://api.github.com/users/jebeaudet/events{/privacy}",
"received_events_url": "https://api.github.com/users/jebeaudet/received_events",
"type": "User",
"site_admin": false,
"contributions": 1
},
{
"login": "jmcampanini",
"id": 316848,
"avatar_url": "https://avatars.githubusercontent.com/u/316848?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/jmcampanini",
"html_url": "https://github.com/jmcampanini",
"followers_url": "https://api.github.com/users/jmcampanini/followers",
"following_url": "https://api.github.com/users/jmcampanini/following{/other_user}",
"gists_url": "https://api.github.com/users/jmcampanini/gists{/gist_id}",
"starred_url": "https://api.github.com/users/jmcampanini/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/jmcampanini/subscriptions",
"organizations_url": "https://api.github.com/users/jmcampanini/orgs",
"repos_url": "https://api.github.com/users/jmcampanini/repos",
"events_url": "https://api.github.com/users/jmcampanini/events{/privacy}",
"received_events_url": "https://api.github.com/users/jmcampanini/received_events",
"type": "User",
"site_admin": false,
"contributions": 1
},
{
"login": "Randgalt",
"id": 264818,
"avatar_url": "https://avatars.githubusercontent.com/u/264818?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/Randgalt",
"html_url": "https://github.com/Randgalt",
"followers_url": "https://api.github.com/users/Randgalt/followers",
"following_url": "https://api.github.com/users/Randgalt/following{/other_user}",
"gists_url": "https://api.github.com/users/Randgalt/gists{/gist_id}",
"starred_url": "https://api.github.com/users/Randgalt/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/Randgalt/subscriptions",
"organizations_url": "https://api.github.com/users/Randgalt/orgs",
"repos_url": "https://api.github.com/users/Randgalt/repos",
"events_url": "https://api.github.com/users/Randgalt/events{/privacy}",
"received_events_url": "https://api.github.com/users/Randgalt/received_events",
"type": "User",
"site_admin": false,
"contributions": 1
},
{
"login": "VanRoy",
"id": 1958756,
"avatar_url": "https://avatars.githubusercontent.com/u/1958756?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/VanRoy",
"html_url": "https://github.com/VanRoy",
"followers_url": "https://api.github.com/users/VanRoy/followers",
"following_url": "https://api.github.com/users/VanRoy/following{/other_user}",
"gists_url": "https://api.github.com/users/VanRoy/gists{/gist_id}",
"starred_url": "https://api.github.com/users/VanRoy/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/VanRoy/subscriptions",
"organizations_url": "https://api.github.com/users/VanRoy/orgs",
"repos_url": "https://api.github.com/users/VanRoy/repos",
"events_url": "https://api.github.com/users/VanRoy/events{/privacy}",
"received_events_url": "https://api.github.com/users/VanRoy/received_events",
"type": "User",
"site_admin": false,
"contributions": 1
},
{
"login": "mhurne",
"id": 677354,
"avatar_url": "https://avatars.githubusercontent.com/u/677354?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/mhurne",
"html_url": "https://github.com/mhurne",
"followers_url": "https://api.github.com/users/mhurne/followers",
"following_url": "https://api.github.com/users/mhurne/following{/other_user}",
"gists_url": "https://api.github.com/users/mhurne/gists{/gist_id}",
"starred_url": "https://api.github.com/users/mhurne/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/mhurne/subscriptions",
"organizations_url": "https://api.github.com/users/mhurne/orgs",
"repos_url": "https://api.github.com/users/mhurne/repos",
"events_url": "https://api.github.com/users/mhurne/events{/privacy}",
"received_events_url": "https://api.github.com/users/mhurne/received_events",
"type": "User",
"site_admin": false,
"contributions": 1
}
]

7
pom.xml

@ -37,6 +37,7 @@ @@ -37,6 +37,7 @@
<module>sax</module>
<module>slf4j</module>
<module>java8</module>
<module>mock</module>
<module>benchmark</module>
</modules>
@ -110,6 +111,12 @@ @@ -110,6 +111,12 @@
<name>Spencer Gibb</name>
<email>spencer@gibb.us</email>
</developer>
<developer>
<id>velo</id>
<name>Marvin Herman Froeder</name>
<email>velo br at gmail dot com</email>
<url>about.me/velo</url>
</developer>
</developers>
<distributionManagement>

Loading…
Cancel
Save