mardi 2 juin 2020

SecurityContextHolder not getting cleared in utility class after every test method when @WithMockUser is used on test methods

I've created a helper method to fetch "User" object from the authenticated user as below.

@Component
@RequiredArgsConstructor
public class AuthUserUtil {
    @NonNull
    private final UserService userService;
    @NonNull
    private final DTOConversionUtil dtoConversionUtil;
    private User user;

    public User getAuthUser() {
        if (this.user != null) {
            return this.user;
        }
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null) {
            if (authentication.isAuthenticated() && !StringUtils.isBlank(authentication.getName())) {
                this.user = dtoConversionUtil.convertToEntity(
                        userService.getUserByUserName(
                               authentication.getName()
                        )
                );
                return this.user;
            } else throw new UsernameNotFoundException("Invalid user or not authorized");
        } else {
            throw new UsernameNotFoundException("Invalid user or not authorized");
        }
    }
}

And I'm using it inside a service class to fetch the authenticated user whenever necessary.

@Service
@Validated
public class CategoryServiceImpl implements CategoryService {

    private final CategoryRepository categoryRepository;
    private final DTOConversionUtil dtoConversionUtils;
    private final AuthUserUtil authUserUtil;

    public CategoryServiceImpl(CategoryRepository categoryRepository, DTOConversionUtil dtoConversionUtils, AuthUserUtil authUserUtil) {
        this.categoryRepository = categoryRepository;
        this.dtoConversionUtils = dtoConversionUtils;
        this.authUserUtil = authUserUtil;
    }

    @Override
    public List<CategoryDTO> getAllCategories() throws Exception {
        List<Category> categories = categoryRepository.findAllByUser(authUserUtil.getAuthUser());
        if (categories.isEmpty()) {
            throw new Exception("No Categories found");
        }
        List<CategoryDTO> resultList = new ArrayList<>();
        categories.forEach(category -> resultList.add(dtoConversionUtils.convertToDTO(category)));
        return resultList;
    }

    @Override
    public CategoryDTO getCategory(int categoryID) throws CategoryNotFoundException {
        Optional<Category> category = categoryRepository.findByIdAndUser(categoryID, authUserUtil.getAuthUser());
        if (category.isEmpty()) {
            throw new CategoryNotFoundException("Category with ID: " + categoryID + " not found!");
        }

        return dtoConversionUtils.convertToDTO(category.get());
    }

    @Override
    public CategoryDTO getCategory(String categoryName) throws CategoryNotFoundException {
        if (StringUtils.isBlank(categoryName)) {
            throw new CategoryNotFoundException("Empty Category Name given");
        }
        Optional<Category> category = categoryRepository.findByCategoryNameAndUser(categoryName, authUserUtil.getAuthUser());
        if (category.isEmpty()) {
            throw new CategoryNotFoundException("No category found with the given name \"" + categoryName + "\"");
        }
        return dtoConversionUtils.convertToDTO(category.get());
    }

    @Override
    public CategoryDTO saveCategory(@Valid CategoryDTO newCategory) throws CategoryAlreadyExistsException {

        // Check if a category exists with the given category name.
        if (categoryRepository.findByCategoryNameAndUser(newCategory.getCategoryName(), authUserUtil.getAuthUser()).isEmpty()) {
            Category categoryToBeSaved = dtoConversionUtils.convertToEntity(newCategory);
            categoryToBeSaved.setUser(authUserUtil.getAuthUser());
            return dtoConversionUtils
                    .convertToDTO(categoryRepository.save(categoryToBeSaved));
        } else {
            throw new CategoryAlreadyExistsException("Category already exists with the given name");
        }

    }

    @Override
    public Integer deleteCategory(int categoryID) throws CategoryNotFoundException {
        Optional<Category> category = categoryRepository.findByIdAndUser(categoryID, authUserUtil.getAuthUser());
        if (category.isEmpty()) {
            throw new CategoryNotFoundException("No category found with the given ID");
        }
        categoryRepository.delete(category.get());
        return category.get().getId();
    }

