Skip to content

Commit facf00c

Browse files
authored
Merge pull request #3 from bleeding182/dev/error_codes
Add better support for error codes
2 parents d560f9a + 7a39e10 commit facf00c

File tree

5 files changed

+115
-31
lines changed

5 files changed

+115
-31
lines changed

auth/src/main/java/com/davidmedenjak/auth/AuthCallback.java

+8-4
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
import android.os.Bundle;
77
import android.os.Handler;
88

9+
import java.io.IOException;
10+
911
import androidx.annotation.NonNull;
1012
import androidx.annotation.Nullable;
1113

12-
import java.io.IOException;
13-
1414
/**
1515
* A callback to link your app with {@link OAuthAuthenticator}. This is used to refresh your users
1616
* access tokens or start a login flow.
@@ -34,8 +34,12 @@ public interface AuthCallback {
3434
*
3535
* @param refreshToken the refresh token stored from {@link TokenPair#refreshToken} at the time
3636
* of the last login or refresh
37-
* @throws IOException when there is an error refreshing the token
37+
* @throws IOException when there is an error refreshing the token. This defaults to {@link
38+
* TokenRefreshError#NETWORK}.
39+
* @throws TokenRefreshError when there is an error refreshing the token to provide a better
40+
* error to the listeners.
3841
* @return the new TokenPair to use for future authentication
3942
*/
40-
TokenPair authenticate(@NonNull final String refreshToken) throws IOException;
43+
TokenPair authenticate(@NonNull final String refreshToken)
44+
throws IOException, TokenRefreshError;
4145
}

auth/src/main/java/com/davidmedenjak/auth/OAuthAuthenticator.java

+9-6
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,20 @@
88
import android.content.Context;
99
import android.content.Intent;
1010
import android.os.Bundle;
11-
import androidx.annotation.NonNull;
12-
import androidx.annotation.Nullable;
1311
import android.text.TextUtils;
1412
import android.util.Log;
1513

14+
import java.io.IOException;
1615
import java.util.ArrayList;
1716
import java.util.Arrays;
1817
import java.util.HashMap;
1918
import java.util.List;
2019

2120
import javax.inject.Inject;
2221

