[JAVA][FEIGN] Automatically retry request that fail due to a 401 or 403 (#10021)

* Renew the access token after receiving a 401/403

Feign clients tries to renew the access token after it receives a 401 or 403. It Retries the request 1 time

* Add unit test for exhausted retries

* Update samples
This commit is contained in:
Hugo Alves
2021-08-22 04:45:04 +01:00
committed by GitHub
parent 4a9a922abf
commit 197cdac1e0
19 changed files with 769 additions and 48 deletions

View File

@@ -20,8 +20,17 @@ import feign.form.FormEncoder;
import feign.jackson.JacksonDecoder;
import feign.jackson.JacksonEncoder;
import feign.slf4j.Slf4jLogger;
import org.openapitools.client.auth.*;
import org.openapitools.client.auth.HttpBasicAuth;
import org.openapitools.client.auth.HttpBearerAuth;
import org.openapitools.client.auth.ApiKeyAuth;
import org.openapitools.client.auth.ApiErrorDecoder;
import org.openapitools.client.auth.OAuth;
import org.openapitools.client.auth.OAuth.AccessTokenListener;
import org.openapitools.client.auth.OAuthFlow;
import org.openapitools.client.auth.OauthPasswordGrant;
import org.openapitools.client.auth.OauthClientCredentialsGrant;
import feign.Retryer;
@javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen")
public class ApiClient {
@@ -41,6 +50,8 @@ public class ApiClient {
.client(new OkHttpClient())
.encoder(new FormEncoder(new JacksonEncoder(objectMapper)))
.decoder(new JacksonDecoder(objectMapper))
.errorDecoder(new ApiErrorDecoder())
.retryer(new Retryer.Default(0, 0, 2))
.logger(new Slf4jLogger());
}

View File

@@ -0,0 +1,25 @@
package org.openapitools.client.auth;
import feign.Response;
import feign.RetryableException;
import feign.codec.ErrorDecoder;
/**
* Error decoder that makes the HTTP 401 and 403 Retryable. Sometimes the 401 or 402 may indicate an expired token
* All the other HTTP status are handled by the {@link feign.codec.ErrorDecoder.Default} decoder
*/
public class ApiErrorDecoder implements ErrorDecoder {
private final Default defaultErrorDecoder = new Default();
@Override
public Exception decode(String methodKey, Response response) {
//401/403 response codes most likely indicate an expired access token, unless it happens two times in a row
Exception httpException = defaultErrorDecoder.decode(methodKey, response);
if (response.status() == 401 || response.status() == 403) {
return new RetryableException(response.status(), "Received status " + response.status() + " trying to renew access token",
response.request().httpMethod(), httpException, null, response.request());
}
return httpException;
}
}

View File

@@ -2,10 +2,10 @@ package org.openapitools.client.auth;
import com.github.scribejava.core.model.OAuth2AccessToken;
import com.github.scribejava.core.oauth.OAuth20Service;
import feign.Request.HttpMethod;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import feign.RetryableException;
import java.util.Collection;
@javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen")
public abstract class OAuth implements RequestInterceptor {
@@ -34,25 +34,27 @@ public abstract class OAuth implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// If the request already have an authorization (eg. Basic auth), do nothing
if (template.headers().containsKey("Authorization")) {
if (requestContainsNonOauthAuthorization(template)) {
return;
}
// If first time, get the token
if (expirationTimeMillis == null || System.currentTimeMillis() >= expirationTimeMillis) {
updateAccessToken(template);
}
if (getAccessToken() != null) {
template.header("Authorization", "Bearer " + getAccessToken());
String accessToken = getAccessToken();
if (accessToken != null) {
template.header("Authorization", "Bearer " + accessToken);
}
}
private synchronized void updateAccessToken(RequestTemplate template) {
OAuth2AccessToken accessTokenResponse;
try {
accessTokenResponse = getOAuth2AccessToken();
} catch (Exception e) {
throw new RetryableException(0, e.getMessage(), HttpMethod.POST, e, null, template.request());
private boolean requestContainsNonOauthAuthorization(RequestTemplate template) {
Collection<String> authorizations = template.headers().get("Authorization");
if (authorizations == null) {
return false;
}
return !authorizations.stream()
.anyMatch(authHeader -> !authHeader.equalsIgnoreCase("Bearer"));
}
private synchronized void updateAccessToken() {
OAuth2AccessToken accessTokenResponse;
accessTokenResponse = getOAuth2AccessToken();
if (accessTokenResponse != null && accessTokenResponse.getAccessToken() != null) {
setAccessToken(accessTokenResponse.getAccessToken(), accessTokenResponse.getExpiresIn());
if (accessTokenListener != null) {
@@ -70,9 +72,18 @@ public abstract class OAuth implements RequestInterceptor {
}
public synchronized String getAccessToken() {
// If first time, get the token
if (expirationTimeMillis == null || System.currentTimeMillis() >= expirationTimeMillis) {
updateAccessToken();
}
return accessToken;
}
/**
* Manually sets the access token
* @param accessToken The access token
* @param expiresIn Seconds until the token expires
*/
public synchronized void setAccessToken(String accessToken, Integer expiresIn) {
this.accessToken = accessToken;
this.expirationTimeMillis = expiresIn == null ? null : System.currentTimeMillis() + expiresIn * MILLIS_PER_SECOND;

View File

@@ -0,0 +1,110 @@
package org.openapitools.client.api;
import com.github.tomakehurst.wiremock.WireMockServer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.openapitools.client.ApiClient;
import org.openapitools.client.auth.ApiKeyAuth;
import java.util.Map;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
public class ApiKeyAuthTest {
private static StoreApi api;
private static WireMockServer wm = new WireMockServer(options().dynamicPort());
@BeforeAll
static void setup() {
wm.start();
}
@AfterAll
static void shutdown() {
wm.shutdown();
}
@Test
void keyInQueryParameter() {
ApiClient apiClient = new ApiClient();
apiClient.setBasePath(wm.baseUrl());
ApiKeyAuth apiKeyAuth = new ApiKeyAuth("query", "api_key");
apiClient.addAuthorization("api_key", apiKeyAuth);
apiClient.setApiKey("MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3");
api = apiClient.buildClient(StoreApi.class);
wm.stubFor(get(urlPathEqualTo("/store/inventory"))
.withQueryParam("api_key", equalTo("MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3"))
.withHeader("Accept", equalTo("application/json"))
.willReturn(ok("{\n" +
" \"prop1\": 1,\n" +
" \"prop2\": 2\n" +
"}")));
Map<String, Integer> inventory = api.getInventory();
assertThat(inventory.keySet().size(), is(2));
assertThat(inventory.get("prop1"), is(1));
assertThat(inventory.get("prop2"), is(2));
}
@Test
void keyInHeader() {
ApiClient apiClient = new ApiClient();
apiClient.setBasePath(wm.baseUrl());
ApiKeyAuth apiKeyAuth = new ApiKeyAuth("header", "api_key");
apiClient.addAuthorization("api_key", apiKeyAuth);
apiClient.setApiKey("MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3");
api = apiClient.buildClient(StoreApi.class);
wm.stubFor(get(urlEqualTo("/store/inventory"))
.withHeader("Accept", equalTo("application/json"))
.withHeader("api_key", equalTo("MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3"))
.willReturn(ok("{\n" +
" \"prop1\": 1,\n" +
" \"prop2\": 2\n" +
"}")));
Map<String, Integer> inventory = api.getInventory();
assertThat(inventory.keySet().size(), is(2));
assertThat(inventory.get("prop1"), is(1));
assertThat(inventory.get("prop2"), is(2));
}
@Test
void keyInCookie() {
ApiClient apiClient = new ApiClient();
apiClient.setBasePath(wm.baseUrl());
ApiKeyAuth apiKeyAuth = new ApiKeyAuth("cookie", "api_key");
apiClient.addAuthorization("api_key", apiKeyAuth);
apiClient.setApiKey("MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3");
api = apiClient.buildClient(StoreApi.class);
wm.stubFor(get(urlEqualTo("/store/inventory"))
.withHeader("Accept", equalTo("application/json"))
.withCookie("api_key", equalTo("MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3"))
.willReturn(ok("{\n" +
" \"prop1\": 1,\n" +
" \"prop2\": 2\n" +
"}")));
Map<String, Integer> inventory = api.getInventory();
assertThat(inventory.keySet().size(), is(2));
assertThat(inventory.get("prop1"), is(1));
assertThat(inventory.get("prop2"), is(2));
}
}

View File

@@ -0,0 +1,58 @@
package org.openapitools.client.api;
import com.github.tomakehurst.wiremock.WireMockServer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.openapitools.client.ApiClient;
import org.openapitools.client.auth.HttpBasicAuth;
import java.util.Map;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
public class HttpBasicAuthTest {
private static StoreApi api;
private static WireMockServer wm = new WireMockServer(options().dynamicPort());
@BeforeAll
static void setup() {
wm.start();
}
@AfterAll
static void shutdown() {
wm.shutdown();
}
@Test
void httpBasicAuth() {
ApiClient apiClient = new ApiClient();
apiClient.setBasePath(wm.baseUrl());
HttpBasicAuth httpBasicAuth = new HttpBasicAuth();
apiClient.addAuthorization("basic", httpBasicAuth);
apiClient.setCredentials("username", "password");
api = apiClient.buildClient(StoreApi.class);
wm.stubFor(get(urlPathEqualTo("/store/inventory"))
.withHeader("Authorization", equalTo("Basic dXNlcm5hbWU6cGFzc3dvcmQ="))
.withHeader("Accept", equalTo("application/json"))
.willReturn(ok("{\n" +
" \"prop1\": 1,\n" +
" \"prop2\": 2\n" +
"}")));
Map<String, Integer> inventory = api.getInventory();
assertThat(inventory.keySet().size(), is(2));
assertThat(inventory.get("prop1"), is(1));
assertThat(inventory.get("prop2"), is(2));
}
}

View File

@@ -0,0 +1,58 @@
package org.openapitools.client.api;
import com.github.tomakehurst.wiremock.WireMockServer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.openapitools.client.ApiClient;
import org.openapitools.client.auth.HttpBearerAuth;
import java.util.Map;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
public class HttpBearerAuthTest {
private static StoreApi api;
private static WireMockServer wm = new WireMockServer(options().dynamicPort());
@BeforeAll
static void setup() {
wm.start();
}
@AfterAll
static void shutdown() {
wm.shutdown();
}
@Test
void httpBearer() {
ApiClient apiClient = new ApiClient();
apiClient.setBasePath(wm.baseUrl());
HttpBearerAuth httpBearerAuth = new HttpBearerAuth("Bearer");
apiClient.addAuthorization("bearer", httpBearerAuth);
apiClient.setBearerToken("MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3");
api = apiClient.buildClient(StoreApi.class);
wm.stubFor(get(urlPathEqualTo("/store/inventory"))
.withHeader("Authorization", equalTo("Bearer MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3"))
.withHeader("Accept", equalTo("application/json"))
.willReturn(ok("{\n" +
" \"prop1\": 1,\n" +
" \"prop2\": 2\n" +
"}")));
Map<String, Integer> inventory = api.getInventory();
assertThat(inventory.keySet().size(), is(2));
assertThat(inventory.get("prop1"), is(1));
assertThat(inventory.get("prop2"), is(2));
}
}

View File

@@ -0,0 +1,72 @@
package org.openapitools.client.api;
import com.github.tomakehurst.wiremock.WireMockServer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.openapitools.client.ApiClient;
import org.openapitools.client.auth.OauthClientCredentialsGrant;
import java.util.Map;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
public class OauthClientCredentialsGrantTest {
private static StoreApi api;
private static WireMockServer wm = new WireMockServer(options().dynamicPort());
@BeforeAll
static void setup() {
wm.start();
ApiClient apiClient = new ApiClient();
apiClient.setBasePath(wm.baseUrl());
OauthClientCredentialsGrant oauthClientCredentialsGrant = new OauthClientCredentialsGrant(wm.baseUrl() + "/auth", wm.baseUrl() + "/token", "read");
oauthClientCredentialsGrant.configure("client_id", "client_secret");
apiClient.addAuthorization("oauth", oauthClientCredentialsGrant);
api = apiClient.buildClient(StoreApi.class);
}
@AfterAll
static void shutdown() {
wm.shutdown();
}
@Test
void oauthClientCredentialsTest() {
wm.stubFor(post(urlEqualTo("/token"))
.withRequestBody(equalTo("client_id=client_id&client_secret=client_secret&scope=read&grant_type=client_credentials"))
.willReturn(ok("{\n" +
" \"access_token\":\"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3\",\n" +
" \"token_type\":\"bearer\",\n" +
" \"expires_in\":3600,\n" +
" \"refresh_token\":\"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk\",\n" +
" \"scope\":\"read\"\n" +
"}")));
wm.stubFor(get(urlEqualTo("/store/inventory"))
.withHeader("Accept", equalTo("application/json"))
.withHeader("Authorization", equalTo("Bearer MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3"))
.willReturn(ok("{\n" +
" \"prop1\": 1,\n" +
" \"prop2\": 2\n" +
"}")));
Map<String, Integer> inventory = api.getInventory();
assertThat(inventory.keySet().size(), is(2));
assertThat(inventory.get("prop1"), is(1));
assertThat(inventory.get("prop2"), is(2));
wm.verify(exactly(1), getRequestedFor(urlEqualTo("/store/inventory")));
wm.verify(exactly(1), postRequestedFor(urlEqualTo("/token")));
assertThat(wm.getAllServeEvents().size(), is(2));
}
}

View File

@@ -0,0 +1,71 @@
package org.openapitools.client.api;
import com.github.tomakehurst.wiremock.WireMockServer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.openapitools.client.ApiClient;
import org.openapitools.client.auth.OauthPasswordGrant;
import java.util.Map;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
public class OauthPasswordGrantTest {
private static StoreApi api;
private static WireMockServer wm = new WireMockServer(options().dynamicPort());
@BeforeAll
static void setup() {
wm.start();
ApiClient apiClient = new ApiClient();
apiClient.setBasePath(wm.baseUrl());
OauthPasswordGrant oauthPasswordGrant = new OauthPasswordGrant(wm.baseUrl() + "/token", "read");
oauthPasswordGrant.configure("username", "password", "client_id", "client_secret");
apiClient.addAuthorization("oauth", oauthPasswordGrant);
api = apiClient.buildClient(StoreApi.class);
}
@AfterAll
static void shutdown() {
wm.shutdown();
}
@Test
void oauthPasswordGrant() {
wm.stubFor(post(urlEqualTo("/token"))
.withRequestBody(equalTo("username=username&password=password&scope=read&grant_type=password&client_id=client_id&client_secret=client_secret"))
.willReturn(ok("{\n" +
" \"access_token\":\"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3\",\n" +
" \"token_type\":\"bearer\",\n" +
" \"expires_in\":3600,\n" +
" \"refresh_token\":\"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk\",\n" +
" \"scope\":\"read\"\n" +
"}")));
wm.stubFor(get(urlEqualTo("/store/inventory"))
.withHeader("Accept", equalTo("application/json"))
.withHeader("Authorization", equalTo("Bearer MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3"))
.willReturn(ok("{\n" +
" \"prop1\": 1,\n" +
" \"prop2\": 2\n" +
"}")));
Map<String, Integer> inventory = api.getInventory();
assertThat(inventory.keySet().size(), is(2));
assertThat(inventory.get("prop1"), is(1));
assertThat(inventory.get("prop2"), is(2));
wm.verify(exactly(1), getRequestedFor(urlEqualTo("/store/inventory")));
wm.verify(exactly(1), postRequestedFor(urlEqualTo("/token")));
}
}

View File

@@ -0,0 +1,160 @@
package org.openapitools.client.api;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.stubbing.Scenario;
import feign.FeignException;
import feign.RetryableException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openapitools.client.ApiClient;
import org.openapitools.client.auth.OauthClientCredentialsGrant;
import java.util.Map;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class OauthRetryAuthErrorsTest {
private static StoreApi api;
private WireMockServer wm = new WireMockServer(options().dynamicPort());
@BeforeEach
void setup() {
wm.start();
ApiClient apiClient = new ApiClient();
apiClient.setBasePath(wm.baseUrl());
OauthClientCredentialsGrant oauthClientCredentialsGrant = new OauthClientCredentialsGrant(null, wm.baseUrl() + "/token", "read");
oauthClientCredentialsGrant.configure("client_id", "client_secret");
apiClient.addAuthorization("oauth", oauthClientCredentialsGrant);
api = apiClient.buildClient(StoreApi.class);
}
@AfterEach
void shutdown() {
wm.shutdown();
}
@Test
void retryableAuthenticationException() {
//First request to fetch the token returns an already expired token
//Just to mock the scenario where already have a token in memory but it expires before the request reaches the server
wm.stubFor(post(urlEqualTo("/token"))
.withRequestBody(equalTo("client_id=client_id&client_secret=client_secret&scope=read&grant_type=client_credentials"))
.inScenario("Retry token")
.whenScenarioStateIs(Scenario.STARTED)
.willReturn(ok("{\n" +
" \"access_token\":\"EXPIRED_TOKEN\",\n" +
" \"token_type\":\"bearer\",\n" +
" \"expires_in\":0,\n" +
" \"refresh_token\":\"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk\",\n" +
" \"scope\":\"read\"\n" +
"}")));
//This token request will be triggered by the RetryableException
wm.stubFor(post(urlEqualTo("/token"))
.withRequestBody(equalTo("client_id=client_id&client_secret=client_secret&scope=read&grant_type=client_credentials"))
.inScenario("Retry token")
.whenScenarioStateIs("After fetching new token")
.willReturn(ok("{\n" +
" \"access_token\":\"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3\",\n" +
" \"token_type\":\"bearer\",\n" +
" \"expires_in\":3600,\n" +
" \"refresh_token\":\"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk\",\n" +
" \"scope\":\"read\"\n" +
"}")));
//First request will fail with a 401
//Simulates a token that expired before reaching the server
wm.stubFor(get(urlEqualTo("/store/inventory"))
.inScenario("Retry token")
.whenScenarioStateIs(Scenario.STARTED)
.willSetStateTo("After fetching new token")
.withHeader("Accept", equalTo("application/json"))
.withHeader("Authorization", equalTo("Bearer EXPIRED_TOKEN"))
.willReturn(aResponse().withStatus(401)));
//The second request sends a newly generated token
wm.stubFor(get(urlEqualTo("/store/inventory"))
.inScenario("Retry token")
.whenScenarioStateIs("After fetching new token")
.withHeader("Accept", equalTo("application/json"))
.withHeader("Authorization", equalTo("Bearer MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3"))
.willReturn(ok("{\n" +
" \"prop1\": 1,\n" +
" \"prop2\": 2\n" +
"}")));
Map<String, Integer> inventory = api.getInventory();
assertThat(inventory.keySet().size(), is(2));
assertThat(inventory.get("prop1"), is(1));
assertThat(inventory.get("prop2"), is(2));
wm.verify(exactly(2), getRequestedFor(urlEqualTo("/store/inventory")));
wm.verify(exactly(2), postRequestedFor(urlEqualTo("/token")));
}
@Test
void retryableAuthenticationExhaustedRetries() {
//First request to fetch the token returns an already expired token
//Just to mock the scenario where already have a token in memory but it expires before the request reaches the server
wm.stubFor(post(urlEqualTo("/token"))
.withRequestBody(equalTo("client_id=client_id&client_secret=client_secret&scope=read&grant_type=client_credentials"))
.inScenario("Retry token")
.whenScenarioStateIs(Scenario.STARTED)
.willReturn(ok("{\n" +
" \"access_token\":\"EXPIRED_TOKEN\",\n" +
" \"token_type\":\"bearer\",\n" +
" \"expires_in\":0,\n" +
" \"refresh_token\":\"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk\",\n" +
" \"scope\":\"read\"\n" +
"}")));
//This token request will be triggered by the RetryableException
wm.stubFor(post(urlEqualTo("/token"))
.withRequestBody(equalTo("client_id=client_id&client_secret=client_secret&scope=read&grant_type=client_credentials"))
.inScenario("Retry token")
.whenScenarioStateIs("After fetching new token")
.willReturn(ok("{\n" +
" \"access_token\":\"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3\",\n" +
" \"token_type\":\"bearer\",\n" +
" \"expires_in\":3600,\n" +
" \"refresh_token\":\"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk\",\n" +
" \"scope\":\"read\"\n" +
"}")));
//First request will fail with a 401
//Simulates a token that expired before reaching the server
wm.stubFor(get(urlEqualTo("/store/inventory"))
.inScenario("Retry token")
.whenScenarioStateIs(Scenario.STARTED)
.willSetStateTo("After fetching new token")
.withHeader("Accept", equalTo("application/json"))
.withHeader("Authorization", equalTo("Bearer EXPIRED_TOKEN"))
.willReturn(aResponse().withStatus(401)));
//Second request also fails with a 401, in this case the 401 is not related with an expired token
wm.stubFor(get(urlEqualTo("/store/inventory"))
.inScenario("Retry token")
.whenScenarioStateIs("After fetching new token")
.withHeader("Accept", equalTo("application/json"))
.withHeader("Authorization", equalTo("Bearer MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3"))
.willReturn(aResponse().withStatus(401)));
RetryableException retryableException = assertThrows(RetryableException.class, () -> api.getInventory());
assertThat(retryableException.getCause(), is(instanceOf(FeignException.Unauthorized.class)));
wm.verify(exactly(2), getRequestedFor(urlEqualTo("/store/inventory")));
wm.verify(exactly(2), postRequestedFor(urlEqualTo("/token")));
}
}

View File

@@ -0,0 +1,46 @@
package org.openapitools.client.auth;
import feign.FeignException;
import feign.Request;
import feign.Response;
import feign.RetryableException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import java.util.Collections;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
class ApiErrorDecoderTest {
private final ApiErrorDecoder apiErrorDecoder = new ApiErrorDecoder();
@Test
void decode400() {
Response response = getDummyResponse(400);
FeignException.BadRequest badRequest = (FeignException.BadRequest) apiErrorDecoder.decode("GET", response);
assertThat(badRequest.status(), is(400));
}
@ParameterizedTest
@ValueSource(ints = {401, 403})
void decodeAuthorizationErrors(Integer httpStatus) {
Response response = getDummyResponse(httpStatus);
RetryableException retryableException = (RetryableException) apiErrorDecoder.decode("GET", response);
assertThat(retryableException.status(), is(httpStatus));
assertThat(retryableException.retryAfter(), is(nullValue()));
}
private Response getDummyResponse(Integer httpStatus) {
Request request = Request.create(Request.HttpMethod.GET, "http://localhost", Collections.emptyMap(), Request.Body.empty(), null);
return Response.builder()
.status(httpStatus)
.request(request)
.build();
}
}