    @Override
    public CategoryDTO updateCategory(int categoryID, @Valid @NonNull UpdateCategoryDTO updatedCategoryDTO)
            throws CategoryNotFoundException, CategoryAlreadyExistsException {
        Optional<Category> categoryOptional = categoryRepository.findByIdAndUser(categoryID, authUserUtil.getAuthUser());
        if (categoryOptional.isEmpty()) {
            throw new CategoryNotFoundException("No category found with the given ID");
        }
        Category category = categoryOptional.get();
        if (!category.getCategoryName().equals(updatedCategoryDTO.getCategoryName())) {
            Optional<Category> checkCategory = categoryRepository
                    .findByCategoryNameAndUser(updatedCategoryDTO.getCategoryName(), authUserUtil.getAuthUser());
            if (checkCategory.isPresent() && !checkCategory.get().getId().equals(category.getId())) {
                throw new CategoryAlreadyExistsException("Duplicate category exists with the given name");
            }
        }
        Category updatedCategory;
        if (category.getId() == categoryID && updatedCategoryDTO.getID().equals(category.getId())) {
            category.setCategoryName(updatedCategoryDTO.getCategoryName());
            category.setIconClassName(updatedCategoryDTO.getIconClassName());
            category.setUser(authUserUtil.getAuthUser());
            updatedCategory = categoryRepository.save(category);
            return dtoConversionUtils.convertToDTO(updatedCategory);
        } else {
            throw new CategoryNotFoundException("Please enter valid category details");
        }
    }

    @Override
    public List<CategoryDTO> searchCategory(String searchQueryString) throws InvalidValueException, CategoryNotFoundException {
        if (StringUtils.isBlank(searchQueryString))
            throw new InvalidValueException("Empty search query given.");
        List<Category> searchedCategories = categoryRepository.findByCategoryNameContainingAndUser(searchQueryString, authUserUtil.getAuthUser());
        if (searchedCategories.isEmpty()) throw new CategoryNotFoundException("No Categories found with the query: " + searchQueryString);
        return searchedCategories.stream().map(dtoConversionUtils::convertToDTO).collect(Collectors.toList());
    }

}

And created test for the above class as follows:

@SuppressWarnings("ConstantConditions")
@SpringBootTest
@ActiveProfiles("test")
class CategoryServiceImplTest {

    @Autowired
    private CategoryServiceImpl categoryService;

    @MockBean
    private UserService userService;

    @MockBean
    private CategoryRepository categoryRepository;

    private final LocalDateTime dateTime = LocalDateTime.now();
    private final Role userRole = new Role(1, "ROLE_USER", dateTime, dateTime);
    private final Set<Role> rolesList = new HashSet<>();
    private final UserDTO userDTO = new UserDTO(1, "user1", "First Name", "Last Name", "e@m.com", 10d, 10d);
    private User user;


    @BeforeEach
    void setUp() {
        rolesList.add(userRole);
        user = new User(1,
                "user1",
                "password",
                "First Name",
                "Last Name",
                "e@m.com",
                Money.of(CurrencyUnit.of("INR"), 10),
                Money.of(CurrencyUnit.of("INR"), 10),
                true,
                false,
                false,
                false,
                rolesList,
                dateTime,
                dateTime);

        when(userService.getUserByUserName(Mockito.anyString())).thenReturn(userDTO);
    }

    @Test
    @WithMockUser
    void givenCategoryDTOToSaveShouldReturnCategory() throws CategoryAlreadyExistsException {
        CategoryDTO categoryDTO = new CategoryDTO("Category 1", "Icon 1");
        when(categoryRepository.save(Mockito.any(Category.class)))
                .thenReturn(new Category(1, "Category 1", "Icon 1", null, user, dateTime, dateTime));
        when(userService.getUserByUserName(Mockito.anyString())).thenReturn(userDTO);
        CategoryDTO resultDto = categoryService.saveCategory(categoryDTO);
        CategoryDTO expectedDto = new CategoryDTO(1, "Category 1", "Icon 1");
        assertNotNull(resultDto);
        assertEquals(expectedDto, resultDto);
    }

