Android - Retrofit 2 - Authenticator Result - android

I'm trying to use Retrofit (2.0.0-beta3), but when using an Authenticator to add a token, I can't seem to get the data from the synchronous call. Our logging on the back-end just shows a lot of login attempts, but I can't get the data from the body to actually add to the header.
public static class TokenAuthenticator implements Authenticator {
#Override
public Request authenticate(Route route, Response response) throws IOException {
// Refresh your access_token using a synchronous api request
UserService userService = createService(UserService.class);
Call<Session> call = userService.emailLogin(new Credentials("handle", "pass"));
// This call is made correctly, as it shows up on the back-end.
Session body = call.execute().body();
// This line is never hit.
Logger.d("Session token: " + body.token);
// Add new header to rejected request and retry it
return response.request().newBuilder()
.header("Auth-Token", body.token)
.build();
}
}
I'm not exactly too sure on why it's not even printing anything out. Any tips on how to solve this issue would be greatly appreciated, thanks for taking the time to help.
These are the sources I've been reading on how to implement Retrofit.
Using Authenticator:
https://stackoverflow.com/a/31624433/3106174
https://github.com/square/okhttp/wiki/Recipes#handling-authentication
Making synchronous calls with Retrofit 2:
https://futurestud.io/blog/retrofit-synchronous-and-asynchronous-requests

