How can I test a secured endpoint with Awaitility in Spring boot? - spring-boot

I'm using spring boot and I want to assert an asynchronous side effect by calling a secured endpoint with MockMvc.
I have been using Awaitility, but apparently the mocked security context is lost when executing in a different thread.
I couldn't find a way of passing the context, I tried with SecurityContextHolder.setContext() but it didn't work, I guess spring's MockMvc stores the context in a different way.
#Test
#WithMockUser(authorities = "admin", username = "user")
void shouldRunSideEffectAsync() throws Exception {
mockMvc.perform(post("/foo")).andExpect(status().isAccepted());
await()
.atMost(TIMEOUT)
.untilAsserted(() -> mockMvc.perform(get("/foo")).andExpect(status().isOk()));
}
The GET would return 404 for a while and then 200 when the async task is completed. However this will always return 403 as the MockUser info is lost.
How can I solve this?

You almost got it. Security for MockMvc is implemented by TestSecurityContextHolderPostProcessor, which uses the TestSecurityContextHolder to set/get the security context. That is just a wrapper around the SecurityContextHolder.
So you can use TestSecurityContextHolder.setContext() in the awaitility thread and it should work.

Related

Is it possible to test specific Spring REST endpoint security and avoid bootstrapping database connection?

We have a couple of Spring tests that call a secured controller endpoints. Our goal is to assure that absence of particular user roles will result into HTTP 403 status.
Our issue is that execution of those tests also bootstraps DB connection which we don't actually need.
I've already tried countless number of all kind of annotations and manual configurations to avoid initialization of DB connection but so far without luck. Can you please share example how to do that?
We use Spring Boot 2.7.
Yes, you can use #WebMvcTest, take a look at the docs. In summary, using #WebMvcTest will only bootstrap the Spring MVC components and avoid loading other application's layers. This annotation also automatically configures Spring Security for you, therefore you can test authentication/authorization rules.
Example:
#WebMvcTest(UserVehicleController.class)
class MyControllerTests {
#Autowired
private MockMvc mvc;
#MockBean
private UserVehicleService userVehicleService;
#Test
#WithMockUser(roles = "ADMIN")
void testAdminSuccess() throws Exception {
given(this.userVehicleService.getVehicleDetails("sboot"))
.willReturn(new VehicleDetails("Honda", "Civic"));
this.mvc.perform(get("/sboot/vehicle").accept(MediaType.TEXT_PLAIN))
.andExpect(status().isOk())
.andExpect(content().string("Honda Civic"));
}
#Test
#WithMockUser(roles = "USER")
void testUserForbidden() throws Exception {
this.mvc.perform(get("/sboot/vehicle").accept(MediaType.TEXT_PLAIN))
.andExpect(status().isForbidden());
}
}

Testing ControllerAdvice with AutoConfigureMockMvc and CompletableFuture

I have added a REST controller returning CompletableFutures to a project using a ControllerAdvice to translate exceptions into error DTOs.
My controller doesn’t throw the exceptions, wrapping them into failed CompletableFutures and returning these.
When running the full application and manually testing it works as expected, but in my tests the mockMvc won’t trigger the advices and always return HTTP 2xx.
Any idea why?
If you have a standalone setup of MockMvc, then you need to specify the controller advice to be used (if any) while creating the mockMvc instance as follows:
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller)
.setControllerAdvice(new YourControllerAdvice())
.build();
The reason for this is that you don't have a context here for spring to detect the controller advice.
I figured out my test was not correct (or, to put it another way.. the testing framework is not designed as I expected ;)
When testing controllers returning CompletableFutures one needs to use asyncDyspatch as in
https://github.com/spring-projects/spring-framework/blob/master/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/AsyncTests.java

Configuring Spring WebSecurityConfigurerAdapter to use exception handler

Spring Boot here. I just read this excellent Baeldung article on Spring Security and implementing basic auth with it. I'm interested in implementing it for a simple REST service (so no UI/webapp) that I need to build.
I'm particularly interested in the BasicAuthenticationEntryPoint impl. In this impl's commence override, the author:
Adds a WWW-Authenticate header to the response; and
Sets the HTTP status code on the response; and
Writes the actual response entity directly to the response; and
Sets the name of the realm
I want to follow this author's example to implement basic auth for my app, but I already have a perfectly functioning ResponseEntityExceptionHandler working for my app:
#ControllerAdvice
public class MyAppExceptionMapper extends ResponseEntityExceptionHandler {
#ExceptionHandler(IllegalArgumentException.class)
#ResponseBody
public ResponseEntity<ErrorResponse> handleIllegalArgumentExeption(IllegalArgumentException iaEx) {
return new ResponseEntity<ErrorResponse>(buildErrorResponse(iaEx,
iaEx.message,
"Please check your request and make sure it contains a valid entity/body."),
HttpStatus.BAD_REQUEST);
}
// other exceptions handled down here, etc.
// TODO: Handle Spring Security-related auth exceptions as well!
}
Is there any way to tie Spring Security and Basic Auth fails into my existing/working ResponseEntityExceptionHandler?
Ideally there's a way to tie my WebSecurityConfigurerAdapter impl into the exception handler such that failed authentication or authorization attempts throw exceptions that are then caught by my exception handler.
My motivation for doing this would be so that my exception handler is the central location for managing and configuring the HTTP response when any exception occurs, whether its auth-related or not.
Is this possible to do, if so, how? And if it is possible, would I need to still add that WWW-Authenticate to the response in my exception handler (why/why not)? Thanks in advance!
I don't think that this is possible. Spring security is applied as a ServletFilter, way before the request ever reaches any #Controller annotated class - thus exceptions thrown by Spring Security cannot be caught by an exception handler (annotated with #ControllerAdvice).
Having had a similar problem, I ended up using a custom org.springframework.security.web.AuthenticationEntryPoint which sends an error, which in turn is forwarded to a custom org.springframework.boot.autoconfigure.web.ErrorController

