Issue
I have a controller which gives the user a 403 response unless they are authenticated with a JWT token which is passed as a Bearer token via the authorization header. I'm looking for resources on how to test this with Mockito but I'm not very successful so far as most of them tell me to use the @WithMockUser annotation, which I understand is for Spring security yes, but does not include the mocking for a JWT token. I've tried to mock a few objects such as the UserDetailsClass and the JwtFilter and even hardcoding the bearer token but I think there should be more to it.
@MockBean
private CategoryCommandService categoryCommandService;
@Autowired
private MockMvc mockMvc;
@MockBean
private MyUserDetailsService myUserDetailsService;
@MockBean
private CategoryRepository categoryRepository;
@MockBean
private JwtUtil jwtUtil;
@Autowired
private JwtRequestFilter filter;
@Test
void testCreateCategory() throws Exception {
CategoryCreateDto categoryCreateDto = new CategoryCreateDto("category");
CategoryCreateDto categoryCreateResponseDto = new CategoryCreateDto(UUID.fromString("2da4002a-31c5-4cc7-9b92-cbf0db998c41"), "category");
String jsonCreate = asJsonString(categoryCreateDto);
String jsonResponse = asJsonString(categoryCreateResponseDto);
RequestBuilder request = MockMvcRequestBuilders
.post("/api/adverts/category")
.content(jsonCreate)
.header("Authorization", "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJmb29AZW1haWwuY29tIiwiZXhwIjoxNjM4ODU1MzA1LCJpYXQiOjE2Mzg4MTkzMDV9.q4FWV7yVDAs_DREiF524VZ-udnqwV81GEOgdCj6QQAs")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON);
mockMvc.perform(request).andReturn();
when(categoryCommandService.createCategory(categoryCreateDto)).thenReturn(
categoryCreateResponseDto);
MvcResult mvcResult = mockMvc.perform(request)
.andExpect(status().is2xxSuccessful())
.andExpect(content().json(jsonResponse, true))
.andExpect(jsonPath("$.id").value("2da4002a-31c5-4cc7-9b92-cbf0db998c41"))
.andExpect(jsonPath("$.title").value("category"))
.andReturn();
logger.info(mvcResult.getResponse().getContentAsString());
}
Here my controller:
@CrossOrigin
@RequestMapping("/api/adverts/category")
@RestController
public class CategoryCommandController {
@Autowired
private CategoryCommandService categoryCommandService;
@Autowired
private CategoryRepository categoryRepository;
@PostMapping(produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Object> createCategory(@RequestBody CategoryCreateDto categoryCreateDto) {
if (categoryCreateDto.getTitle() != null) {
return new ResponseEntity<>(categoryCommandService.createCategory(categoryCreateDto), HttpStatus.CREATED);
}
else {
return new ResponseEntity<>(new FeedbackMessage("Missing title"), HttpStatus.BAD_REQUEST);
}
}
}
And here my filter:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import static com.example.adverts.SecurityConstants.SIGN_UP_URL;
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private MyUserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String path = request.getRequestURI();
if (path.equals(SIGN_UP_URL)) {
chain.doFilter(request, response);
return;
}
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
username = jwtUtil.extractUsername(jwt);
} else {
response.setStatus(HttpStatus.FORBIDDEN.value());
}
if (username != null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
chain.doFilter(request, response);
}
}
}
And JwtUtil class:
@Service
public class JwtUtil {
private String SECRET_KEY = "secret";
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY).compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
Here is the whole Github branch.
https://github.com/francislainy/adverts-backend/tree/dev_jwt
Thank you.
UPDATE
For clarity if I hardcode a valid token I get a 200 status code but my tests will still fail with nothing returned for content whereas before JWT and Spring security they were passing.
Solution
The main problem is using
@MockBean
private JwtUtil jwtUtil;
Which make JwtRequestFilter perform wrongly in
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
username = jwtUtil.extractUsername(jwt);
}
As username
will always return null from the mock bean.
To use the actual JwtUtils
Add includeFilters
to include it in spring context,
then we also need to mock myUserDetailsService.loadUserByUsername
used in JwtRequestFilter
. After that the test will pass.
Refer to comment inside below code for the changes.
@WebMvcTest(value = CategoryCommandController.class, includeFilters = {
// to include JwtUtil in spring context
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = JwtUtil.class)})
class CategoryCommandControllerTest {
Logger logger = LoggerFactory.getLogger(CategoryCommandController.class);
@MockBean
private CategoryCommandService categoryCommandService;
@Autowired
private MockMvc mockMvc;
@MockBean
private MyUserDetailsService myUserDetailsService;
@MockBean
private CategoryRepository categoryRepository;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private JwtRequestFilter filter;
// @WithMockUser
@Test
void testCreateCategory() throws Exception {
CategoryCreateDto categoryCreateDto = new CategoryCreateDto("category");
CategoryCreateDto categoryCreateResponseDto = new CategoryCreateDto(UUID.fromString("2da4002a-31c5-4cc7-9b92-cbf0db998c41"), "category");
String jsonCreate = asJsonString(categoryCreateDto);
String jsonResponse = asJsonString(categoryCreateResponseDto);
UserDetails dummy = new User("[email protected]", "foo", new ArrayList<>());
String jwtToken = jwtUtil.generateToken(dummy);
RequestBuilder request = MockMvcRequestBuilders
.post("/api/adverts/category")
.content(jsonCreate)
.header("Authorization", "Bearer " + jwtToken)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON);
// Below line is not used
// mockMvc.perform(request).andReturn();
// Should be createCategory(eq(categoryCreateDto))?
when(categoryCommandService.createCategory(categoryCreateDto)).thenReturn(
categoryCreateResponseDto);
// Mock Service method used in JwtRequestFilter
when(myUserDetailsService.loadUserByUsername(eq("[email protected]"))).thenReturn(dummy);
MvcResult mvcResult = mockMvc.perform(request)
.andExpect(status().is2xxSuccessful())
// .andExpect(content().json(jsonResponse, true))
.andExpect(jsonPath("$.id").value("2da4002a-31c5-4cc7-9b92-cbf0db998c41"))
// .andExpect(jsonPath("$.title").value("category"))
.andReturn();
logger.info(mvcResult.getResponse().getContentAsString());
}
...
}
Answered By - samabcde
Answer Checked By - Clifford M. (JavaFixing Volunteer)