22+
import androidx.annotation.NonNull;
23+
import androidx.annotation.Nullable;
24+
2325
/**
2426
* A basic implementation of an {@link AbstractAccountAuthenticator} to support OAuth use cases,
2527
* where accounts get persisted with a refresh token as the {@code password}.
@@ -252,7 +254,9 @@ private void refresh(String refreshToken) {
252254
try {
253255
TokenPair result = service.authenticate(refreshToken);
254256
onAuthenticated(result);
255-
} catch (Exception e) {
257+
} catch (IOException e) {
258+
onError(TokenRefreshError.NETWORK);
259+
} catch (TokenRefreshError e) {
256260
onError(e);
257261
}
258262
}
@@ -265,9 +269,8 @@ private void onAuthenticated(@NonNull TokenPair tokenPair) {
265269
returnResultToQueuedResponses(account, (r) -> r.onResult(bundle));
266270
}
267271

268-
private void onError(@NonNull Throwable error) {
269-
int code = AccountManager.ERROR_CODE_NETWORK_ERROR;
270-
returnResultToQueuedResponses(account, (r) -> r.onError(code, error.getMessage()));
272+
private void onError(@NonNull TokenRefreshError error) {
273+
returnResultToQueuedResponses(account, (r) -> r.onError(error.getCode(), error.getErrorMessage()));
271274
}
272275
}
273276
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.davidmedenjak.auth;
2+
3+
import android.accounts.AccountManager;
4+
5+
import androidx.annotation.Nullable;
6+
7+
/**
8+
* Error to report failure when trying to refresh a token. We are limited by {@code AccountManager}
9+
* to return an error code and errorMessage only.
10+
*
11+
* @see #TokenRefreshError(int, String)
12+
*/
13+
public class TokenRefreshError extends Exception {
14+
15+
public static final TokenRefreshError NETWORK =
16+
new TokenRefreshError(AccountManager.ERROR_CODE_NETWORK_ERROR, null);
17+
18+
private final int code;
19+
private final String errorMessage;
20+
21+
/**
22+
* Construct a new error using an error code and message to return as a result from the token
23+
* refresh operation.
24+
*
25+
* @param code the error code. May be one of the predefined error codes from {@link
26+
* android.accounts.AccountManager AccountManager}
27+
* <ul>
28+
* <li>{@link android.accounts.AccountManager#ERROR_CODE_REMOTE_EXCEPTION
29+
* ERROR_CODE_REMOTE_EXCEPTION},
30+
* <li>{@link android.accounts.AccountManager#ERROR_CODE_NETWORK_ERROR
31+
* ERROR_CODE_NETWORK_ERROR},
32+
* <li>{@link android.accounts.AccountManager#ERROR_CODE_CANCELED ERROR_CODE_CANCELED},
33+
* <li>{@link android.accounts.AccountManager#ERROR_CODE_INVALID_RESPONSE
34+
* ERROR_CODE_INVALID_RESPONSE},
35+
* <li>{@link android.accounts.AccountManager#ERROR_CODE_UNSUPPORTED_OPERATION
36+
* ERROR_CODE_UNSUPPORTED_OPERATION},
37+
* <li>{@link android.accounts.AccountManager#ERROR_CODE_BAD_ARGUMENTS
38+
* ERROR_CODE_BAD_ARGUMENTS},
39+
* <li>{@link android.accounts.AccountManager#ERROR_CODE_BAD_REQUEST
40+
* ERROR_CODE_BAD_REQUEST},
41+
* <li>{@link android.accounts.AccountManager#ERROR_CODE_BAD_AUTHENTICATION
42+
* ERROR_CODE_BAD_AUTHENTICATION}
43+
* </ul>
44+
*
45+
* @param errorMessage an optional errorMessage
46+
*/
47+
public TokenRefreshError(int code, @Nullable String errorMessage) {
48+
this.code = code;
49+
this.errorMessage = errorMessage;
50+
}
51+
52+
public int getCode() {
53+
return code;
54+
}
55+
56+
public String getErrorMessage() {
57+
return errorMessage;
58+
}
59+
}
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.davidmedenjak.auth;
22

3+
import java.io.IOException;
4+
35
@FunctionalInterface
46
public interface Function1<T, R> {
5-
R run(T object);
7+
R run(T object) throws IOException, TokenRefreshError;
68
}

auth/src/test/java/com/davidmedenjak/auth/OAuthAuthenticatorTest.java

+36-20
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,16 @@
1616
import org.robolectric.RuntimeEnvironment;
1717

1818
import java.io.IOException;
19+
import java.net.UnknownHostException;
1920