I managed to get a decent solution using the TokenAuthenticator and an Interceptor and thought I'd share the idea as it may help some others.
Adding the 'TokenInterceptor' class that handles adding the token to the header is the token exists, and the 'TokenAuthenticator' class handles the case when there is no token, and we need to generate one.
I'm sure there are some better ways to implement this, but it's a good starting point I think.
public static class TokenAuthenticator implements Authenticator {
#Override
public Request authenticate( Route route, Response response) throws IOException {
...
Session body = call.execute().body();
Logger.d("Session token: " + body.token);
// Storing the token somewhere.
session.token = body.token;
...
}
private static class TokenInterceptor implements Interceptor {
#Override
public Response intercept( Chain chain ) throws IOException {
Request originalRequest = chain.request();
// Nothing to add to intercepted request if:
// a) Authorization value is empty because user is not logged in yet
// b) There is already a header with updated Authorization value
if (authorizationTokenIsEmpty() || alreadyHasAuthorizationHeader(originalRequest)) {
return chain.proceed(originalRequest);
}
// Add authorization header with updated authorization value to intercepted request
Request authorisedRequest = originalRequest.newBuilder()
.header("Auth-Token", session.token )
.build();
return chain.proceed(authorisedRequest);
}
}
Source:
http://lgvalle.xyz/2015/07/27/okhttp-authentication/

I have similar authenticator and it works with 2.0.0-beta2.
If you get lots of login attempts from you Authenticator, I suggest make sure that when you make the synchronous call, you are not using Authenticator with that call.
That could end up in loop, if also your "emailLogin" fails.
Also I would recommend adding loggingInterceptor to see all trafic to server: Logging with Retrofit 2

I know it's a late answer but for anyone still wondering how to Add / Refresh token with Retrofit 2 Authenticator, here is a working solution:
Note: preferenceHelper is your Preference Manager class where you set/get your shared preferences.
public class AuthenticationHelper implements Authenticator {
private static final String HEADER_AUTHORIZATION = "Authorization";
private static final int REFRESH_TOKEN_FAIL = 403;
private Context context;
AuthenticationHelper(#ApplicationContext Context context) {
this.context = context;
}
#Override
public Request authenticate(#NonNull Route route, #NonNull Response response) throws IOException {
// We need to have a token in order to refresh it.
String token = preferencesHelper.getAccessToken();
if (token == null)
return null;
synchronized (this) {
String newToken = preferencesHelper.getAccessToken();
if (newToken == null)
return null;
// Check if the request made was previously made as an authenticated request.
if (response.request().header(HEADER_AUTHORIZATION) != null) {
// If the token has changed since the request was made, use the new token.
if (!newToken.equals(token)) {
return response.request()
.newBuilder()
.removeHeader(HEADER_AUTHORIZATION)
.addHeader(HEADER_AUTHORIZATION, "Bearer " + newToken)
.build();
}
JsonObject refreshObject = new JsonObject();
refreshObject.addProperty("refreshToken", preferencesHelper.getRefreshToken());
retrofit2.Response<UserToken> tokenResponse = apiService.refreshToken(refreshObject).execute();
if (tokenResponse.isSuccessful()) {
UserToken userToken = tokenResponse.body();
if (userToken == null)
return null;
preferencesHelper.saveAccessToken(userToken.getToken());
preferencesHelper.saveRefreshToken(userToken.getRefreshToken());
// Retry the request with the new token.
return response.request()
.newBuilder()
.removeHeader(HEADER_AUTHORIZATION)
.addHeader(HEADER_AUTHORIZATION, "Bearer " + userToken.getToken())
.build();
} else {
if (tokenResponse.code() == REFRESH_TOKEN_FAIL) {
logoutUser();
}
}
}
}
return null;
}
private void logoutUser() {
// logout user
}
}
Also note:
preferenceHelper and apiService needs to be provided in some way.
This is not an example that will work for all systems and api's but an example in how adding and refreshing the token should be done using Retrofit 2 Authenticator

Related

Send JWT token using AWS Android SDK

We have some API Gateway endpoints. Each endpoint is protected with a Custom Authorizer (CA). This CA inspect the HTTP call and verify the presence of the Authorization header. This header must contains an OpenId Connect token (a simple JWT token), so the CA can inspect it and make some checks and validations.
When we use Postman to invoke the endpoint, it works without a problem, since we can setup the right headers.
The problems start to raises when we use the generated Android SDK to make the same call, since each attempt to make the call sends an AWS4 signature as header. We can figure out how to send Authorization header with JWT.
We get what we need by extending ApiClientFactory class and adding the header in an explicit ways:
public class CustomApiClientFactory extends ApiClientFactory {
private String LOGIN_NAME = "a.provider.com";
private AWSCredentialsProvider provider;
#Override
public ApiClientFactory credentialsProvider(AWSCredentialsProvider provider) {
this.provider = provider;
return super.credentialsProvider(provider);
}
#Override
ApiClientHandler getHandler(String endpoint, String apiName) {
final Signer signer = provider == null ? null : getSigner(getRegion(endpoint));
// Ensure we always pass a configuration to the handler
final ClientConfiguration configuration = new ClientConfiguration();
return new ApiClientHandler(endpoint, apiName, signer, provider, null, configuration) {
#Override
Request<?> buildRequest(Method method, Object[] args) {
Request<?> request = super.buildRequest(method, args);
request.addHeader("Authorization", "Bearer " + ((CognitoCachingCredentialsProvider) provider).getLogins().get(LOGIN_NAME));
return request;
}
};
}
}
Even if it works, it sounds to me like a workaround. There is some known best practice in facing this problem?
API Gateway allows the request to be defined for each method just add a HTTP request header in the method request and the generated SDK will have a field to specify the header.
Console:
Generated Android SDK:
#com.amazonaws.mobileconnectors.apigateway.annotation.Operation(path = "/companies", method = "GET")
CompanyList companiesGet(
#com.amazonaws.mobileconnectors.apigateway.annotation.Parameter(name = "x-vendor-authorization", location = "header")
String xVendorAuthorization,
#com.amazonaws.mobileconnectors.apigateway.annotation.Parameter(name = "continuationToken", location = "query")
String continuationToken,
#com.amazonaws.mobileconnectors.apigateway.annotation.Parameter(name = "pageSize", location = "query")
String pageSize,
#com.amazonaws.mobileconnectors.apigateway.annotation.Parameter(name = "properties", location = "query")
String properties);
SDK call:
client.companiesGet("abc123AuthToken", "continueToken1", "20", "prop1");

How to send a POST request from RestSharp on android to a WebAPI server

I have an android app that needs to make a call to a asp.net core web api server.
I am using RestSharp to make the request.
Here is the code generating the request:
public LoginResponse SignInWithGoogle(string token)
{
//Api request for token
RestRequest request = new RestRequest("login/google", Method.POST);
request.AddJsonBody(new { Token = token });
//request.AddParameter("token", token, ParameterType.GetOrPost);
var response = restClient.Execute<LoginResponse>(request);
if (response.ErrorException != null)
{
throw new Exception("The APi request failed. See inner exception for more details", response.ErrorException);
}
AuthenticationToken = response.Data.token;
restClient.Authenticator = authenticator;
return response.Data;
}
Here is the web api code:
[AllowAnonymous]
[HttpPost]
[Route("google")]
public IActionResult GoogleLogin([FromBody] GoogleLoginDto data)
{
GoogleJsonWebSignature.Payload payload;
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
SigningCredentials creds = new SigningCredentials(Global.symmetricSecurityKey, SecurityAlgorithms.HmacSha256);
//Try to validate the Google token.
try
{
payload = GoogleJsonWebSignature.ValidateAsync(data.Token).Result;
}
catch (Exception e)
{
return Unauthorized();
}
...
}
GoogleLoginDto contains one property Token that is public.
The problem is that I get a 404. It seems to me that the JSON in the request is not being serialized to GoogleLoginDto but I can't find out why... I'm guessing because the API can't find the data field and so thinks i'm asking for a route that doesn't exist...
I also tried doing request.AddParameter("token", token, ParameterType.GetOrPost); as you can see, but I get an exception saying that Content-Type can't be null.
I thought about adding the Content-Type header but that seems ridiculous because RestSharp is supposed to determine that automatically...
Can anyone see anything I'm missing here? Thanks.
The API is unable to map the provided URL to a controller action. That is what it is 404 Not Found. Nothing to do with the data. It is the URL.
Given that the desired URL is login/google, ensure that the target controller has the proper routes defined that would allow the request to be mapped to the correct actions.
[Route("login")] // Route prefix
public class LoginController : Controller {
[AllowAnonymous]
[HttpPost("google")] // Matches POST login/google
public async Task<IActionResult> GoogleLogin([FromBody] GoogleLoginDto data) {
if(ModelState.IsValid) {
GoogleJsonWebSignature.Payload payload;
var tokenHandler = new JwtSecurityTokenHandler();
var creds = new SigningCredentials(Global.symmetricSecurityKey, SecurityAlgorithms.HmacSha256);
//Try to validate the Google token.
try {
payload = await GoogleJsonWebSignature.ValidateAsync(data.Token);
} catch (Exception e) {
return Unauthorized();
}
return Ok();
}
return BadRequest();
}
}

Using Retrofit with Restfull Post WebServices

I'm trying to use Retrofit with Restful WebService. Everything seems alright, but somehow when I run this code this will always returns this
Method not found. Retrofit 404 Error
Here is my WebServices Code
public function processApi() {
$func = strtolower(trim(str_replace("/","",$_POST['request'])));
if ((int)method_exists($this,$func) > 0) {
$this->$func();
} else {
// If the method not exist with in this class, response would be "Page not found".
$this->response('Method not found',404);
}
}
private function login() {
// Cross validation if the request method is POST else it will return "Not Acceptable" status
if ($this->get_request_method() != "POST") {
// If invalid inputs "Bad Request" status message and reason
$error = array('status' => "0", "msg" => "Bad Request");
$this->response($this->json($error), 406);
}
// Input validations
if (empty($email) and empty($password)) {
$error = array('status' => "0", "msg" => "Invalid Email address or Password");
$this->response($this->json($error), 400);
}
}
public class ObjectPost {
#SerializedName("request")
String request;
#SerializedName("email")
String event_id;
public void setRequest(String request) {
this.request = request;
}
public void setEvent_id(String event_id) {
this.event_id = event_id;
}
}
And here is my Android Request Code
public class RestClient {
public interface ClientInterface {
#POST(Config.LOGIN_URL)
void login(#Body ObjectPost mObject,
Callback<LoginBeans> callback);
}
public static ClientInterface initRestAdapter() {
OkHttpClient client = new OkHttpClient();
return (ClientInterface) new RestAdapter.Builder()
.setLogLevel(RestAdapter.LogLevel.FULL)
.setClient(new OkClient(client))
.setEndpoint(Config.SERVER_URL)
.build()
.create(ClientInterface.class);
}
}
The value in the
Config.LOGIN_URL
Is most likely incorrect. Please remember that
Config.SERVER_URL
Must contain the base URL address. e.g. http://www.server.com/ (also please note the slashes are important)
Next, what is in your attribute must be only the remainder of the specific method that would be appended on that base url. e.g. if the method you want to call is login, it should be
#POST("/login")
Once again, I am not kidding about the slashes.
Also remember that if a query parameter is sent through as null, retrofit ignores it (you may face this problem later).
If you need any further help, you already have your loglevel set to full, please add the logcat to your question so we can see what is happening.
Your code looks alright.
Basically you need to make sure your backend. Ensure that the controller or whatever is actually right.
Maybe this will be quite applicable to you https://github.com/square/retrofit/issues/789
So, instead of looking at your Android code, it's a good reason to look somewhere else I would say.

Android OkHttp, refresh expired token

Scenario: I am using OkHttp / Retrofit to access a web service: multiple HTTP requests are sent out at the same time. At some point the auth token expires, and multiple requests will get a 401 response.
Issue: In my first implementation I use an interceptor (here simplified) and each thread tries to refresh the token. This leads to a mess.
public class SignedRequestInterceptor implements Interceptor {
#Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
// 1. sign this request
request = request.newBuilder()
.header(AUTH_HEADER_KEY, BEARER_HEADER_VALUE + token)
.build();
// 2. proceed with the request
Response response = chain.proceed(request);
// 3. check the response: have we got a 401?
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
// ... try to refresh the token
newToken = mAuthService.refreshAccessToken(..);
// sign the request with the new token and proceed
Request newRequest = request.newBuilder()
.removeHeader(AUTH_HEADER_KEY)
.addHeader(AUTH_HEADER_KEY, BEARER_HEADER_VALUE + newToken.getAccessToken())
.build();
// return the outcome of the newly signed request
response = chain.proceed(newRequest);
}
return response;
}
}
Desired solution: All threads should wait for one single token refresh: the first failing request triggers the refresh, and together with the other requests waits for the new token.
What is a good way to proceed about this? Can some built-in features of OkHttp (like the Authenticator) be of help? Thank you for any hint.
I had the same problem and I managed to solve it using a ReentrantLock.
import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import timber.log.Timber;
public class RefreshTokenInterceptor implements Interceptor {
private Lock lock = new ReentrantLock();
#Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
// first thread will acquire the lock and start the refresh token
if (lock.tryLock()) {
Timber.i("refresh token thread holds the lock");
try {
// this sync call will refresh the token and save it for
// later use (e.g. sharedPreferences)
authenticationService.refreshTokenSync();
Request newRequest = recreateRequestWithNewAccessToken(chain);
return chain.proceed(newRequest);
} catch (ServiceException exception) {
// depending on what you need to do you can logout the user at this
// point or throw an exception and handle it in your onFailure callback
return response;
} finally {
Timber.i("refresh token finished. release lock");
lock.unlock();
}
} else {
Timber.i("wait for token to be refreshed");
lock.lock(); // this will block the thread until the thread that is refreshing
// the token will call .unlock() method
lock.unlock();
Timber.i("token refreshed. retry request");
Request newRequest = recreateRequestWithNewAccessToken(chain);
return chain.proceed(newRequest);
}
} else {
return response;
}
}
private Request recreateRequestWithNewAccessToken(Chain chain) {
String freshAccessToken = sharedPreferences.getAccessToken();
Timber.d("[freshAccessToken] %s", freshAccessToken);
return chain.request().newBuilder()
.header("access_token", freshAccessToken)
.build();
}
}
The main advantage of using this solution is that you can write an unit test using mockito and test it. You will have to enable Mockito Incubating feature for mocking final classes (response from okhttp). Read more about here.
The test looks something like this:
#RunWith(MockitoJUnitRunner.class)
public class RefreshTokenInterceptorTest {
private static final String FRESH_ACCESS_TOKEN = "fresh_access_token";
#Mock
AuthenticationService authenticationService;
#Mock
RefreshTokenStorage refreshTokenStorage;
#Mock
Interceptor.Chain chain;
#BeforeClass
public static void setup() {
Timber.plant(new Timber.DebugTree() {
#Override
protected void log(int priority, String tag, String message, Throwable t) {
System.out.println(Thread.currentThread() + " " + message);
}
});
}
#Test
public void refreshTokenInterceptor_works_as_expected() throws IOException, InterruptedException {
Response unauthorizedResponse = createUnauthorizedResponse();
when(chain.proceed((Request) any())).thenReturn(unauthorizedResponse);
when(authenticationService.refreshTokenSync()).thenAnswer(new Answer<Boolean>() {
#Override
public Boolean answer(InvocationOnMock invocation) throws Throwable {
//refresh token takes some time
Thread.sleep(10);
return true;
}
});
when(refreshTokenStorage.getAccessToken()).thenReturn(FRESH_ACCESS_TOKEN);
Request fakeRequest = createFakeRequest();
when(chain.request()).thenReturn(fakeRequest);
final Interceptor interceptor = new RefreshTokenInterceptor(authenticationService, refreshTokenStorage);
Timber.d("5 requests try to refresh token at the same time");
final CountDownLatch countDownLatch5 = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
#Override
public void run() {
try {
interceptor.intercept(chain);
countDownLatch5.countDown();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}).start();
}
countDownLatch5.await();
verify(authenticationService, times(1)).refreshTokenSync();
Timber.d("next time another 3 threads try to refresh the token at the same time");
final CountDownLatch countDownLatch3 = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
#Override
public void run() {
try {
interceptor.intercept(chain);
countDownLatch3.countDown();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}).start();
}
countDownLatch3.await();
verify(authenticationService, times(2)).refreshTokenSync();
Timber.d("1 thread tries to refresh the token");
interceptor.intercept(chain);
verify(authenticationService, times(3)).refreshTokenSync();
}
private Response createUnauthorizedResponse() throws IOException {
Response response = mock(Response.class);
when(response.code()).thenReturn(401);
return response;
}
private Request createFakeRequest() {
Request request = mock(Request.class);
Request.Builder fakeBuilder = createFakeBuilder();
when(request.newBuilder()).thenReturn(fakeBuilder);
return request;
}
private Request.Builder createFakeBuilder() {
Request.Builder mockBuilder = mock(Request.Builder.class);
when(mockBuilder.header("access_token", FRESH_ACCESS_TOKEN)).thenReturn(mockBuilder);
return mockBuilder;
}
}
You should not use interceptors or implement the retry logic yourself as this leads to a maze of recursive issues.
Instead implement the okhttp's Authenticator which is provided specifically to solve this problem:
okHttpClient.setAuthenticator(...);
Thanks for your answers - they led me to the solution. I ended up using a ConditionVariable lock and an AtomicBoolean. Here's how you can achieve this: read through the comments.
/**
* This class has two tasks:
* 1) sign requests with the auth token, when available
* 2) try to refresh a new token
*/
public class SignedRequestInterceptor implements Interceptor {
// these two static variables serve for the pattern to refresh a token
private final static ConditionVariable LOCK = new ConditionVariable(true);
private static final AtomicBoolean mIsRefreshing = new AtomicBoolean(false);
...
#Override
public Response intercept(#NonNull Chain chain) throws IOException {
Request request = chain.request();
// 1. sign this request
....
// 2. proceed with the request
Response response = chain.proceed(request);
// 3. check the response: have we got a 401?
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
if (!TextUtils.isEmpty(token)) {
/*
* Because we send out multiple HTTP requests in parallel, they might all list a 401 at the same time.
* Only one of them should refresh the token, because otherwise we'd refresh the same token multiple times
* and that is bad. Therefore we have these two static objects, a ConditionVariable and a boolean. The
* first thread that gets here closes the ConditionVariable and changes the boolean flag.
*/
if (mIsRefreshing.compareAndSet(false, true)) {
LOCK.close();
// we're the first here. let's refresh this token.
// it looks like our token isn't valid anymore.
mAccountManager.invalidateAuthToken(AuthConsts.ACCOUNT_TYPE, token);
// do we have an access token to refresh?
String refreshToken = mAccountManager.getUserData(account, HorshaAuthenticator.KEY_REFRESH_TOKEN);
if (!TextUtils.isEmpty(refreshToken)) {
.... // refresh token
}
LOCK.open();
mIsRefreshing.set(false);
} else {
// Another thread is refreshing the token for us, let's wait for it.
boolean conditionOpened = LOCK.block(REFRESH_WAIT_TIMEOUT);
// If the next check is false, it means that the timeout expired, that is - the refresh
// stuff has failed. The thread in charge of refreshing the token has taken care of
// redirecting the user to the login activity.
if (conditionOpened) {
// another thread has refreshed this for us! thanks!
....
// sign the request with the new token and proceed
// return the outcome of the newly signed request
response = chain.proceed(newRequest);
}
}
}
}
// check if still unauthorized (i.e. refresh failed)
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
... // clean your access token and prompt user for login again.
}
// returning the response to the original request
return response;
}
}
If you wan't your threads to bock while the first one refresh the token you can use a synchronized block.
private final static Object lock = new Object();
private static long lastRefresh;
...
synchronized(lock){ // lock all thread untill token is refreshed
// only the first thread does the w refresh
if(System.currentTimeMillis()-lastRefresh>600000){
token = refreshToken();
lastRefresh=System.currentTimeMillis();
}
}
Here 600000 (10 min) is arbitrary this number should be big enouth to prevent muliple refresh call and smaller than your token expiration time so that you call the refresh when the token expires.
Edited for thread safety
Havent looked at OkHttp or retrofit but how about having a static flag that is set as soon as a token fails and check for that flag before you request a new token?
private static AtomicBoolean requestingToken = new AtomicBoolean(false);
//.....
if (requestingToken.get() == false)
{
requestingToken.set(true);
//.... request a new token
}