    @Test
    @WithMockUser
    void givenCategoryWithInvalidUserShouldThrowError() {
        CategoryDTO categoryDTO = new CategoryDTO("Category 1", "Icon 1");
        when(userService.getUserByUserName(Mockito.anyString())).thenThrow(UsernameNotFoundException.class);

        assertThrows(UsernameNotFoundException.class, () -> categoryService.saveCategory(categoryDTO));
    }

    @Test
    void givenCategoryAndNullAsUserToSaveShouldThrowError() {
        CategoryDTO categoryDTO = new CategoryDTO("Category 1", "Icon 1");
        assertThrows(UsernameNotFoundException.class, () -> categoryService.saveCategory(categoryDTO));
    }

    @Test
    @WithMockUser
    void givenDuplicateCategoryToSaveShouldThrowError() {
        Category category = new Category(1, "Category 1", "Icon 1", null, user, dateTime, dateTime);
        when(categoryRepository.findByCategoryNameAndUser(Mockito.anyString(), Mockito.any(User.class))).thenReturn(Optional.of(category));
        CategoryAlreadyExistsException ex = assertThrows(CategoryAlreadyExistsException.class, () -> {
            CategoryDTO categoryDTO = new CategoryDTO("Category 1", "Icon 1");
            categoryService.saveCategory(categoryDTO);
        });
        assertEquals("Category already exists with the given name", ex.getMessage());
    }

    @Test
    void givenCategoryWithInvalidValuesToSaveShouldThrowError() {
        ConstraintViolationException exception = assertThrows(ConstraintViolationException.class, () -> {
            CategoryDTO categoryDTO = new CategoryDTO(null, null, "");
            categoryService.saveCategory(categoryDTO);
        });
        exception.getConstraintViolations().forEach(violation -> assertEquals("must not be blank", violation.getMessage()));
    }

    @Test
    @WithMockUser
    void givenCallToGetAllCategoriesShouldGiveCategoriesList() throws Exception {
        List<CategoryDTO> expectedCategoryDTOs = new ArrayList<>();
        expectedCategoryDTOs.add(new CategoryDTO(1, "Category 1", "Icon 1"));

        List<Category> categories = new ArrayList<>();
        categories.add(new Category(1, "Category 1", "Icon 1", null, user, dateTime, dateTime));

        when(categoryRepository.findAllByUser(Mockito.any(User.class))).thenReturn(categories);

        List<CategoryDTO> resultCategoryDTOs = categoryService.getAllCategories();

        assertEquals(expectedCategoryDTOs, resultCategoryDTOs);
    }

    @Test
    @WithMockUser
    void givenCallToGetAllCategoriesShouldReturnError() {
        when(categoryRepository.findAll()).thenReturn(new ArrayList<>());

        Exception ex = assertThrows(Exception.class, () -> categoryService.getAllCategories());

        assertEquals("No Categories found", ex.getMessage());
    }

    @Test
    @WithMockUser
    void givenIDToGetCategoryShouldReturnCategory() throws CategoryNotFoundException {
        when(categoryRepository.findByIdAndUser(Mockito.anyInt(), Mockito.any(User.class)))
                .thenReturn(Optional.of(new Category(1, "Category 1", "Icon 1", null, user, dateTime, dateTime)));

        CategoryDTO expectedDto = new CategoryDTO(1, "Category 1", "Icon 1");
        CategoryDTO resultDto = categoryService.getCategory(1);

        assertNotNull(resultDto);
        assertEquals(expectedDto, resultDto);
    }

