First of all, i'm not a pro with tests, actually i'm learning to use them, and i'm finding myself in a lot of trouble trying to test Spring Controllers with the Spring Test framework, so i did the test after the production code, because i didn't know exactly how build the test.
The project is a Spring Boot (1.2.3) with Web, Thymeleaf, Security, MongoDB and Actuator modules, and using the platform-bom (1.1.2) for version management.
The functionality is easy, while the user fills a form, ajax requests using jQuery are sent to the server to verify the last field the user filled, then the server performs validation, and returns a JSON response.
Client side validation is not an option because some information has to be queried to a database to perform the validation (like if a username is already in use or not), so it needs to be done server side.
Also all the validation is performed with Hibernate Validator and custom annotations, both field and class annotations, since we use 1 entry point to perform the interactive form validation, the entire form is send in each request.
Here is the method signature (and first validations) in a #RestController :
#RequestMapping(value = "/some-url", method = RequestMethod.POST)
public ResponseEntity<ValidationResponse> validateFormField(
#Valid #ModelAttribute UserRegistrationForm form, BindingResult result) {
if (form == null || form.getSomeValue() == null) {
return new ResponseEntity<ValidationResponse>(HttpStatus.BAD_REQUEST);
}
// more input validation and actions
}
Now the call code, using jQuery:
$.ajax({
url: '/some-url',
type: 'post',
data: $(selector).serialize(),
dataType: 'json',
...
});
Now, the unit test code i'm writting to automatic test this is:
#ActiveProfiles("testing")
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = {
Application.class, WebConfiguration.class,
MongoConfiguration.class, SecurityConfiguration.class
})
#WebAppConfiguration
public class RegistrationInteractiveValidationRestControllerTest {
#Resource
private FilterChainProxy springSecurityFilterChain;
#Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
#Before
public void setUp() throws Exception {
mockMvc = webAppContextSetup(wac)
.addFilter(springSecurityFilterChain)
.build();
}
#Test
public void withValidInput_returnStatusOK() throws Exception {
// test init
MockHttpServletRequestBuilder ajaxRequest = post("/some-url")
.content( "username=Bobby&email=& ...more fields... " )
.secure( true ) .with( csrf().asHeader() );
// test action
ResultActions performRequest = mockMvc.perform(ajaxRequest);
// test verification
performRequest.andExpect( status().isOk() );
}
#Test
public void withInvalidInput_returnStatusBadRequest() throws Exception {
// test init
MockHttpServletRequestBuilder ajaxRequest = post("/some-url")
.content( "{ 'im' : 'a bad content request' }" )
.secure( true ) .with( csrf().asHeader() );
// test action
ResultActions performRequest = mockMvc.perform(ajaxRequest);
// test verification
performRequest.andExpect(status().isBadRequest());
}
}
The second test goes alright, status code is 400 as expected, but in the first test, it fails because it also returns a 400.
I did some test debugging and the form object itself is not null, but all its fields are null.
But if i deploy the app in a server, and do the test manually, it works just fine, ajax calls get responses with valid JSON and HTTP 200 status codes.
I also did some debugging of the app deployed in a server and the databinding works just fine.
My thoughts are that i'm missing something building the request in the test, so what i've tried is to verify in the browser the request headers and form data when the app is deployed on a server, and added those to the requests i'm building in the tests, all of them, without any luck at all.
I've researched also the Spring Framework, Spring Boot, Spring security and Spring Test reference documentation, and I didn't find anything that could lead me to the right answer.
Related
I tested Spring Security as part of my Spring Boot Setup in version 6.0-M5, 6.0-RC1 and 6.0-RC2. I recognized a behavior change and wanted to ask whether this may be a bug. I return the CSRF token as a serialized JSON, but since RC1 the content of the token in the JSON is garbage.
My working code in Spring Boot 6 Milestone 5 still working as expected.
#RestController
public class CsrfController {
#GetMapping("/rest/user/csrf")
public CsrfToken csrf(CsrfToken token) {
return token;
}
}
In my use case I query the controller using a unit test.
#LocalServerPort
int serverPort;
#Autowired
private TestRestTemplate webclient;
#Test
public void getCsrf() {
ResponseEntity<String> entity = webclient.getForEntity("http://localhost:" + serverPort +
"/rest/user/csrf", String.class);
// ... here some code to get the token from the JSON body ...
assertTrue(result.matches("^[a-f0-9\\-]+$"));
This is the first query of the server. A session object between client and server is not established in past queries. This worked in M5 but stopped working in Spring Boot 6 RC1 and RC2
The following controller code made it work again in RC2:
#GetMapping("/rest/user/csrf")
public CsrfToken csrf(HttpServletRequest request, HttpServletResponse response) {
CsrfToken repoToken = tokenRepo.loadToken(request);
if (repoToken != null) {
return repoToken;
}
// required because it is required but ay not be initialized by the tokenRepo
request.getSession();
repoToken = tokenRepo.generateToken(request);
tokenRepo.saveToken(repoToken, request, response);
return repoToken;
}
If I tried the old code in RC2, I received on client side a malformed string. I did not receive a UUID styled token in my JSON serialized response body. I think it is related to the uninitialized session object.
Is this a bug or is an uninitialized session and a resulting not working CrsfToken specified behavior?
I think the issue is in the way I try to get and use the XSFR token.
Because I want to use an Angular frontend, I configured my token repository to provide the tokens via Cookie.
http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
This produces cookies the old UUID style. However the authentication expects the new tokens as generated by https://github.com/spring-projects/spring-security/issues/11960 . Probably the cookie mechanism still needs to be migrated until final Spring Boot 3.0.
I'm working at a small parking service REST API.
For example I have endpoint:
#RequestMapping("/departure")
public ResponseEntity<String> departure(#RequestBody CarAtGateModel carAtGateModel) throws Exception {
return parkingService.carDeparture(carAtGateModel.getCarEntity().getIdCar());
}
method parkingService.carDeparture looks like this:
public ResponseEntity<String> carDeparture(String carID) throws UnidentifiedCarException {
CarAndParkingIDsEntity carAndParkingIDsEntity = carAndParkingIDsRepository.findByIdCar(carID);
if (carAndParkingIDsEntity == null) {
throw new UnidentifiedCarException();
} else {
carAndParkingIDsEntity.setIdParking("-1");
carAndParkingIDsRepository.flush();
return new ResponseEntity<>("Gate up", HttpStatus.OK);
}
}
And the problem is when I'm trying to do some integration tests. Unit tests for parking service pass correctly but I don't know exactly what should I do for integration tests.
I was thinking about something like. Mock carAtGateModel (it's carId and ParkingId) and send it to the endpoint and then mock parkingservice because I'm using it inside and I don't want to change data in the database.
when(parkingService.carDeparture(anyString())).thenReturn(new ResponseEntity<>("Gate up",
HttpStatus.OK));
HttpEntity<CarAtGateModel> request = new HttpEntity<>(carAtGateModel);
ResponseEntity<String> response = testRestTemplate.postForEntity("/departure", request,
String.class);
I would suggest you use #WebMvcTest instead to have a slice test (sort of integration test that focuses on a single application layer).
#ExtendWith(SpringExtension.class)
#WebMvcTest(YourController.class)
public class YourControllerIntegrationTest {
#Autowired
private MockMvc mockMvc;
#MockBean
private ParkingService parkingService;
// Your test cases here
}
You would use mockMvc to perform an actual call to the REST endpoint with an actual CarAtGateModel object as JSON in the request body. You would also mock the behaviour of your ParkingService, thus focusing the test on your Controller.
I am writing an integrationtest for a resource (Jax-rs with resteasy) which invokes a thirdparty request to the Trustpilot Api to get a review link for a product.
In my tests I wanted to mock this call by using Wiremock and invoke the request by using resteasy Dispatcher.
The code below shows my Setup (names etc are changed).
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = { MyResourceTestContext.class })
public class MyResourceIntegrationTest {
#Rule
public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort().dynamicHttpsPort());
#Autowired
private MyResource myResource;
private Dispatcher dispatcher;
private MockHttpResponse response;
#Before
public void setUp() throws Exception {
dispatcher = MockDispatcherFactory.createDispatcher();
dispatcher.getRegistry().addSingletonResource(myResource);
response = new MockHttpResponse();
}
#Test
public void testResourceShouldReturnRedirectToTrustpilotreview() throws Exception {
URI apiRequestURI = UriBuilder
.fromUri("https://api.trustpilot.com/v1/private/product-reviews/business-units"
+ "/{businessUnitId}}/invitation-links")
.build(BUSINESS_UNIT_ID);
URI expectedRedirectURI = UriBuilder.fromUri("https://products.trustpilot.com/#evaluate/{reviewLinkId}")
.build(REVIEW_LINK_ID);
stubFor(post(apiRequestURI.toString())
.willReturn(okJson("{\"reviewLinkId\":\"" + REVIEW_LINK_ID + "\","
+ "\"reviewUrl\":\"" + expectedRedirectURI.toString() + "\"}")));
MockHttpRequest request = MockHttpRequest.get("/myResource");
dispatcher.invoke(request, response);
WireMock.verify(1, postRequestedFor(urlEqualTo(apiRequestURI.toString())));
Assert.assertEquals(HttpStatus.TEMPORARY_REDIRECT.value(), response.getStatus());
Assert.assertEquals(expectedRedirectURI.toString(), response.getOutputHeaders().getFirst("Location"));
}
But what I'm getting is (with my actual bid in the url):
com.github.tomakehurst.wiremock.client.VerificationException: Expected at least one request matching: {
"url" : "https://api.trustpilot.com/v1/private/product-reviews/business-units/<bid>/invitation-links",
"method" : "POST"
}
Requests received: [ ]
Stepping into the request with the Debugger I can say this request and other requests are actually executed successfully. But they are not recognized/mocked by Wiremock.
Is there something wrong with my Wiremock setup? Any Ideas? Thank you!
There are a couple of reasons why this isn't sending anything to WireMock at the moment:
WireMock only accepts relative URLs when stubbing so passing the full URL including protocol and domain won't work. Try something equivalent to stubFor(post(urlPathEqualTo("/v1/private/product-reviews/business-units/<bid>/invitation-links") instead.
There doesn't seem to be any way for your class under test to know which host/port WireMock is running on and no forward proxying set up so the code is presumably still trying to call the real Trustpilot API.
To solve 2. you have a couple of options:
Set up your class/code under test to use localhost as the destination host and the result of wireMockRule.port() as the port number, or wireMockRule.httpsPort() if you're on HTTPS.
Use the new auto JVM proxy setup feature to proxy your app's calls through WireMock. This assumes you're on WireMock 2.31.0+ and that your app's HTTP client respects the JVM proxy system properties.
due to this tutorial - https://www.baeldung.com/spring-boot-custom-error-page I wanted to customize my error page ie. when someone go to www.myweb.com/blablablalb3 I want to return page with text "wrong url request".
All works fine:
#Controller
public class ApiServerErrorController implements ErrorController {
#Override
public String getErrorPath() {
return "error";
}
#RequestMapping("/error")
public String handleError() {
return "forward:/error-page.html";
}
}
But I dont know how to test it:
#Test
public void makeRandomRequest__shouldReturnErrorPage() throws Exception {
this.mockMvc.perform(get(RANDOM_URL))
.andDo(print());
}
print() returns:
MockHttpServletResponse:
Status = 404
Error message = null
Headers = {X-Application-Context=[application:integration:-1]}
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
So I cant created something like this:
.andExpect(forwardedUrl("error-page"));
because it fails, but on manual tests error-page is returned.
Testing of a custom ErrorController with MockMvc is unfortunately not supported.
For a detailed explanation, see the official recommendation from the Spring Boot team (source).
To be sure that any error handling is working fully, it's necessary to
involve the servlet container in that testing as it's responsible for
error page registration etc. Even if MockMvc itself or a Boot
enhancement to MockMvc allowed forwarding to an error page, you'd be
testing the testing infrastructure not the real-world scenario that
you're actually interested in.
Our recommendation for tests that want to be sure that error handling
is working correctly, is to use an embedded container and test with
WebTestClient, RestAssured, or TestRestTemplate.
My suggestion is to use #ControllerAdvice
In this way you can work around the problem and you can continue to use MockMvc with the big advantage that you are not required to have a running server.
Of course to test explicitly the error page management you need a running server. My suggestion is mainly for those who implemented ErrorController but still want to use MockMvc for unit testing.
#ControllerAdvice
public class MyControllerAdvice {
#ExceptionHandler(FileSizeLimitExceededException.class)
public ResponseEntity<Throwable> handleFileException(HttpServletRequest request, FileSizeLimitExceededException ex) {
return new ResponseEntity<>(ex, HttpStatus.PAYLOAD_TOO_LARGE);
}
#ExceptionHandler(Throwable.class)
public ResponseEntity<Throwable> handleUnexpected(HttpServletRequest request, Throwable throwable) {
return new ResponseEntity<>(throwable, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Need a help with integration tests for Oauth2 client.
Setup:
Client with protected UI and API
Authentication server where all
password validation done and access token retrieved
Integration test:
rest-assured used for end-point testing
before implementing Oauth2 tests worked fine
Ole test example:
given().auth()
.preemptive()
.basic(USER_EMAIL,PASSWORD) <-- this not valid any more
.contentType(ContentType.JSON)
.when()
.pathParam("id","123")
.delete(PROFILE_FIELD_BASE_URL)
.andReturn()
.body();
Question:
how I can make this tests work again?
How res-assured setup should be changed to support oauth2?
Do I need to mock Authentication server or can I inject/mock security context?
The code you were shown is only and only for basic auth and for using the rest assured for OAuth, in general, you have to change that code. From REST Assured github page you can see following two exaples:
#Test public void
oauth2_works_with_preemptive_header_signing() {
final String accessToken = "accessToken";
given().
auth().preemptive().oauth2(accessToken).
filter(new Filter() {
public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, FilterContext ctx) {
assertThat(requestSpec.getHeaders().getValue("Authorization"), equalTo("Bearer "+accessToken));
return new ResponseBuilder().setBody("ok").setStatusCode(200).build();
}
}).
when().
get("/somewhere").
then().
statusCode(200);
}
#Test public void
oauth2_works_with_non_preemptive_header_signing() {
final String accessToken = "accessToken";
given().
auth().oauth2(accessToken).
filter(new Filter() {
public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, FilterContext ctx) {
AuthenticationScheme scheme = requestSpec.getAuthenticationScheme();
assertThat(scheme, instanceOf(PreemptiveOAuth2HeaderScheme.class));
assertThat(((PreemptiveOAuth2HeaderScheme) scheme).getAccessToken(), equalTo(accessToken));
return new ResponseBuilder().setBody("ok").setStatusCode(200).build();
}
}).
when().
get("/somewhere").
then().
statusCode(200);
}
and as the other example, you can have a look here.