I'm trying to test a quarkus rest-endpoint which is secured with #RolesAllowed
...
#GET
#Path("{id}")
#Produces(MediaType.APPLICATION_OCTET_STREAM)
#RolesAllowed({ "APPLICATION_USER"})
public Response getFile(#PathParam(value = "id") String documentId, #Context UriInfo uriInfo)
...
The test case
#QuarkusTest
class DocumentResourceTest {
#Test
public void testDocumentEndpoint() {
String documentId = "someId";
given()
.when().get("/documents/" + documentId)
.then()
.statusCode(200);
}
}
How can i mock an authenticated user with role 'APPLICATION_USER' for my test case ?
You can inject a SecurityIdentity which you can then stub out with the relevant role using Mockito:
#QuarkusTest
public class DocumentResourceTest {
#InjectMock
SecurityIdentity identity;
#BeforeEach
public void setup() {
Mockito.when(identity.hasRole("APPLICATION_USER")).thenReturn(true);
}
#Test
public void testDocumentEndpoint() {
String documentId = "someId";
given()
.when().get("/documents/" + documentId)
.then()
.statusCode(200);
}
}
You can of course move the stubbing call to your individual tests if you want to test a variety of different roles.
Note that you'll need to add the quarkus-junit5-mockito dependency for this to work:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>
A more convinient way to mock the security is to use Quarkus' security testing features:
https://quarkus.io/guides/security-testing#testing-security
Including
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-security</artifactId>
<scope>test</scope>
</dependency>
allows you to write
#Test
#TestSecurity(authorizationEnabled = false)
void someTestMethod() {
...
}
#Test
#TestSecurity(user = "testUser", roles = {"admin", "user"})
void otherTestMethod() {
...
}
In addition to the accepted answer, there is also this guide which explains how to deal with integration tests: https://quarkus.io/guides/security-oauth2#integration-testing
The first sentence there is:
If you don’t want to use a real OAuth2 authorization server for your integration tests, you can use the Properties based security extension for your test, or mock an authorization server using Wiremock.
So I think the property based security extension could also work for you: https://quarkus.io/guides/security-properties
Related
I have a AWS Cognito user pool issuing tokens to my frontend application. The frontend application then uses the tokens to talk to my backend service.
This flow is working as intended. I am validating the tokens that hit my backend service using org.springframework.security:spring-security-oauth2-resource-server:6.0.1 which is configured to point back to Cognito
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://cognito-idp.us-east-1.amazonaws.com/my_pool_endpoint
I have a simple SecurityConfig
#Configuration
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity(useAuthorizationManager = true)
public class SecurityConfig {
#Bean
SecurityWebFilterChain securityWebFilterChain(final ServerHttpSecurity http) {
return http.authorizeExchange()
.pathMatchers("/v3/api-docs/**")
.permitAll()
.anyExchange()
.authenticated()
.and()
.oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::jwt)
.build();
}
So far everything is looking good.
But how do I gather additional information from incoming tokens, for example things such as email and username are not included in the token response from Cognito. An example of a decoded token looks like:
{
"sub": "00000000000000000000",
"cognito:groups": [
"00000000000000000000"
],
"iss": "https://cognito-idp.us-east-1.amazonaws.com/00000000000000000000",
"version": 2,
"client_id": "00000000000000000000",
"origin_jti": "00000000000000000000",
"token_use": "access",
"scope": "openid profile email",
"auth_time": 1676066347,
"exp": 1676186814,
"iat": 1676143614,
"jti": "00000000000000000000",
"username": "google_00000000000000000000"
}
When I need extra information from the token, I'm calling https://my-congito-pool.auth.us-east-1.amazoncognito.com/oauth2/userInfo and passing the JWT as the Bearer token, which works and returns the information I'm looking for such as email, picture, username etc.
My question is I don't think doing this manually every time I want additional information is the 'correct' way of handling it.
Should I be using something like a UserDetailsService to perform this once and transforming the incoming JWT into my own User which holds this information?
If so, how do I do this using ReactiveSpringSecurity?
It looks like Cognito allows to enrich ID tokens, but not access tokens. That's sad, most competitors allow it and it makes spring resource-servers configurations much easier.
I can think of two solutions:
configure your resource-server with access-token introspection (with http.oauth2ResourceServer().opaqueToken()), using your /oauth2/userInfo as introspection endpoint and the JWT access-token as "opaque" token
require clients to add the ID token in a dedicated header (let's say X-ID-Token) in addition to the access token (provided in the Authorization header as usual). Then in the authentication converter, retrieve and decode this additional header and build an Authentication of your own with both access and ID tokens strings and claims
I will only develop the second solution for two reasons:
the first has the usual performance cost of token introspection (a call is made from the resource-server to the authorization-server before each request is processed)
the second permits to add any data from as many headers as we need to the Authentication instance for authentication and authorization (not only ID token as we demo here) with very little performance impact
Spoiler: here is what I got:
with valid access and ID tokens
with just the access-token
Isn't it exactly what you are looking for: an Authentication instance with the roles from the access token and the email from the ID token (or Unauthorized if authorization data is missing / invalid / incomplete)?
Detailed Security Configuration
Here is the security configuration for a reactive app. For Servlets, main lines are the same, only the tooling to statically access the request context is quite different. You can refer to this tutorial I just added to my collection for details.
#Configuration
#EnableReactiveMethodSecurity
#EnableWebFluxSecurity
public class SecurityConfig {
static final String ID_TOKEN_HEADER_NAME = "X-ID-Token";
public static Mono<ServerHttpRequest> getServerHttpRequest() {
return Mono.deferContextual(Mono::just)
.map(contextView -> contextView.get(ServerWebExchange.class).getRequest());
}
public static Mono<String> getIdTokenHeader() {
return getServerHttpRequest().map(req -> {
final var headers = req.getHeaders().getOrEmpty(ID_TOKEN_HEADER_NAME).stream()
.filter(StringUtils::hasLength).toList();
if (headers.size() == 0) {
throw new MissingIdTokenException();
}
if (headers.size() > 1) {
throw new MultiValuedIdTokenException();
}
return headers.get(0);
});
}
#Bean
SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http, ReactiveJwtDecoder jwtDecoder) {
http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(accessToken -> getIdTokenHeader()
.flatMap(idTokenString -> jwtDecoder.decode(idTokenString).doOnError(JwtException.class, e -> {
throw new InvalidIdTokenException();
}).map(idToken -> {
final var idClaims = idToken.getClaims();
#SuppressWarnings("unchecked")
final var authorities = ((List<String>) accessToken.getClaims().getOrDefault("cognito:groups",
List.of())).stream().map(SimpleGrantedAuthority::new).toList();
return new MyAuth(authorities, accessToken.getTokenValue(), idTokenString, accessToken.getClaims(),
idClaims);
})));
http.securityContextRepository(NoOpServerSecurityContextRepository.getInstance()).csrf().disable();
http.authorizeExchange().anyExchange().authenticated();
return http.build();
}
public static class MyAuth extends AbstractAuthenticationToken {
private static final long serialVersionUID = 9115947200114995708L;
// Save access and ID tokens strings just in case we need to call another
// micro-service on behalf of the user who initiated the request and as so,
// position "Authorization" and "X-ID-Token" headers
private final String accessTokenString;
private final String idTokenString;
private final Map<String, Object> accessClaims;
private final Map<String, Object> idClaims;
public MyAuth(Collection<? extends GrantedAuthority> authorities, String accessTokenString,
String idTokenString, Map<String, Object> accessClaims, Map<String, Object> idClaims) {
super(authorities);
this.accessTokenString = accessTokenString;
this.accessClaims = Collections.unmodifiableMap(accessClaims);
this.idTokenString = idTokenString;
this.idClaims = Collections.unmodifiableMap(idClaims);
// Minimal security checks: assert that issuer and subject claims are the same
// in access and ID tokens.
if (!Objects.equals(accessClaims.get(IdTokenClaimNames.ISS), idClaims.get(IdTokenClaimNames.ISS))
|| !Objects.equals(accessClaims.get(StandardClaimNames.SUB), idClaims.get(IdTokenClaimNames.SUB))) {
throw new InvalidIdTokenException();
}
// You could also make assertions on ID token audience, but this will require
// adding a custom property for expected ID tokens audience.
// You can't just check for audience equality with already validated access
// token one.
this.setAuthenticated(true);
}
#Override
public String getCredentials() {
return accessTokenString;
}
#Override
public String getPrincipal() {
return (String) accessClaims.get(StandardClaimNames.SUB);
}
public String getAccessTokenString() {
return accessTokenString;
}
public String getIdTokenString() {
return idTokenString;
}
public Map<String, Object> getAccessClaims() {
return accessClaims;
}
public Map<String, Object> getIdClaims() {
return idClaims;
}
}
#ResponseStatus(code = HttpStatus.UNAUTHORIZED, reason = ID_TOKEN_HEADER_NAME + " is missing")
static class MissingIdTokenException extends RuntimeException {
private static final long serialVersionUID = -4894061353773464761L;
}
#ResponseStatus(code = HttpStatus.UNAUTHORIZED, reason = ID_TOKEN_HEADER_NAME + " is not unique")
static class MultiValuedIdTokenException extends RuntimeException {
private static final long serialVersionUID = 1654993007508549674L;
}
#ResponseStatus(code = HttpStatus.UNAUTHORIZED, reason = ID_TOKEN_HEADER_NAME + " is not valid")
static class InvalidIdTokenException extends RuntimeException {
private static final long serialVersionUID = -6233252290377524340L;
}
}
Now, each time an authorization succeeds (isAuthenticated() is true), you'll have a MyAuth instance in the security context and it contains both the access and ID tokens claims!
Sample Controller
#RestController
public class GreetingController {
#GetMapping("/greet")
#PreAuthorize("isAuthenticated()")
Mono<String> greet(MyAuth auth) {
return Mono.just("Hello %s! You are granted with %s".formatted(
auth.getIdClaims().get("email"),
auth.getAuthorities()));
}
}
You may also build your #PreAuthorize expressions based on it. Something like:
#RequiredArgsConstructor
#RestController
#RequestMapping("/something/protected")
#PreAuthorize("isAuthenticated()")
public class ProtectedResourceController {
private final SomeResourceRepository resourceRepo;
#GetMapping("/{resourceId}")
#PreAuthorize("#auth.idClaims['email'] == #resource.email")
ResourceDto getProtectedResource(MyAuth auth, #RequestParam("resourceId") SomeResource resource) {
...
}
}
EDIT: Code reusability & spring-addons starters
I maintain wrappers around spring-boot-starter-oauth2-resource-server. It is very thin and opensource. If you don't want to use it, you should have a look at how it is done to get inspiration from it:
inspect resources to find out what it takes to build your own spring-boot starters
inspect beans to pick ideas for creating your own configurable ones
browse to dependencies like OpenidClaimSet and OAuthentication which could be of inspiration
Here is what the sample above becomes with "my" starter for reactive resource-servers with JWT decoders:
#Configuration
#EnableReactiveMethodSecurity
#EnableWebFluxSecurity
public class SecurityConfig {
static final String ID_TOKEN_HEADER_NAME = "X-ID-Token";
#Bean
OAuth2AuthenticationFactory authenticationFactory(
Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> authoritiesConverter,
ReactiveJwtDecoder jwtDecoder) {
return (accessBearerString, accessClaims) -> ServerHttpRequestSupport.getUniqueHeader(ID_TOKEN_HEADER_NAME)
.flatMap(idTokenString -> jwtDecoder.decode(idTokenString).doOnError(JwtException.class, e -> {
throw new InvalidHeaderException(ID_TOKEN_HEADER_NAME);
}).map(idToken -> new MyAuth(
authoritiesConverter.convert(accessClaims),
accessBearerString,
new OpenidClaimSet(accessClaims),
idTokenString,
new OpenidClaimSet(idToken.getClaims()))));
}
#Data
#EqualsAndHashCode(callSuper = true)
public static class MyAuth extends OAuthentication<OpenidClaimSet> {
private static final long serialVersionUID = 1734079415899000362L;
private final String idTokenString;
private final OpenidClaimSet idClaims;
public MyAuth(Collection<? extends GrantedAuthority> authorities, String accessTokenString,
OpenidClaimSet accessClaims, String idTokenString, OpenidClaimSet idClaims) {
super(accessClaims, authorities, accessTokenString);
this.idTokenString = idTokenString;
this.idClaims = idClaims;
}
}
}
Update #Controller (pay attention to the direct accessor to email claim):
#RestController
public class GreetingController {
#GetMapping("/greet")
#PreAuthorize("isAuthenticated()")
Mono<String> greet(MyAuth auth) {
return Mono.just("Hello %s! You are granted with %s".formatted(
auth.getIdClaims().getEmail(),
auth.getAuthorities()));
}
}
This are the configuration properties (with different claims used as authorities source depending on the authorization-server configured in the profile):
server:
error.include-message: always
spring:
lifecycle.timeout-per-shutdown-phase: 30s
security.oauth2.resourceserver.jwt.issuer-uri: https://localhost:8443/realms/master
com:
c4-soft:
springaddons:
security:
issuers:
- location: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}
authorities:
claims:
- realm_access.roles
- resource_access.spring-addons-public.roles
- resource_access.spring-addons-confidential.roles
caze: upper
prefix: ROLE_
cors:
- path: /greet
---
spring.config.activate.on-profile: cognito
spring.security.oauth2.resourceserver.jwt.issuer-uri: https://cognito-idp.us-west-2.amazonaws.com/us-west-2_RzhmgLwjl
com.c4-soft.springaddons.security.issuers:
- location: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}
authorities:
claims:
- cognito:groups
caze: upper
prefix: ROLE_
---
spring.config.activate.on-profile: auth0
com.c4-soft.springaddons.security.issuers:
- location: https://dev-ch4mpy.eu.auth0.com/
authorities:
claims:
- roles
- permissions
caze: upper
prefix: ROLE_
Unit-tests with mocked identity for the #Controller above can be as simple as:
#WebFluxTest(controllers = GreetingController.class)
#AutoConfigureAddonsWebSecurity
#Import(SecurityConfig.class)
class GreetingControllerTest {
#Autowired
WebTestClientSupport api;
#Test
#WithMyAuth(authorities = { "AUTHOR" }, idClaims = #OpenIdClaims(email = "ch4mp#c4-soft.com"))
void givenUserIsAuthenticated_whenGreet_thenOk() throws Exception {
api.get("/greet").expectStatus().isOk()
.expectBody(String.class).isEqualTo("Hello ch4mp#c4-soft.com! You are granted with [AUTHOR]");
}
#Test
void givenRequestIsAnonymous_whenGreet_thenUnauthorized() throws Exception {
api.get("/greet").expectStatus().isUnauthorized();
}
}
With annotation definition (to build the custom Authentication implementation and set it in the security context):
#Target({ ElementType.METHOD, ElementType.TYPE })
#Retention(RetentionPolicy.RUNTIME)
#Inherited
#Documented
#WithSecurityContext(factory = WithMyAuth.MyAuthFactory.class)
public #interface WithMyAuth {
#AliasFor("authorities")
String[] value() default {};
#AliasFor("value")
String[] authorities() default {};
OpenIdClaims accessClaims() default #OpenIdClaims();
OpenIdClaims idClaims() default #OpenIdClaims();
String accessTokenString() default "machin.truc.chose";
String idTokenString() default "machin.bidule.chose";
#AliasFor(annotation = WithSecurityContext.class)
TestExecutionEvent setupBefore()
default TestExecutionEvent.TEST_METHOD;
#Target({ ElementType.METHOD, ElementType.TYPE })
#Retention(RetentionPolicy.RUNTIME)
public static #interface Proxy {
String onBehalfOf();
String[] can() default {};
}
public static final class MyAuthFactory extends AbstractAnnotatedAuthenticationBuilder<WithMyAuth, MyAuth> {
#Override
public MyAuth authentication(WithMyAuth annotation) {
final var accessClaims = new OpenidClaimSet(super.claims(annotation.accessClaims()));
final var idClaims = new OpenidClaimSet(super.claims(annotation.idClaims()));
return new MyAuth(super.authorities(annotation.authorities()), annotation.accessTokenString(), accessClaims, annotation.idTokenString(), idClaims);
}
}
}
And this is the pom body:
<properties>
<java.version>17</java.version>
<spring-addons.version>6.0.13</spring-addons.version>
</properties>
<dependencies>
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-webflux-jwt-resource-server</artifactId>
<version>${spring-addons.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-webflux-jwt-test</artifactId>
<version>${spring-addons.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
I am trying to test spring rest documentation for rest API for our services using spring cucumber jvm but end up with a null pointer exeception when I try to execute the scenario, as the framework is not able to intialize the Junit context.
Error Message:
java.lang.NullPointerException at
org.springframework.restdocs.ManualRestDocumentation.beforeOperation(ManualRestDocumentation.java:90) at
org.springframework.restdocs.JUnitRestDocumentation.beforeOperation(JUnitRestDocumentation.java:76)
Code:
private AppProperties props;
#Before("#rest") public void beforeScenario() {
JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( "target/generated-snippets" );
System.out.println( "jUnitRestDocumentation " +restDocumentation );
spec = new RequestSpecBuilder().addFilter( documentationConfiguration( restDocumentation ) ).build();
System.out.println( "\n spec init .. " +restDocumentation );
}
Step definition code:
#Given("^create a rest document for VHR API$")
public void create_a_rest_document_for_VHR_API() throws Throwable {
estAssured.given( spec )
.accept( "application/json" )
.filter( document( "vhrdocument" ) ) .when()
.get( props.getVhrrequesturl() + "/vhrData/{vehicleID}", "5VW4T7AU0FM029999" ) .then().log().all();
}
You aren't using JUnitRestDocumentation as it's intended to be used. It's designed to be used as a JUnit rule which means it should be a public field annotated with #Rule:
#Rule
public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();
Being a rule means that JUnit will automatically call restDocumentation for each test, allowing Spring REST Docs to set up and tear down the test-specific context. The NullPointerException is occurring because restDocumentation hasn't been called in this way and, therefore, the context hasn't been set up.
You haven't described how you're using Cucumber, but if you're using it's JUnit runner you should be able to fix the problem by declaring restDocumentation as a #Rule-annotated field as shown above. If you're not using its JUnit runner, you may need to use Spring REST Docs' ManualRestDocumentation instead. The Spring REST Docs reference documentation contains a section that describes how to set up your tests when you're not using JUnit.
I had the same problem because I had multiple test class inheriting the class, in which I declared the JUnitRestDocumentation instance. My mistake was that I declared the rule using the #Rule annotation. I should have used #ClassRule and declared the instance as static.
#ClassRule
public static JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();
it happened with test SpockFramework, and i added to pom.xml:
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-junit4</artifactId>
<scope>test</scope>
</dependency>
I had the same symptoms when migrating from RestAssured 2.x to RestAssured 3.1.1.
The codebase had a way to setup RestAssured in order to avoid repetitive ceremony for every tests :
#Rule
public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();
#Before
public void configure_rest_assured() {
RestAssured.port = springServerPort;
RestAssured.config = config().objectMapperConfig(
objectMapperConfig().jackson2ObjectMapperFactory((cls, charset) -> customObjectMapper)
)
...;
RestAssured.requestSpecification = new RequestSpecBuilder()
.addRequestSpecification(documentationConfiguration(docRule, ...))
...
.build();
}
This was working well, until I migrated to 3.x. The issue was that new RequestSpecBuilder() will append itself to the default static RestAssured.requestSpecification.
The first test passed, but when it finished the rule was disposed (the after part), when the second test started to ran, the Before method was chaining
the specification created for the first test (referencing the disposed rule used by the first test method)
the specification created for the second test (referencing the active rule for second test method)
And so on as new tests are ran.
But when the second test is run RestAssured invoke specification in order, e.g. the number 1, but since it was referencing a disposed rule (the beforeOperation was executed on a null context)
To fix that the code had to clear the previous specifications :
#Before
public void configure_rest_assured() {
RestAssured.port = springServerPort;
RestAssured.config = config().objectMapperConfig(
objectMapperConfig().jackson2ObjectMapperFactory((cls, charset) -> customObjectMapper)
)
...;
RestAssured.requestSpecification = null; // avoid the builder to acquire previous specs.
RestAssured.requestSpecification = new RequestSpecBuilder()
.addRequestSpecification(documentationConfiguration(docRule, ...))
...
.build();
}
For using cucumber-java-8 with spring rest docs and spring-security the following worked for me.
This is combining #AndyWilkison's answer from above but using the cucumber hooks instead of junit rules.
public class StepDefs implements En {
#Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
private ManualRestDocumentation restDocumentation = new ManualRestDocumentation();
public StepDefs() {
BeforeStep((Scenario scenario) -> {
restDocumentation.beforeTest(AuthenticationStepDefs.class, scenario.getName());
mockMvc = MockMvcBuilders.webAppContextSetup(context).apply(springSecurity()).apply(documentationConfiguration(restDocumentation)).build();
});
AfterStep((Scenario scenario) -> {
restDocumentation.afterTest();
});
When("create a rest document for VHR API", () -> {
MvcResult result = mockMvc.perform(/*
your normal call here
*/).
.andDo(document("documentation")).
.andReturn();
}
}
}
I have a restful service calling an external service using Spring Cloud Feign client
#FeignClient(name = "external-service", configuration = FeignClientConfig.class)
public interface ServiceClient {
#RequestMapping(value = "/test/payments", method = RequestMethod.POST)
public void addPayment(#Valid #RequestBody AddPaymentRequest addPaymentRequest);
#RequestMapping(value = "/test/payments/{paymentId}", method = RequestMethod.PUT)
public ChangePaymentStatusResponse updatePaymentStatus(#PathVariable("paymentId") String paymentId,
#Valid #RequestBody PaymentStatusUpdateRequest paymentStatusUpdateRequest);
}
I noticed the following failure 3-4 times in the last 3 months in my log file:
json.ERROR_RESPONSE_BODY:Connection refused executing POST
http://external-service/external/payments json.message:Send Payment
Add Payment Failure For other reason: {ERROR_RESPONSE_BODY=Connection
refused executing POST http://external-service/external/payments,
EVENT=ADD_PAYMENT_FAILURE, TRANSACTION_ID=XXXXXXX} {}
json.EVENT:ADD_PAYMENT_FAILURE
json.stack_trace:feign.RetryableException: Connection refused
executing POST http://external-service/external/payments at
feign.FeignException.errorExecuting(FeignException.java:67) at
feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:104)
at
feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:76)
at
feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:103)
Is it possible to add Spring Retry on a Feign client.
What I wanted to annotate the addPayment operation with
#Retryable(value = {feign.RetryableException.class }, maxAttempts = 3, backoff = #Backoff(delay = 2000, multiplier=2))
But this is not possible, what other options do I have?
You can add a Retryer in the FeignClientConfig
#Configuration
public class FeignClientConfig {
#Bean
public Retryer retryer() {
return new Custom();
}
}
class Custom implements Retryer {
private final int maxAttempts;
private final long backoff;
int attempt;
public Custom() {
this(2000, 3);
}
public Custom(long backoff, int maxAttempts) {
this.backoff = backoff;
this.maxAttempts = maxAttempts;
this.attempt = 1;
}
public void continueOrPropagate(RetryableException e) {
if (attempt++ >= maxAttempts) {
throw e;
}
try {
Thread.sleep(backoff);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
}
#Override
public Retryer clone() {
return new Custom(backoff, maxAttempts);
}
}
Updated with sample Retryer example config based on the Retryer.Default.
If you are using ribbon you can set properties, you can use below properties for retry:
myapp.ribbon.MaxAutoRetries=5
myapp.ribbon.MaxAutoRetriesNextServer=5
myapp.ribbon.OkToRetryOnAllOperations=true
Note: "myapp" is your service id.
Checkout this Github implementation for working example
Just new a contructor Default
#Configuration
public class FeignClientConfig {
#Bean
public Retryer retryer() {
return new Retryer.Default(100, 2000, 3);
}
}
Adding this if it can help someone. I was getting connection reset using feign, as some unknown process was running on that port.
Try changing the port. Refer this to find the process running on a port
I prepared a blog post about using Spring Retry with Feign Client methods. You may consider checking the Post. All steps have been explained in the post.
This is my config. Test OK in spring boot 2.2.0.RELEASE
spring cloud Hoxton.M3.
feign.hystrix.enabled=true
MY-SPRING-API.ribbon.MaxAutoRetries=2
MY-SPRING-API.ribbon.MaxAutoRetriesNextServer=2
MY-SPRING-API.ribbon.OkToRetryOnAllOperations=true
MY-SPRING-API.ribbon.retryableStatusCodes=404,500
feign.client.config.PythonPatentClient.connectTimeout=500
feign.client.config.PythonPatentClient.readTimeout=500
hystrix.command.PythonPatentClient#timeTest(String).execution.isolation.thread.timeoutInMilliseconds=5000
java code is :
#FeignClient(name = "MY-SPRING-API",configuration = {PythonPatentConfig.class},fallbackFactory = FallBack.class)
public interface PythonPatentClient
#RequestLine("GET /test?q={q}")
void timeTest(#Param("appNo") String q);
Controller is :
#RequestMapping(value = "/test",method = {RequestMethod.POST,RequestMethod.GET})
public Object test() throws InterruptedException {
log.info("========important print enter test========");
TimeUnit.SECONDS.sleep(10L);
pom.xml additon add:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
optional:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
#EnableRetry
#SpringBootApplication
public class ApiApplication
this is document :
https://docs.spring.io/spring-cloud-netflix/docs/2.2.10.RELEASE/reference/html/#retrying-failed-requests
https://github.com/spring-projects/spring-retry
https://github.com/spring-cloud/spring-cloud-netflix/
I resolved that by creating a wrapper on top of ServiceClient
#Configuration
public class ServiceClient {
#Autowired
ServiceFeignClient serviceFeignClient;
#Retryable(value = { ClientReprocessException.class }, maxAttemptsExpression = "#{${retryMaxAttempts}}", backoff = #Backoff(delayExpression = "#{${retryDelayTime}}"))
public void addPayment( AddPaymentRequest addPaymentRequest){
return serviceFeignClient.addPayment(addPaymentRequest);
}
}
I want to be able to use a test properties files and only override a few properties. Having to override every single property will get ugly fast.
This is the code I am using to test my ability to mock properties and use existing properties in a test case
#RunWith(SpringRunner.class)
#SpringBootTest(classes = MyApp.class)
#TestPropertySource(
locations = { "classpath:myapp-test.properties" },
properties = { "test.key = testValue" })
public class EnvironmentMockedPropertiesTest {
#Autowired private Environment env;
// #MockBean private Environment env;
#Test public void testExistingProperty() {
// some.property=someValue
final String keyActual = "some.property";
final String expected = "someValue";
final String actual = env.getProperty(keyActual);
assertEquals(expected, actual);
}
#Test public void testMockedProperty() {
final String keyMocked = "mocked.test.key";
final String expected = "mockedTestValue";
when(env.getProperty(keyMocked)).thenReturn(expected);
final String actual = env.getProperty(keyMocked);
assertEquals(expected, actual);
}
#Test public void testOverriddenProperty() {
final String expected = "testValue";
final String actual = env.getProperty("test.key");
assertEquals(expected, actual);
}
}
What I find is:
#Autowired private Environment env;
testExistingProperty() and testOverriddenProperty() pass
testMockedProperty() fails
#MockBean private Environment env;
testMockedProperty() passes
testExistingProperty() and testOverriddenProperty() fail
Is there a way to achieve what I am aiming for?
Dependencies:
<spring.boot.version>1.4.3.RELEASE</spring.boot.version>
...
<!-- Spring -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<!-- Starter for testing Spring Boot applications with libraries including JUnit,
Hamcrest and Mockito -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring.boot.version}</version>
</dependency>
Ok i have made this work, you need to use Mockito to accompish what you are looking for:
Maven Dependency
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.6.4</version>
</dependency>
Test Class Set up
import static org.mockito.Mockito.*;
import static org.springframework.test.util.AopTestUtils.getTargetObject;
#RunWith(SpringRunner.class)
#SpringBootTest(classes = MyApp.class)
#TestPropertySource(
locations = { "classpath:myapp-test.properties" },
properties = { "test.key = testValue" })
public class AnswerTest {
// This will be only for injecting, we will not be using this object in tests.
#Autowired
private Environment env;
// This is the reference that will be used in tests.
private Environment envSpied;
// Map of properties that you intend to mock
private Map<String, String> mockedProperties;
#PostConstruct
public void postConstruct(){
mockedProperties = new HashMap<String, String>();
mockedProperties.put("mocked.test.key_1", "mocked.test.value_1");
mockedProperties.put("mocked.test.key_2", "mocked.test.value_2");
mockedProperties.put("mocked.test.key_3", "mocked.test.value_3");
// We use the Spy feature of mockito which enabled partial mocking
envSpied = Mockito.spy((Environment) getTargetObject(env));
// We mock certain retrieval of certain properties
// based on the logic contained in the implementation of Answer class
doAnswer(new CustomAnswer()).when(envSpied).getProperty(Mockito.anyString());
}
Test case
// Testing for both mocked and real properties in same test method
#Test public void shouldReturnAdequateProperty() {
String mockedValue = envSpied.getProperty("mocked.test.key_3");
String realValue = envSpied.getProperty("test.key");
assertEquals(mockedValue, "mocked.test.value_3");
assertEquals(realValue, "testValue");
}
Implementation of Mockito's Answer interface
// Here we define what should mockito do:
// a) return mocked property if the key is a mock
// b) invoke real method on Environment otherwise
private class CustomAnswer implements Answer<String>{
#Override
public String answer(InvocationOnMock invocationOnMock) throws Throwable {
Object[] arguments = invocationOnMock.getArguments();
String parameterKey = (String) arguments[0];
String mockedValue = mockedProperties.get(parameterKey);
if(mockedValue != null){
return mockedValue;
}
return (String) invocationOnMock.callRealMethod();
}
}
}
Try it out, and let me know if all is clear here.
I wish to test the following scenarios:
Set the hystrix.command.default.execution.isolation.thread.timeoutInMillisecond value to a low value, and see how my application behaves.
Check my fallback method is called using Unit test.
Please can someone provide me with link to samples.
A real usage can be found bellow. The key to enable Hystrix in the test class are these two annotations:
#EnableCircuitBreaker
#EnableAspectJAutoProxy
class ClipboardService {
#HystrixCommand(fallbackMethod = "getNextClipboardFallback")
public Task getNextClipboard(int numberOfTasks) {
doYourExternalSystemCallHere....
}
public Task getNextClipboardFallback(int numberOfTasks) {
return null;
}
}
#RunWith(SpringRunner.class)
#EnableCircuitBreaker
#EnableAspectJAutoProxy
#TestPropertySource("classpath:test.properties")
#ContextConfiguration(classes = {ClipboardService.class})
public class ClipboardServiceIT {
private MockRestServiceServer mockServer;
#Autowired
private ClipboardService clipboardService;
#Before
public void setUp() {
this.mockServer = MockRestServiceServer.createServer(restTemplate);
}
#Test
public void testGetNextClipboardWithBadRequest() {
mockServer.expect(ExpectedCount.once(), requestTo("https://getDocument.com?task=1")).andExpect(method(HttpMethod.GET))
.andRespond(MockRestResponseCreators.withStatus(HttpStatus.BAD_REQUEST));
Task nextClipboard = clipboardService.getNextClipboard(1);
assertNull(nextClipboard); // this should be answered by your fallBack method
}
}
Fore open the circuit in your unit test case just before you call the client. Make sure fall back is called. You can have a constant returned from fallback or add some log statements.
Reset the circuit.
#Test
public void testSendOrder_openCircuit() {
String order = null;
ServiceResponse response = null;
order = loadFile("/order.json");
// use this in case of feign hystrix
ConfigurationManager.getConfigInstance()
.setProperty("hystrix.command.default.circuitBreaker.forceOpen", "true");
// use this in case of just hystrix
System.setProperty("hystrix.command.default.circuitBreaker.forceOpen", "true");
response = client.sendOrder(order);
assertThat(response.getResultStatus()).isEqualTo("Fallback");
// DONT forget to reset
ConfigurationManager.getConfigInstance()
.setProperty("hystrix.command.default.circuitBreaker.forceOpen", "false");
// use this in case of just hystrix
System.setProperty("hystrix.command.default.circuitBreaker.forceOpen", "false");
}