    @Test
    @WithMockUser
    void givenIDToGetCategoryShouldThrowError() {
        when(categoryRepository.findById(Mockito.anyInt())).thenReturn(Optional.empty());

        CategoryNotFoundException ex = assertThrows(CategoryNotFoundException.class, () -> categoryService.getCategory(1));

        assertEquals("Category with ID: 1 not found!", ex.getMessage());
    }

    @Test
    void givenNullToGetCategoryShouldThrowError() {
        assertThrows(NullPointerException.class, () -> categoryService.getCategory((Integer) null));
    }

    @Test
    @WithMockUser
    void givenCategoryNameToGetCategoryShouldReturnCategory() throws CategoryNotFoundException {
        when(categoryRepository.findByCategoryNameAndUser(Mockito.anyString(), Mockito.any(User.class)))
                .thenReturn(Optional.of(new Category(1, "Category 1", "Icon 1", null, user, dateTime, dateTime)));

        CategoryDTO expectedDto = new CategoryDTO(1, "Category 1", "Icon 1");
        CategoryDTO resultDto = categoryService.getCategory("Category 1");

        assertNotNull(resultDto);
        assertEquals(expectedDto, resultDto);
    }

    @Test
    @WithMockUser
    void givenCategoryNameToGetCategoryShouldThrowError() {
        when(categoryRepository.findByCategoryName(Mockito.anyString())).thenReturn(Optional.empty());

        CategoryNotFoundException ex = assertThrows(CategoryNotFoundException.class, () -> categoryService.getCategory("Category 1"));

        assertEquals("No category found with the given name \"Category 1\"", ex.getMessage());
    }

    @Test
    void givenNullAsCategoryNameToGetCategoryShouldThrowError() {
        assertThrows(CategoryNotFoundException.class, () -> categoryService.getCategory(null));
    }

    @Test
    void givenBlankAsCategoryNameToGetCategoryShouldThrowError() {
        CategoryNotFoundException ex = assertThrows(CategoryNotFoundException.class, () -> categoryService.getCategory(""));
        assertEquals("Empty Category Name given", ex.getMessage());
    }

    @Test
    @WithMockUser
    void givenIDToDeleteShouldGiveInteger() throws CategoryNotFoundException {
        when(categoryRepository.findByIdAndUser(Mockito.anyInt(), Mockito.any(User.class)))
                .thenReturn(Optional.of(new Category(1, "Category 1", "Icon 1", null, user, dateTime, dateTime)));

        Integer resultID = categoryService.deleteCategory(1);
        assertEquals(1, resultID);
    }

    @Test
    @WithMockUser
    void givenIDToDeleteShouldThrowError() {
        when(categoryRepository.findById(Mockito.anyInt())).thenReturn(Optional.empty());

        CategoryNotFoundException ex = assertThrows(CategoryNotFoundException.class, () -> categoryService.deleteCategory(1));

        assertEquals("No category found with the given ID", ex.getMessage());
    }

    @Test
    void givenNullAsIDToDeleteShouldThrowError() {
        assertThrows(NullPointerException.class, () -> categoryService.deleteCategory((Integer) null));
    }

    @Test
    @WithMockUser
    void givenIDAndCategoryToUpdateShouldReturnUpdatedCategory() throws CategoryNotFoundException, CategoryAlreadyExistsException {
        Category category = new Category(1, "Category 1", "Icon 1", null, user, dateTime, dateTime);
        CategoryDTO expectedCategoryDTO = new CategoryDTO(1, "Updated Category", "Icon 1");

        when(categoryRepository.findByIdAndUser(Mockito.anyInt(), Mockito.any(User.class))).thenReturn(Optional.of(category));
        when(categoryRepository.findByCategoryNameAndUser(Mockito.anyString(), Mockito.any(User.class))).thenReturn(Optional.empty());
        when(categoryRepository.save(Mockito.any(Category.class)))
                .thenReturn(new Category(1, "Updated Category", "Icon 1", null, user, dateTime, dateTime));

        CategoryDTO resultCategoryDTO = categoryService.updateCategory(1,
                new UpdateCategoryDTO(1, "Updated Category", "Icon 1"));

        assertNotNull(resultCategoryDTO);
        assertEquals(expectedCategoryDTO, resultCategoryDTO);
    }

