diff --git a/backend/src/main/java/ch/puzzle/okr/controller/UserController.java b/backend/src/main/java/ch/puzzle/okr/controller/UserController.java index bc6d772a9a..229aee6871 100644 --- a/backend/src/main/java/ch/puzzle/okr/controller/UserController.java +++ b/backend/src/main/java/ch/puzzle/okr/controller/UserController.java @@ -2,6 +2,7 @@ import ch.puzzle.okr.dto.NewUserDto; import ch.puzzle.okr.dto.UserDto; +import ch.puzzle.okr.dto.userOkrData.UserOkrDataDto; import ch.puzzle.okr.mapper.UserMapper; import ch.puzzle.okr.service.authorization.AuthorizationService; import ch.puzzle.okr.service.authorization.UserAuthorizationService; @@ -11,13 +12,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.util.List; @@ -85,4 +80,33 @@ public List createUsers( return userMapper.toDtos(createdUsers); } + @Operation(summary = "Check if User is member of Teams", description = "Check if User is member of any Team.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "true if user is member of a Team", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = Boolean.class)) }), }) + + @GetMapping(path = "/{id}/ismemberofteams") + public Boolean isUserMemberOfTeams( + @Parameter(description = "The ID of the user.", required = true) @PathVariable long id) { + + return this.userAuthorizationService.isUserMemberOfTeams(id); + } + + @Operation(summary = "Get User OKR Data", description = "Get User OKR Data") + @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Returned User OKR Data.", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = UserOkrDataDto.class)) }), }) + @GetMapping(path = "/{id}/userokrdata") + public UserOkrDataDto getUserOkrData(@PathVariable long id) { + return this.userAuthorizationService.getUserOkrData(id); + } + + @Operation(summary = "Delete User by Id", description = "Delete User by Id") + @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Deleted User by Id"), + @ApiResponse(responseCode = "401", description = "Not authorized to delete a User", content = @Content), + @ApiResponse(responseCode = "404", description = "Did not find the User with requested id") }) + @DeleteMapping(path = "/{id}") + public void deleteUserById(@PathVariable long id) { + this.userAuthorizationService.deleteEntityById(id); + } + } diff --git a/backend/src/main/java/ch/puzzle/okr/dto/userOkrData/UserKeyResultDataDto.java b/backend/src/main/java/ch/puzzle/okr/dto/userOkrData/UserKeyResultDataDto.java new file mode 100644 index 0000000000..c84be731b5 --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/dto/userOkrData/UserKeyResultDataDto.java @@ -0,0 +1,4 @@ +package ch.puzzle.okr.dto.userOkrData; + +public record UserKeyResultDataDto(Long keyResultId, String keyResultName, Long objectiveId, String objectiveName) { +} diff --git a/backend/src/main/java/ch/puzzle/okr/dto/userOkrData/UserOkrDataDto.java b/backend/src/main/java/ch/puzzle/okr/dto/userOkrData/UserOkrDataDto.java new file mode 100644 index 0000000000..0f7f06146a --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/dto/userOkrData/UserOkrDataDto.java @@ -0,0 +1,6 @@ +package ch.puzzle.okr.dto.userOkrData; + +import java.util.List; + +public record UserOkrDataDto(List keyResults) { +} diff --git a/backend/src/main/java/ch/puzzle/okr/mapper/UserOkrDataMapper.java b/backend/src/main/java/ch/puzzle/okr/mapper/UserOkrDataMapper.java new file mode 100644 index 0000000000..72b78d772d --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/mapper/UserOkrDataMapper.java @@ -0,0 +1,25 @@ +package ch.puzzle.okr.mapper; + +import ch.puzzle.okr.dto.userOkrData.UserKeyResultDataDto; +import ch.puzzle.okr.dto.userOkrData.UserOkrDataDto; +import ch.puzzle.okr.models.keyresult.KeyResult; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class UserOkrDataMapper { + + public UserOkrDataDto toDto(List keyResults) { + return new UserOkrDataDto(toUserKeyResultDataDtos(keyResults)); + } + + private List toUserKeyResultDataDtos(List keyResults) { + return keyResults.stream() // + .map(keyResult -> new UserKeyResultDataDto( // + keyResult.getId(), keyResult.getTitle(), // + keyResult.getObjective().getId(), keyResult.getObjective().getTitle() // + )) // + .toList(); + } +} diff --git a/backend/src/main/java/ch/puzzle/okr/service/authorization/UserAuthorizationService.java b/backend/src/main/java/ch/puzzle/okr/service/authorization/UserAuthorizationService.java index 2cd0a29259..fab3dba8fe 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/authorization/UserAuthorizationService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/authorization/UserAuthorizationService.java @@ -1,9 +1,13 @@ package ch.puzzle.okr.service.authorization; import ch.puzzle.okr.ErrorKey; +import ch.puzzle.okr.dto.userOkrData.UserOkrDataDto; import ch.puzzle.okr.exception.OkrResponseStatusException; +import ch.puzzle.okr.mapper.UserOkrDataMapper; import ch.puzzle.okr.models.User; import ch.puzzle.okr.models.UserTeam; +import ch.puzzle.okr.models.keyresult.KeyResult; +import ch.puzzle.okr.service.business.KeyResultBusinessService; import ch.puzzle.okr.service.business.UserBusinessService; import org.springframework.stereotype.Service; @@ -17,12 +21,14 @@ public class UserAuthorizationService { private final AuthorizationService authorizationService; private final TeamAuthorizationService teamAuthorizationService; + private final KeyResultBusinessService keyResultBusinessService; public UserAuthorizationService(UserBusinessService userBusinessService, AuthorizationService authorizationService, - TeamAuthorizationService teamAuthorizationService) { + TeamAuthorizationService teamAuthorizationService, KeyResultBusinessService keyResultBusinessService) { this.userBusinessService = userBusinessService; this.authorizationService = authorizationService; this.teamAuthorizationService = teamAuthorizationService; + this.keyResultBusinessService = keyResultBusinessService; } public List getAllUsers() { @@ -58,4 +64,21 @@ public List createUsers(List userList) { OkrResponseStatusException.of(ErrorKey.NOT_AUTHORIZED_TO_WRITE, USER)); return userBusinessService.createUsers(userList); } + + public boolean isUserMemberOfTeams(long id) { + List userTeamList = userBusinessService.getUserById(id).getUserTeamList(); + return userTeamList != null && !userTeamList.isEmpty(); + } + + public void deleteEntityById(long id) { + AuthorizationService.checkRoleWriteAndReadAll(authorizationService.updateOrAddAuthorizationUser(), + OkrResponseStatusException.of(ErrorKey.NOT_AUTHORIZED_TO_DELETE, USER)); + + userBusinessService.deleteEntityById(id); + } + + public UserOkrDataDto getUserOkrData(long id) { + List keyResultsOwnedByUser = keyResultBusinessService.getKeyResultsOwnedByUser(id); + return new UserOkrDataMapper().toDto(keyResultsOwnedByUser); + } } diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/KeyResultBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/KeyResultBusinessService.java index 309a09d524..3d2fcf802e 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/KeyResultBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/KeyResultBusinessService.java @@ -126,4 +126,8 @@ public boolean isImUsed(Long id, KeyResult keyResult) { private boolean isKeyResultTypeChangeable(Long id) { return !hasKeyResultAnyCheckIns(id); } + + public List getKeyResultsOwnedByUser(long id) { + return keyResultPersistenceService.getKeyResultsOwnedByUser(id); + } } diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/UserBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/UserBusinessService.java index 6155e4fd0d..cfb7d3901e 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/UserBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/UserBusinessService.java @@ -69,4 +69,10 @@ public List createUsers(List userList) { var userIter = userPersistenceService.saveAll(userList); return StreamSupport.stream(userIter.spliterator(), false).toList(); } + + @Transactional + public void deleteEntityById(long id) { + validationService.validateOnDelete(id); + userPersistenceService.deleteById(id); + } } diff --git a/backend/src/main/java/ch/puzzle/okr/service/persistence/KeyResultPersistenceService.java b/backend/src/main/java/ch/puzzle/okr/service/persistence/KeyResultPersistenceService.java index 14e223bff5..ad2358588f 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/persistence/KeyResultPersistenceService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/persistence/KeyResultPersistenceService.java @@ -38,4 +38,18 @@ public KeyResult recreateEntity(Long id, KeyResult keyResult) { public KeyResult updateEntity(KeyResult keyResult) { return save(keyResult); } + + public boolean isUserOwnerOfKeyResults(long id) { + List allKeyResults = findAll(); + long numberOfKeyResultsOfUser = allKeyResults.stream() + .filter(keyResult -> keyResult.getOwner().getId().equals(id)).count(); + return numberOfKeyResultsOfUser > 0; + } + + public List getKeyResultsOwnedByUser(long userId) { + return findAll().stream() // + .filter(keyResult -> keyResult.getOwner().getId().equals(userId)) // + .toList(); + } + } diff --git a/backend/src/test/java/ch/puzzle/okr/controller/UserControllerIT.java b/backend/src/test/java/ch/puzzle/okr/controller/UserControllerIT.java index 33d0af9aac..6e6b7b0059 100644 --- a/backend/src/test/java/ch/puzzle/okr/controller/UserControllerIT.java +++ b/backend/src/test/java/ch/puzzle/okr/controller/UserControllerIT.java @@ -9,6 +9,7 @@ import org.hamcrest.Matchers; import org.hamcrest.core.Is; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.BDDMockito; @@ -16,11 +17,13 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.springframework.web.server.ResponseStatusException; import java.util.ArrayList; import java.util.Arrays; @@ -29,6 +32,7 @@ import static ch.puzzle.okr.controller.ActionControllerIT.SUCCESSFUL_UPDATE_BODY; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -163,4 +167,19 @@ void shouldCreateUsers() throws Exception { .andExpect(jsonPath("$[0].isOkrChampion", Is.is(false))); } + @Test + void shouldDeleteUser() throws Exception { + mvc.perform(delete("/api/v1/users/10").with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(MockMvcResultMatchers.status().isOk()); + } + + @DisplayName("should throw exception when user with id cant be found while deleting") + @Test + void throwExceptionWhenUserWithIdCantBeFoundWhileDeleting() throws Exception { + doThrow(new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found")).when(userAuthorizationService) + .deleteEntityById(1000); + + mvc.perform(delete("/api/v1/users/1000").with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(MockMvcResultMatchers.status().isNotFound()); + } } \ No newline at end of file diff --git a/backend/src/test/java/ch/puzzle/okr/service/authorization/UserAuthorizationServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/authorization/UserAuthorizationServiceTest.java index 8575cdd561..73af1ff069 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/authorization/UserAuthorizationServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/authorization/UserAuthorizationServiceTest.java @@ -4,6 +4,7 @@ import ch.puzzle.okr.models.User; import ch.puzzle.okr.models.authorization.AuthorizationUser; import ch.puzzle.okr.service.business.UserBusinessService; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -106,4 +107,35 @@ void createUsers_shouldThrowErrorIfLoggedInUserIsNotOkrChampion() { assertThrows(OkrResponseStatusException.class, () -> userAuthorizationService.createUsers(List.of(user, user2))); } + + @DisplayName("isUserMemberOfTeams() should return false if user is not member of teams") + @Test + void isUserMemberOfTeamsShouldReturnFalseIfUserIsNotMemberOfTeams() { + // arrange + Long userId = 1L; + User userWithoutTeams = defaultUser(userId); + when(userBusinessService.getUserById(userId)).thenReturn(userWithoutTeams); + + // act + boolean isUserMemberOfTeams = userAuthorizationService.isUserMemberOfTeams(1L); + + // assert + assertFalse(isUserMemberOfTeams); + } + + @DisplayName("isUserMemberOfTeams() should return true if user is member of teams") + @Test + void isUserMemberOfTeamsShouldReturnTrueIfUserIsMemberOfTeams() { + // arrange + User userWithTeams = user2; + Long userId = user2.getId(); + when(userBusinessService.getUserById(userId)).thenReturn(userWithTeams); + + // act + boolean isUserMemberOfTeams = userAuthorizationService.isUserMemberOfTeams(userId); + + // assert + assertTrue(isUserMemberOfTeams); + } + } diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/UserBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/UserBusinessServiceTest.java index 29eb1f74af..2362a2b6cd 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/UserBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/UserBusinessServiceTest.java @@ -38,11 +38,19 @@ class UserBusinessServiceTest { @BeforeEach void setUp() { - User userAlice = User.Builder.builder().withId(2L).withFirstname("Alice").withLastname("Wunderland") - .withEmail("wunderland@puzzle.ch").build(); - - User userBob = User.Builder.builder().withId(9L).withFirstname("Bob").withLastname("Baumeister") - .withEmail("baumeister@puzzle.ch").build(); + User userAlice = User.Builder.builder() // + .withId(2L) // + .withFirstname("Alice") // + .withLastname("Wunderland") // + .withEmail("wunderland@puzzle.ch") // + .build(); + + User userBob = User.Builder.builder() // + .withId(9L) // + .withFirstname("Bob") // + .withLastname("Baumeister") // + .withEmail("baumeister@puzzle.ch") // + .build(); userList = Arrays.asList(userAlice, userBob); } @@ -120,9 +128,8 @@ void getOrCreateUserShouldThrowResponseStatusExceptionWhenInvalidUser() { Mockito.doThrow(new ResponseStatusException(HttpStatus.BAD_REQUEST, "Not allowed to give an id")) .when(validationService).validateOnGetOrCreate(newUser); - ResponseStatusException exception = assertThrows(ResponseStatusException.class, () -> { - userBusinessService.getOrCreateUser(newUser); - }); + ResponseStatusException exception = assertThrows(ResponseStatusException.class, + () -> userBusinessService.getOrCreateUser(newUser)); assertEquals(HttpStatus.BAD_REQUEST, exception.getStatusCode()); assertEquals("Not allowed to give an id", exception.getReason()); @@ -162,4 +169,11 @@ void setOkrChampion_shouldNotThrowExceptionIfSecondLastOkrChampIsRemoved() { verify(userPersistenceService, times(1)).save(user); assertFalse(user.isOkrChampion()); } + + @Test + void shouldDeleteUser() { + userBusinessService.deleteEntityById(23L); + + verify(userPersistenceService, times(1)).deleteById(23L); + } } diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/UserPersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/UserPersistenceServiceIT.java index 0104060b48..21449511d8 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/UserPersistenceServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/UserPersistenceServiceIT.java @@ -1,5 +1,6 @@ package ch.puzzle.okr.service.persistence; +import ch.puzzle.okr.exception.OkrResponseStatusException; import ch.puzzle.okr.models.User; import ch.puzzle.okr.multitenancy.TenantContext; import ch.puzzle.okr.test.SpringIntegrationTest; @@ -8,6 +9,9 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; import java.util.List; import java.util.Optional; @@ -178,4 +182,42 @@ private void assertUser(String firstName, String lastName, String email, User cu assertEquals(lastName, currentUser.getLastname()); assertEquals(email, currentUser.getEmail()); } + + @DisplayName("deleteById() should delete user when user found") + @Test + void deleteByIdShouldDeleteUserWhenUserFound() { + // arrange + User user = createUser(); + + // act + userPersistenceService.deleteById(user.getId()); + + // assert + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, // + () -> userPersistenceService.findById(createdUser.getId())); + + assertEquals(HttpStatus.NOT_FOUND, exception.getStatusCode()); + } + + private User createUser() { + User newUser = User.Builder.builder() // + .withId(null) // + .withFirstname("firstname") // + .withLastname("lastname") // + .withEmail("lastname@puzzle.ch") // + .build(); + createdUser = userPersistenceService.getOrCreateUser(newUser); + assertNotNull(createdUser.getId()); + return createdUser; + } + + @DisplayName("deleteById() should throw exception when Id is null") + @Test + void deleteByIdShouldThrowExceptionWhenIdIsNull() { + InvalidDataAccessApiUsageException exception = assertThrows(InvalidDataAccessApiUsageException.class, // + () -> userPersistenceService.deleteById(null)); + + assertEquals("The given id must not be null", exception.getMessage()); + } + } \ No newline at end of file