I am playing around with Spring Security and now I am trying get some knowledge about testing my REST-controller with regards to security.
So I prepared my test-class with:
#Autowired
private WebApplicationContext context;
private MockMvc mvc;
#Before
public void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
containing test-cases mostly of the following or similar form:
#Test
public void handleSecuredRequest_shouldReturn200_withAdminUser() throws Exception{
ResultActions action = mvc.perform(get("/secured").with(user("admin").roles("ADMIN")));
int status = action.andReturn().getResponse().getStatus();
assertTrue("expected status code = 200 ; current status code = " + status, status == 200);
}
What I was not able to achieve until now were things concerning sessions.
Most notably I would be interested to verify that session-invalidation is performed correctly.
How can I achieve that?
EDIT:
I was able to find something close to a solution doing the following based on
https://stackoverflow.com/a/26281932/6294605 :
#Test
public void logout_shouldInvalidateSession_withLoggedInUser() throws Exception{
ResultActions action = mvc.perform(get("/userAsJSON").with(user("user")));
MockHttpSession session = (MockHttpSession) action.andReturn().getRequest().getSession();
ResultActions action2 = mvc.perform(post("/logout").session(session));
ResultActions action3 = mvc.perform(get("/userAsJSON").session(session));
int status3 = action3.andReturn().getResponse().getStatus();
assertTrue("expected status code = 401 ; current status code = " + status3, status3 == 401);
}
But I am not entirely satisfied with this.
It requires to process several steps to let me test what I want.
Resulting from 1.: it is not detached from certain other things to function correctly (e.g. the "/userAsJSON" endpoint returning 401 for not authenticated users).
Resulting from 2.: it requires additional attention to make sure that test-cases exist that ensure that the formal requirements for my test to be valid are met.
So I would wish for an option to make this less error-prone.
Any ideas?
Related
I need some help with unit tests. Our company has decided to start using TDD, and I'm supposed to implement that, but I've got very limited experience with unit tests in general, so I'm trying to cover some of the old code to get up to speed. That's when I got stuck with this:
public Analytics generateAnalytics(String id, String domain) {
List<Result> totalResults = new ArrayList<>();
for(String url : ANALYTIC_URLS) {
url += domain;
String scrubbedUrl = url.replace(API_KEY, "XXXXXXXXXX");
Audit audit = new Audit(id, scrubbedUrl);
try {
totalResults.add(new Result(getData(url, audit)));
} catch(Exception e) {
audit.setResponse(e.toString());
throw new Exception(e);
} finally {
auditRepository.save(audit);
}
}
return composeAnalytics(totalResults);
}
private List<Map<String, String>> getData(String request, Audit audit) throws Exception {
try (CloseableHttpClient client = HttpClients.createDefault()) {
CloseableHttpResponse response = client.execute(new HttpGet(request));
if(response.getStatusLine().getStatusCode() == 200) {
return readInputStream(response.getEntity().getContent(), audit);
} else {
throw new Exception(response.toString());
}
}
}
My issue is, when I wanna test the generateAnalytics method, the getData method goes and gets data from a live API that costs units per each request. Obviously I wanna stop this from bleeding out all our units during the testing. I've tried mocking the ClosableHttpClient like so:
#Mock
CloseableHttpClient client;
#InjectMocks
Service service;
#Test
void testTest() throws Exception {
when(client.execute(any())).thenReturn(mock(CloseableHttpResponse.class));
service.generateAnalytics("123", "no.com");
assertEquals(true, true);
}
This works when there's another service that needs to be mocked in one of my other tests, but in this case it still calls the API and drains our units. What should I do about this?
That's because your client mock is never used:
try (CloseableHttpClient client = HttpClients.createDefault()) {
Either mock HttpClients.createDefault() or , maybe better, inject the client into the service instead of creating on the fly.
Have you try to use MockMvc?
With MockMvc you can perform requests against a mocked servlet environment.
There won't be any real HTTP communication for such tests.
Official documentation: spring.io/testing-web
This function is used to update the user details in the database. can someone help me to write test cases for this function.
#RequestMapping(value = "/updateDetails", method = RequestMethod.POST)
public String updateVendorDetails(#Valid #ModelAttribute("users") Users users, BindingResult result,Model model) {
logger.info("{}.{}",new VendorController().getClass().getPackageName(), new VendorController().getClass().getName());
if(result.hasErrors()) {
model.addAttribute("edit","edit");
logger.warn("Function: updateVendorDetails(), Information: Error while updating vendor details");
return register.toString();
}
userDao.updateVendorDetails(users);
logger.info("Function: updateVendorDetails(), Information: Vendor details updated successfully");
return vendor.toString();
}
Update
Code:
mockMvc.perform(post("/updateDetails").accept(MediaType.TEXT_HTML).params(params)).andExpect(status().isOk());
Resulting error:
This says that post method is forbidden and my test fails
This is my Test class
#RunWith(SpringJUnit4ClassRunner.class)
#SpringBootTest
public class TestVendorPage {
#Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
#Before
public void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context).apply(springSecurity()).build();
}
#WithMockUser(roles = "VENDOR")
#Test
public void testIfUpdateEdtailsIsAvailableOnlyForVendor() throws Exception {
MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>();
params.add("firstName", "vinod");
params.add("lastName", "babu");
params.add("contactNumber", "9952016709");
mockMvc.perform(post("/updateDetails").accept(MediaType.TEXT_HTML).params(params)).andExpect(status().isOk());
}
}
Regarding your update:
Thank you for clarifying your post with a specific error/specific problem.
For that specific error - HTTP 403: Forbidden - this should resolve the problem:
Unit test Springboot MockMvc returns 403 Forbidden
i think probleam is happend in "mockMvc" object is not
autowired.mockMvc object should load from WebApplicationContext in
before program run.
Please - PLEASE - consider looking at one or more of the links I cited above.
baeldung.com: Testing in Spring Boot
spring.io: Testing the Web Layer
mkyong.com: Spring REST Integration Example
I've found all three sites very valuable resources. Time spent with these tutorials will help you a great deal!
I'm in the process of building a new microservice and securing it with access tokens from Keycloak. So far I've been successful, the endpoint /token/test is only accessible with a valid token from Keycloak, the application properties look like this:
keycloak.auth-server-url=http://localhost:8888/auth
keycloak.realm=realm
keycloak.resource=api
keycloak.public-client=true
keycloak.securityConstraints[0].authRoles[0]=basic-token
keycloak.securityConstraints[0].securityCollections[0].name=secured by basic access token
keycloak.securityConstraints[0].securityCollections[0].patterns[0]=/*
This is working fine when starting the project with mvn spring-boot:run (I'm using spring-boot-starter, keycloak-spring-boot-starter and without spring-boot-starter-security which I want to avoid if possible.
Now I'm writing some tests for the fun of it and Keycloak's security constraints are simply not working. I've followed the test setup from https://github.com/cremich/testcontainers-keycloak (with updated versions and JUnit 5), the only difference being that the example is doing a lot of Keycloak setup by hand using spring-boot-starter-security. Why does it only work in tests when done with -security and why does my way not seem to work?
Am I missing something?
Thanks.
Edit: Example project
After some hours of debugging, I finally figured it out. The problem is that Keycloak's authentication is (for whatever reason, lol) done in a Tomcat valve, not in a filter. MockMvc doesn't go through the servlet container (and its valves), so it never even reaches the point where it would be authenticated.
TestRestTemplate does, though (and if you use starter-security, it is also a filter not a valve). I don't know the design decision behind using a valve and not a filter but you can either use the configuration from keycloak-starter and test it with a TestRestTemplate or use the more 'expensive' starter-security configuration in combination with MockMvc.
You need to set the authentication in SecurityContext with a mock or instance of the right type in each test: SecurityContextHolder.getContext().setAuthentication(authentication)
I wrote a set of libs to ease this. It includes a #WithMockKeycloackAuth annotation, along with Keycloak dedicated MockMvc request post-processor and WebTestClient configurer / mutator
Sample #WithMockKeycloackAuth usage:
#RunWith(SpringRunner.class)
#WebMvcTest(controllers = GreetingController.class)
#Import({
ServletKeycloakAuthUnitTestingSupport.UnitTestConfig.class,
KeycloakSpringBootSampleApp.KeycloakConfig.class })
// because this sample stands in the middle of non spring-boot-keycloak projects, keycloakproperties are isolated in
// application-keycloak.properties
#ActiveProfiles("keycloak")
public class GreetingControllerAnnotatedTest {
private static final String GREETING = "Hello %s! You are granted with %s.";
#MockBean
MessageService messageService;
#MockBean
JwtDecoder jwtDecoder;
#Autowired
MockMvcSupport api;
#Before
public void setUp() {
when(messageService.greet(any())).thenAnswer(invocation -> {
final var auth = invocation.getArgument(0, Authentication.class);
return String.format(GREETING, auth.getName(), auth.getAuthorities());
});
}
#Test
#WithMockKeycloakAuth
public void whenAuthenticatedWithoutAuthorizedPersonnelThenSecuredRouteIsForbidden() throws Exception {
api.get("/secured-route").andExpect(status().isForbidden());
}
#Test
#WithMockKeycloakAuth({ "AUTHORIZED_PERSONNEL" })
public void whenAuthenticatedWithAuthorizedPersonnelThenSecuredRouteIsOk() throws Exception {
api.get("/secured-route").andExpect(status().isOk());
}
#Test
#WithMockKeycloakAuth(
authorities = { "USER", "AUTHORIZED_PERSONNEL" },
id = #IdTokenClaims(sub = "42"),
oidc = #OidcStandardClaims(
email = "ch4mp#c4-soft.com",
emailVerified = true,
nickName = "Tonton-Pirate",
preferredUsername = "ch4mpy"),
accessToken = #KeycloakAccessToken(
realmAccess = #KeycloakAccess(roles = { "TESTER" }),
authorization = #KeycloakAuthorization(
permissions = #KeycloakPermission(rsid = "toto", rsname = "truc", scopes = "abracadabra"))),
privateClaims = #ClaimSet(stringClaims = #StringClaim(name = "foo", value = "bar")))
public void whenAuthenticatedWithKeycloakAuthenticationTokenThenCanGreet() throws Exception {
api.get("/greet")
.andExpect(status().isOk())
.andExpect(content().string(startsWith("Hello ch4mpy! You are granted with ")))
.andExpect(content().string(containsString("AUTHORIZED_PERSONNEL")))
.andExpect(content().string(containsString("USER")))
.andExpect(content().string(containsString("TESTER")));
}
}
Different libs are available from maven-central, choose one of following according to your use-case:
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-security-oauth2-test-addons</artifactId>
<version>2.3.4</version>
<scope>test</test>
</dependency>
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-security-oauth2-test-webmvc-addons</artifactId>
<version>2.3.4</version>
<scope>test</test>
</dependency>
I consider myself a novice at unit-testing, completely new to Mockito and junit. I have to write unit-tests for some simple api-calls. But my test seems somewhat pointless to me, I can't tell where I am going wrong. I have added a method to an existing web-service, ManagerWS.java , See Below.
ManagerWS.java Method:
public String healthCheck(String userId) {
String healthCheckUrlEndpoint = this.baseUrl()+"/health";
logger.debug("Calling health check: {}", healthCheckUrlEndpoint);
HttpHeaders healthCheckHeaders = new HttpHeaders();
healthCheckHeaders.setContentType(MediaType.APPLICATION_JSON_UTF8);
healthCheckHeaders.add(USER_KEY, userId);
healthCheckHeaders.add(TOKEN_NAME, TOKEN_VALUE);
healthCheckHeaders.add(Constants.ACCEPT_LANGUAGE_HEADER, LocaleContextHolder.getLocale().toString());
healthCheckHeaders.add(CORRELATION_HEADER, myService.get(AppLoggingMDCService.LOG_KEY_REQUEST_ID));
HttpEntity<Object> request = new HttpEntity<Object>(healthCheckHeaders);
ResponseEntity<String> response;
try {
response = makeRequest(HttpMethod.GET, healthCheckUrlEndpoint, request, String.class);
} catch (Exception e) {
logger.error("Exception encountered during health check", e);
throw e;
}
logger.debug("RESPONSE : http status: {} - body: {}", response.getStatusCode(), response.getBody());
return response.getStatusCode().toString();
}
The logic is simple. Construct the url, create headers and add headers to the request. make the request and extract the status-code from the response. Here is my test. NOTE: the class is using #RunWith(SpringRunner.class) and I am using #Mock for dependencies and #InjectMocks for the local instance ManagerWS. ManagerWS.java is the service calss being tested.
TEST-CLASS TEST-Method:
#Test
public void testHealthCheck() throws Exception {
//Given
managerWS = new ManagerWS(templateFactory, configParamService, mdcService, env);
String url = "http://baseurl/health";
HttpHeaders headers = new HttpHeaders();
HttpEntity<Object> request = new HttpEntity<Object>(headers);
ResponseEntity<String> response = new ResponseEntity<>(HttpStatus.OK);
//when
when(managerWS.makeRequest(HttpMethod.GET, url, request, String.class)).thenReturn(response);
String actualStatus = response.getStatusCode().toString();
//then
Assert.assertEquals("200",actualStatus);
}
To me this test seems stupid (for want of a batter word). I basicall set the status to give a "200" and assert that what i set is "200". That is not really making much sense.To me it literally does nothing. I tried using spy(ManagerWS.class). But I am literally grasping at straws without the full understanding.
SonarQube still complains with "Not covered by unit tests". I cam completely stumped as to how else to write this test. I also have to do similar tests for three other calls.
I am a total novice to testing and I cannot see my mistake. Please advise.
SonarQube still complains with "Not covered by unit tests".
Your unit test doesn't test from the entry point of the method to test : healthCheck(String), so it is not covered by unit tests.
Besides, you also mock the part of the method that you want to test :
when(managerWS.makeRequest(HttpMethod.GET, url, request, String.class)).thenReturn(response);
So indeed your approach looks wrong.
In fact, writing an unit test for this code looks wrong too or at least looks like a white box test with few value.
Why ?
Your logic depends on :
response = makeRequest(HttpMethod.GET, healthCheckUrlEndpoint, request, String.class);
But you can know if it works only at runtime, with a running HTTP Server.
So the single thing that you can do is mocking everything, spying the object under test and verifying that each statement in the implementation is performed : no readable test, no robust and few/no value.
Your method that relies essentially on side effect would make more sense to be tested with as an integration test :
ManagerWS managerWS; // real ManagerWS implementation without mock
#Test
public void healthCheck() throws Exception {
//Given
String url = "http://baseurl/health";
// When
String actual managerWS.healthCheck(url);
// Then
String expected = "...";
Assertions.assertEquals(expected, actual);
}
As a side note, if you used Spring, I would advise you to look at test slicing #WebMvcTest that focuses on the web part of the component under test. It allows mainly to test the HTTP part/logic (headers, request, response).
A test case for my contact formular page is to make sure it's always in a secure context respectively using SSL. Basically, all I want to know, is that I have a given request where request.secure = true;
The following response does not contain any information about this and its headers are empty:
#Test
public void shouldShowContactForm() {
Response response = GET("/contact");
// How can I ask the response, if the complete URL is in HTTPS?
}
Even if I explicitly set my own request, I cant see the right way to do this:
#Test
public void shouldShowContactFormInSSLContext() {
Request request = newRequest();
request.secure = true;
Response response = GET(request, "/contact");
// Is it now possible?
}
Is this even the right approach to test this or am I simply missing something important about the request/response?
For this question I think what I've done for my apps in the past is have a #before interceptor on all my controllers that looks like this.
#Before
static void checkSSL(){
if(Play.mode.equals(Play.Mode.PROD)){
if(!request.secure) {
Router.ActionDefinition cashTicketDefinition = Router.reverse(request.controller + "." + request.actionMethod);
cashTicketDefinition.absolute();
String url = cashTicketDefinition.url.replaceFirst( "http:", "https:");
redirect(url, true);
}
}
}