    @Test
    @WithMockUser
    void givenNonExistentIDAndCategoryToUpdateShouldThrowError() {
        when(categoryRepository.findByIdAndUser(Mockito.anyInt(), Mockito.any(User.class))).thenReturn(Optional.empty());

        CategoryNotFoundException ex = assertThrows(CategoryNotFoundException.class, () -> categoryService.updateCategory(1, new UpdateCategoryDTO(1, "Updated Category", "Icon 1")));

        assertEquals("No category found with the given ID", ex.getMessage());
    }

    @Test
    @WithMockUser
    void givenIDAndDuplicateCategoryToUpdateShouldThrowError() {
        Category category = new Category(1, "Category 1", "Icon 1", null, user, dateTime, dateTime);

        when(categoryRepository.findByIdAndUser(Mockito.anyInt(), Mockito.any(User.class))).thenReturn(Optional.of(category));
        when(categoryRepository.findByCategoryNameAndUser(Mockito.anyString(), Mockito.any(User.class)))
                .thenReturn(Optional.of(new Category(2, "Category 1", "Icon 1", null, user, dateTime, dateTime)));

        CategoryAlreadyExistsException ex = assertThrows(CategoryAlreadyExistsException.class, () -> categoryService.updateCategory(1, new UpdateCategoryDTO(2, "Updated Category", "Icon 1")));

        assertEquals("Duplicate category exists with the given name", ex.getMessage());
    }

    @Test
    @WithMockUser
    void givenMissMatchedIDAndCategoryToUpdateShouldThrowError() {
        Category category = new Category(1, "Category 1", "Icon 1", null, user, dateTime, dateTime);

        when(categoryRepository.findByIdAndUser(Mockito.anyInt(), Mockito.any(User.class))).thenReturn(Optional.of(category));
        when(categoryRepository.findByCategoryNameAndUser(Mockito.anyString(), Mockito.any(User.class))).thenReturn(Optional.empty());

        CategoryNotFoundException ex = assertThrows(CategoryNotFoundException.class, () -> categoryService.updateCategory(1, new UpdateCategoryDTO(2, "Updated Category", "Icon 1")));

        assertEquals("Please enter valid category details", ex.getMessage());
    }

    @Test
    void givenNullIDToUpdateShouldThrowError() {
        assertThrows(NullPointerException.class,
                () -> categoryService.updateCategory((Integer) null, new UpdateCategoryDTO()));
    }

    @Test
    void givenIDAndNullAsUpdatedCategoryToUpdateShouldThrowError() {
        assertThrows(NullPointerException.class, () -> categoryService.updateCategory(1, null));
    }

    @Test
    void givenIDAndCategoryWithNullValuesToUpdateShouldThrowError() {
        ConstraintViolationException ex = assertThrows(ConstraintViolationException.class,
                () -> categoryService.updateCategory(1, new UpdateCategoryDTO(null, null, null)));

        assertEquals(ex.getConstraintViolations().size(), 3);
//      Test with Null ID
        ex = assertThrows(ConstraintViolationException.class,
                () -> categoryService.updateCategory(1, new UpdateCategoryDTO(null, "Update", "Icon")));
        assertEquals(ex.getConstraintViolations().size(), 1);
        ex.getConstraintViolations().forEach(violation -> assertEquals("must not be null", violation.getMessage()));

//      Test with Blank Category Name and Icon name
        ex = assertThrows(ConstraintViolationException.class,
                () -> categoryService.updateCategory(1, new UpdateCategoryDTO(1, "", "  ")));
        assertEquals(ex.getConstraintViolations().size(), 2);
        ex.getConstraintViolations().forEach(violation -> assertEquals("must not be blank", violation.getMessage()));
    }

