Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for async requests #328

Merged
merged 3 commits into from
Feb 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ implementation 'com.auth0:auth0:1.26.0'

The Auth0 Authentication API and User's Management API are available for Android in the `auth0.android` library. Check https://github.com/auth0/auth0.android for more information.


## Auth API

The implementation is based on the [Authentication API Docs](https://auth0.com/docs/api/authentication).
Expand Down Expand Up @@ -570,6 +569,10 @@ try {
}
```

## Asynchronous requests

Requests can be executed asynchronously, using the `executeAsync()` method, which returns a `CompletableFuture<T>`.

## API Clients Recommendations
The SDK implements a custom networking stack on top of the **OkHttp** library. The [official recommendation](https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/#okhttpclients-should-be-shared) from Square is to re-use as much as possible these clients. However, it's not possible to pass an existing `OkHttpClient` instance to our `AuthAPI` and `ManagementAPI` clients.

Expand Down
36 changes: 36 additions & 0 deletions src/main/java/com/auth0/net/BaseRequest.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package com.auth0.net;

import com.auth0.exception.Auth0Exception;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Response;
import org.jetbrains.annotations.NotNull;

import java.io.IOException;
import java.util.concurrent.CompletableFuture;

public abstract class BaseRequest<T> implements Request<T> {

Expand Down Expand Up @@ -35,4 +39,36 @@ public T execute() throws Auth0Exception {
throw new Auth0Exception("Failed to execute request", e);
}
}

@Override
public CompletableFuture<T> executeAsync() {
final CompletableFuture<T> future = new CompletableFuture<T>();

okhttp3.Request request;
try {
request = createRequest();
} catch (Auth0Exception e) {
future.completeExceptionally(e);
return future;
}

client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
future.completeExceptionally(new Auth0Exception("Failed to execute request", e));
}

@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
try {
T parsedResponse = parseResponse(response);
future.complete(parsedResponse);
} catch (Auth0Exception e) {
future.completeExceptionally(e);
}
}
});

return future;
}
}
20 changes: 19 additions & 1 deletion src/main/java/com/auth0/net/Request.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.auth0.exception.APIException;
import com.auth0.exception.Auth0Exception;

import java.util.concurrent.CompletableFuture;

/**
* Class that represents an HTTP Request that can be executed.
*
Expand All @@ -11,11 +13,27 @@
public interface Request<T> {

/**
* Executes this request.
* Executes this request synchronously.
*
* @return the response body JSON decoded as T
* @throws APIException if the request was executed but the response wasn't successful.
* @throws Auth0Exception if the request couldn't be created or executed successfully.
*/
T execute() throws Auth0Exception;

/**
* Executes this request asynchronously.
*
* @apiNote This method was added after the interface was released in version 1.0.
* It is defined as a default method for compatibility reasons.
* From version 2.0 on, the method will be abstract and all implementations of this interface
* will have to provide their own implementation.
*
* @implSpec The default implementation throws an {@linkplain UnsupportedOperationException}.
*
* @return a {@linkplain CompletableFuture} representing the specified request.
*/
default CompletableFuture<T> executeAsync() {
throw new UnsupportedOperationException("executeAsync");
}
}
141 changes: 131 additions & 10 deletions src/test/java/com/auth0/net/BaseRequestTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,24 @@
import com.auth0.exception.APIException;
import com.auth0.exception.Auth0Exception;
import com.auth0.exception.RateLimitException;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.*;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.util.concurrent.CompletableFuture;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.*;

// FIXME: These test require mocking of the final class okhttp3.Response. To do so
// an opt-in incubating Mockito feature is used, for more information see:
// https://github.com/mockito/mockito/wiki/What%27s-new-in-Mockito-2#mock-the-unmockable-opt-in-mocking-of-final-classesmethods
// This issue can be tracked to see if/when this feature will become standard for Mockito (perhaps Mockito 4):
// https://github.com/mockito/mockito/issues/1728
public class BaseRequestTest {

private Response response;
Expand Down Expand Up @@ -117,13 +116,135 @@ protected String parseResponse(Response response) throws Auth0Exception {
verify(response, times(1)).close();
}

private abstract class MockBaseRequest<String> extends BaseRequest {
@Test
public void asyncCompletesWithExceptionWhenRequestCreationFails() throws Exception {
CompletableFuture<?> request = new MockBaseRequest<String>(client) {
@Override
protected Request parseResponse(Response response) throws Auth0Exception {
throw new Auth0Exception("Response Parsing Error");
}
@Override
protected Request createRequest() throws Auth0Exception {
throw new Auth0Exception("Create Request Error");
}
}.executeAsync();

Exception exception = null;
Object result = null;

try {
result = request.get();
} catch (Exception e) {
exception = e;
}

assertThat(result, is(nullValue()));
assertThat(exception, is(notNullValue()));
assertThat(exception.getCause(), is(instanceOf(Auth0Exception.class)));
assertThat(exception.getCause().getMessage(), is("Create Request Error"));
}

@Test
public void asyncCompletesWithExceptionWhenRequestFails() throws Exception {
doReturn(call).when(client).newCall(any());

doAnswer(invocation -> {
((Callback) invocation.getArgument(0)).onFailure(call, new IOException("Error!"));
return null;
}).when(call).enqueue(any());

CompletableFuture<?> request = new MockBaseRequest<String>(client) {
@Override
protected String parseResponse(Response response) throws Auth0Exception {
throw new Auth0Exception("Response Parsing Error");
}
}.executeAsync();

Exception exception = null;
Object result = null;

try {
result = request.get();
} catch (Exception e) {
exception = e;
}

assertThat(exception, is(notNullValue()));
assertThat(result, is(nullValue()));
assertThat(exception.getCause(), is(instanceOf(Auth0Exception.class)));
assertThat(exception.getCause().getMessage(), is("Failed to execute request"));
assertThat(exception.getCause().getCause(), is(instanceOf(IOException.class)));
assertThat(exception.getCause().getCause().getMessage(), is("Error!"));
}

@Test
public void asyncCompletesWithExceptionWhenResponseParsingFails() throws Exception {
doReturn(call).when(client).newCall(any());

doAnswer(invocation -> {
((Callback) invocation.getArgument(0)).onResponse(call, response);
return null;
}).when(call).enqueue(any());

CompletableFuture<?> request = new MockBaseRequest<String>(client) {
@Override
protected String parseResponse(Response response) throws Auth0Exception {
throw new Auth0Exception("Response Parsing Error");
}
}.executeAsync();

Exception exception = null;
Object result = null;

try {
result = request.get();
} catch (Exception e) {
exception = e;
}

assertThat(result, is(nullValue()));
assertThat(exception, is(notNullValue()));
assertThat(exception.getCause(), is(instanceOf(Auth0Exception.class)));
assertThat(exception.getCause().getMessage(), is("Response Parsing Error"));
}

@Test
public void asyncCompletesSuccessfully() {
doReturn(call).when(client).newCall(any());

doAnswer(invocation -> {
((Callback) invocation.getArgument(0)).onResponse(call, response);
return null;
}).when(call).enqueue(any());

CompletableFuture<?> request = new MockBaseRequest<String>(client) {
@Override
protected String parseResponse(Response response) throws Auth0Exception {
return "Success";
}
}.executeAsync();

Exception exception = null;
Object result = null;

try {
result = request.get();
} catch (Exception e) {
exception = e;
}

assertThat(exception, is(nullValue()));
assertThat(result, is(instanceOf(String.class)));
assertThat(result, is("Success"));
}

private abstract static class MockBaseRequest<String> extends BaseRequest {
MockBaseRequest(OkHttpClient client) {
super(client);
}

@Override
protected Request createRequest() {
protected Request createRequest() throws Auth0Exception {
return null;
}
}
Expand Down
20 changes: 20 additions & 0 deletions src/test/java/com/auth0/net/RequestTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.auth0.net;

import com.auth0.exception.Auth0Exception;
import org.junit.Test;
import static org.junit.Assert.assertThrows;

public class RequestTest {

@Test
public void defaultImplementationShouldThrow() {
assertThrows("executeAsync",
UnsupportedOperationException.class,
new Request<String>() {
@Override
public String execute() throws Auth0Exception {
return null;
}
}::executeAsync);
}
}