2021
import static org.junit.Assert.assertEquals;
2122
import static org.junit.Assert.assertNotNull;
2223
import static org.junit.Assert.assertNull;
2324
import static org.junit.Assert.fail;
2425
import static org.mockito.ArgumentMatchers.any;
25-
import static org.mockito.ArgumentMatchers.anyInt;
2626
import static org.mockito.ArgumentMatchers.anyString;
2727
import static org.mockito.ArgumentMatchers.argThat;
28+
import static org.mockito.ArgumentMatchers.eq;
2829
import static org.mockito.Mockito.mock;
2930
import static org.mockito.Mockito.times;
3031
import static org.mockito.Mockito.verify;
@@ -41,7 +42,7 @@ public class OAuthAuthenticatorTest {
4142
private AccountAuthenticatorResponse response;
4243

4344
@Before
44-
public void setUp() throws Exception {
45+
public void setUp() {
4546
am = AccountManager.get(RuntimeEnvironment.application);
4647

4748
response = mock(AccountAuthenticatorResponse.class);
@@ -51,9 +52,7 @@ public void setUp() throws Exception {
5152
}
5253

5354
@Test
54-
public void accessTokenReturnedImmediately()
55-
throws NetworkErrorException, AuthenticatorException, OperationCanceledException,
56-
IOException {
55+
public void accessTokenReturnedImmediately() {
5756
am.addAccountExplicitly(account, null, null);
5857
final String accessToken = "access1";
5958
am.setAuthToken(account, tokenType, accessToken);
@@ -67,23 +66,21 @@ public void accessTokenReturnedImmediately()
6766
}
6867

6968
@Test
70-
public void errorOnInvalidRefreshToken()
71-
throws NetworkErrorException, AuthenticatorException, OperationCanceledException,
72-
IOException {
69+
public void errorOnInvalidRefreshToken() throws IOException, TokenRefreshError {
7370
am.addAccountExplicitly(account, null, null);
7471
am.setPassword(account, "invalid");
7572

7673
withServiceResponse(
7774
callback -> {
78-
throw new RuntimeException();
75+
throw new UnknownHostException();
7976
});
8077

8178
// when
8279
Bundle result = getAuthTokenWithResponse();
8380

8481
// then
8582
assertNull(result);
86-
verify(response).onError(anyInt(), any());
83+
verify(response).onError(eq(AccountManager.ERROR_CODE_NETWORK_ERROR), any());
8784
}
8885

8986
@Test
@@ -95,8 +92,8 @@ public void noLoginIntentProvided() throws NetworkErrorException {
9592

9693
@Test
9794
public void accessTokenReturnedAfterRefresh()
98-
throws NetworkErrorException, AuthenticatorException, OperationCanceledException,
99-
IOException {
95+
throws AuthenticatorException, OperationCanceledException, IOException,
96+
TokenRefreshError {
10097
am.addAccountExplicitly(account, null, null);
10198
final String accessToken = "access1";
10299
am.setPassword(account, "refresh1");
@@ -113,9 +110,7 @@ public void accessTokenReturnedAfterRefresh()
113110
}
114111

115112
@Test
116-
public void multipleRequestsTriggerASingleRefresh()
117-
throws NetworkErrorException, AuthenticatorException, OperationCanceledException,
118-
IOException {
113+
public void multipleRequestsTriggerASingleRefresh() throws IOException, TokenRefreshError {
119114
am.addAccountExplicitly(account, null, null);
120115
final String accessToken = "access1";
121116
am.setPassword(account, "refresh1");
@@ -148,9 +143,7 @@ public void multipleRequestsTriggerASingleRefresh()
148143
}
149144

150145
@Test
151-
public void multipleUserRequestsTriggerRunConcurrently()
152-
throws NetworkErrorException, AuthenticatorException, OperationCanceledException,
153-
IOException {
146+
public void multipleUserRequestsTriggerRunConcurrently() throws IOException, TokenRefreshError {
154147

155148
// given some complicated setup... simulate "concurrency" :/
156149
Account[] users =
@@ -204,11 +197,34 @@ public void multipleUserRequestsTriggerRunConcurrently()
204197
}
205198
}
206199

207-
private void withServiceResponse(Function0<TokenPair> action) throws IOException {
200+
@Test
201+
public void returnCustomError() throws IOException, TokenRefreshError {
202+
am.addAccountExplicitly(account, null, null);
203+
am.setPassword(account, "invalid");
204+
205+
final int errCode = AccountManager.ERROR_CODE_BAD_AUTHENTICATION;
206+
final String errMessage = "unauthorized";
207+
208+
withServiceResponse(
209+
callback -> {
210+
throw new TokenRefreshError(errCode, errMessage);
211+
});
212+
213+
// when
214+
Bundle result = getAuthTokenWithResponse();
215+
216+
// then
217+
assertNull(result);
218+
verify(response).onError(errCode, errMessage);
219+
}
220+
221+
private void withServiceResponse(Function0<TokenPair> action)
222+
throws TokenRefreshError, IOException {
208223
withServiceResponse((obj1) -> action.run());
209224
}
210225

211-
private void withServiceResponse(Function1<String, TokenPair> action) throws IOException {
226+
private void withServiceResponse(Function1<String, TokenPair> action)
227+
throws TokenRefreshError, IOException {
212228
Mockito.doAnswer(
213229
invocation -> {
214230
String refreshToken = (String) invocation.getArguments()[0];

0 commit comments

Comments
 (0)