    @Test
    @WithMockUser
    void givenQueryStringToSearchShouldReturnCategoriesList() throws InvalidValueException, CategoryNotFoundException {
        List<CategoryDTO> expectedCategoryDTOs = new ArrayList<>();
        expectedCategoryDTOs.add(new CategoryDTO(1, "Category 1", "Icon 1"));

        List<Category> categories = new ArrayList<>();
        categories.add(new Category(1, "Category 1", "Icon 1", null, user, dateTime, dateTime));

        when(categoryRepository.findByCategoryNameContainingAndUser(Mockito.anyString(), Mockito.any(User.class))).thenReturn(categories);

        List<CategoryDTO> result = categoryService.searchCategory("a");
        assertFalse(result.isEmpty());
        assertEquals(expectedCategoryDTOs, result);
    }

    @Test
    @WithMockUser
    void givenInvalidQueryStringToSearchShouldThrowError() {
        InvalidValueException exception = assertThrows(InvalidValueException.class, () -> categoryService.searchCategory(""));
        assertEquals("Empty search query given.", exception.getMessage());
        exception = assertThrows(InvalidValueException.class, () -> categoryService.searchCategory("   "));
        assertEquals("Empty search query given.", exception.getMessage());
        exception = assertThrows(InvalidValueException.class, () -> categoryService.searchCategory(null));
        assertEquals("Empty search query given.", exception.getMessage());
    }

    @Test
    @WithMockUser
    void givenQueryStringToSearchShouldThrowError() {
        when(categoryRepository
                .findByCategoryNameContainingAndUser(Mockito.anyString(),
                        Mockito.any(User.class))
        ).thenReturn(new ArrayList<>());

        CategoryNotFoundException categoryNotFoundException = assertThrows(CategoryNotFoundException.class,
                () -> categoryService.searchCategory("a")
        );
        assertEquals("No Categories found with the query: a", categoryNotFoundException.getMessage());
    }
}

I've written test cases for both authorized and unauthorized users. But when I run the test class, the tests for unauthorized users fail.

For test methods givenCategoryWithInvalidUserShouldThrowError() and givenCategoryAndNullAsUserToSaveShouldThrowError() I get the below error message.

org.opentest4j.AssertionFailedError: Unexpected exception type thrown ==> expected: <org.springframework.security.core.userdetails.UsernameNotFoundException> but was: <java.lang.IllegalArgumentException>

    at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:65)
    at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:37)
    at org.junit.jupiter.api.Assertions.assertThrows(Assertions.java:2952)
    at com.bsd.dailyexpensetracker.service.CategoryServiceImplTest.givenCategoryWithInvalidUserShouldThrowError(CategoryServiceImplTest.java:109)

But when I run the failed test methods(givenCategoryWithInvalidUserShouldThrowError() and givenCategoryAndNullAsUserToSaveShouldThrowError()) induvidually, they are getting passed.

So I tried to debug the test class and found out the SecurityContextHolder is not getting cleared or reset after every test method eventhough I use @WithMockUser for only some test methods. And because of this the helper method in AuthUserUtils was not throwing any expected UsernameNotFoundExceptions and continued to execute which lead to unexpected exceptions. I placed the @WithMockUser annotation on a single test method and removed it from other test methods and executed the test class, the SecurityContext was still getting applied to all the test methods.

This occurs only when I run the whole test class. When I run induvidual test cases, they are executing as expected i.e the SecurityContextHolder was getting set only when required.

When I put the helper method in the Service class with all the required dependencies, and run the test class, all the tests are getting passed.

So is there something mising in my Util class or did I configure my test class wrong?

Aucun commentaire:

Enregistrer un commentaire