Mock Api Responses in Android Testing

I'm looking for a way to mock api responses in android tests.
I have read the roboelectric could be used for this but I would really appreciate any advice on this.
After a small bit of looking around on the web I have found MockWebServer to be what I was looking for.
A scriptable web server for testing HTTP clients. This library makes it easy to test that your app Does The Right Thing when it makes HTTP and HTTPS calls. It lets you specify which responses to return and then verify that requests were made as expected.
To get setup just add the following to your build.gradle file.
androidTestCompile 'com.google.mockwebserver:mockwebserver:20130706'
Here is a simple example taking from their GitHub page.
public void test() throws Exception {
// Create a MockWebServer. These are lean enough that you can create a new
// instance for every unit test.
MockWebServer server = new MockWebServer();
// Schedule some responses.
server.enqueue(new MockResponse().setBody("hello, world!"));
// Start the server.
server.play();
// Ask the server for its URL. You'll need this to make HTTP requests.
URL baseUrl = server.getUrl("/v1/chat/");
// Exercise your application code, which should make those HTTP requests.
// Responses are returned in the same order that they are enqueued.
Chat chat = new Chat(baseUrl);
chat.loadMore();
assertEquals("hello, world!", chat.messages());
// Shut down the server. Instances cannot be reused.
server.shutdown();
}
Hope this helps.
MockWebServer didn't work for me with AndroidTestCase. For instance, ECONNREFUSED error happened quite randomly (described in https://github.com/square/okhttp/issues/1069). I didn't try Robolectric.
As of OkHttp 2.2.0, I found an alternative way which worked well for me: Interceptors. I placed the whole mock response inside a json file stored on androidTest/assets/, say, 'mock_response.json'. When I instanced an OkHttp for testing, I exposed an Interceptor which I would rewrite the incoming response. Basically, body() would instead stream the data in 'mock_response.json'.
public class FooApiTest extends AndroidTestCase {
public void testFetchData() throws InterruptedException, IOException {
// mock_response.json is placed on 'androidTest/assets/'
final InputStream stream = getContext().getAssets().open("mock_response.json");
OkHttpClient httpClient = new OkHttpClient();
httpClient.interceptors().add(new Interceptor() {
#Override
public Response intercept(Chain chain) throws IOException {
return new Response.Builder()
.protocol(Protocol.HTTP_2)
// This is essential as it makes response.isSuccessful() returning true.
.code(200)
.request(chain.request())
.body(new ResponseBody() {
#Override
public MediaType contentType() {
return null;
}
#Override
public long contentLength() {
// Means we don't know the length beforehand.
return -1;
}
#Override
public BufferedSource source() {
try {
return new Buffer().readFrom(stream);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
})
.build();
}
});
FooApi api = new FooApi(httpClient);
api.fetchData();
// TODO: Let's assert the data here.
}
}
This is now even easier with Mockinizer which makes working with MockWebServer easier:
val mocks: Map<RequestFilter, MockResponse> = mapOf(
RequestFilter("/mocked") to MockResponse().apply {
setResponseCode(200)
setBody("""{"title": "Banana Mock"}""")
},
RequestFilter("/mockedError") to MockResponse().apply {
setResponseCode(400)
}
)
Just create a map of RequestFilter and MockResponses and then plug it into your OkHttpClient builder chain:
OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.mockinize(mocks) // <-- just plug in your custom mocks here
.build()

Categories

Resources