How can I share a MockMvc session with HtmlUnit?

I am using Spring Test MVC HtmlUnit with Geb to drive functional tests for my Spring MVC application. I would like to check that some session variables are saved properly during an interaction. I tried creating a test controller to return those variables, but HtmlUnit and mvc.perform() are using different sessions. Is there a way to use a single shared session between them?
driver setup:
MockMvc mvc = MockMvcBuilders.webAppContextSetup(ctx)
.apply(SecurityMockMvcConfigurers.springSecurity())
.build()
HtmlUnitDriver driver = MockMvcHtmlUnitDriverBuilder.mockMvcSetup(mvc).javascriptEnabled(true).build()
test:
when:
via ProtectedPage
then:
// this uses session A
at LoginPage
and:
// this uses session B
println mvc.perform(get('/test/sessionAttributes')).andReturn().response.contentAsString
Summary
Can I ask why you are trying to do some of your work in MockMvc and some of it in HtmlUnit? It really isn't designed to be used this way. Instead, I would recommend interacting with HtmlUnit to consume the session (as your browser would) and verifying those results.
The reason this doesn't work is that MockMvc.perform works in isolation. The HtmlUnit integration bridges the MockMvc.perform invocations to ensure they work as you would expect in a browser (i.e. tracking sessions). However, the logic to bridge the the MockMvc.perform invocations is encapsulated.
MockMvc Behavior
MockMvc requests work in isolation and will by default use a new session on every request. For example, the following two requests operate on distinct sessions:
// this uses session A
mvc.perform(get("/test"))
// this uses session B
mvc.perform(get("/test"))
In order to reuse the session, you must obtain the session from the first MockMvc.perform invocation and set it on the second the MockMvc.perform invocation. For example:
MvcResult mvcResult = mvc
.perform(get("/a"))
.andReturn();
// reuse the previous session
MockHttpSession session = (MockHttpSession) mvcResult
.getRequest().getSession();
mvc.perform(get("/b").session(session));
The HtmlUnit support keeps track of the sessions in MockMvcWebConnection and sets the appropriate session (similar to what you saw above) based upon the JSESSIONID cookie.
In order for you to reuse the HttpSession from the HtmlUnit support in a MockMvc request, you would need access to the original session. However, this logic is encapsulated within the HtmlUnit support and thus you cannot access it.
Workaround
I don't expect that we will expose the internals of the HtmlUnit integration. I also would not recommend mixing and matching MockMvc HtmlUnit integration with straight MockMvc usage. However, you can workaround the issue.
The first step is to create a ResultHandler that tracks the last session.
public class SessionTracking implements ResultHandler {
private MockHttpSession lastSession;
#Override
public void handle(MvcResult result) throws Exception {
lastSession = (MockHttpSession) result.getRequest().getSession(false);
}
public MockHttpSession getLastSession() {
return lastSession;
}
}
The next step is to ensure you register SessionTracking with your MockMvc instance.
SessionTracking sessions = new SessionTracking();
MockMvc mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(SecurityMockMvcConfigurers.springSecurity())
// ADD THIS
.alwaysDo(sessions)
.build();
HtmlUnitDriver driver = MockMvcHtmlUnitDriverBuilder
.mockMvcSetup(mvc)
.build();
Now if you need to make a MockMvc request, you can access the previous session using the SessionTracking object.
when:
via ProtectedPage
then:
// this uses session A
at LoginPage
and:
// this uses session A
println mvc.perform(get('/test/sessionAttributes').session(sessions.lastSession).andReturn().response.contentAsString

How to test REST in spring app with spring security

I've got spring web application with jersey rest services. However rest is secured via spring security and login process is very hard to perform from unit test code. I'd like to test rest services with whole spring security disabled. Is it even possible?
One of the advantages of annotation based web services is that you can unit-test them easily.
class WebServiceEndpoint {
#Path("/foo/{fooId}")
#POST
#Produces({ MediaType.APPLICATION_XML })
public Response doFoo(#PathParam("fooId") Integer fooId) {
/// ... web service endpoint implementation
}
}
If you're using Spring's servlet filter for security, then there shouldn't be any security-related code in the doFoo method, so you can just create a new WebServiceEndpoint class and call the method. So that's one way of 'disabling' security.
When you say the login process is 'hard', what do you mean? If you've succeeded in logging in once, then you can just reuse the same code in your other unit tests (e.g. in a #Before method).
Just test it as a pojo. Pass in whatever, return whatever, don't load an app context at all - that would be an integration test.
The ability to easily test functionality without the framework loaded is one of the key advantages of spring.
You don't say what's "hard," so I'm assuming that you've got something in your REST service, i.e. in the java method that you want to test, which requires authentication results. Spring has utilities for mocking the authentication results. For example, you can do the following in a #Before setup method:
Object principal = null; // fix this
Object credentials = null; // fix this
Authentication auth = new org.springframework.security.authentication.TestingAuthenticationToken(principal, credentials);
SecurityContextHolder.getContext().setAuthentication(auth);
But again, you haven't said what problem you're actually trying to solve...

Resources