diff --git a/src/main/java/net/szymonsawicki/reactivetimesheetapp/application/service/TeamService.java b/src/main/java/net/szymonsawicki/reactivetimesheetapp/application/service/TeamService.java index c222ff7..1ac4e69 100644 --- a/src/main/java/net/szymonsawicki/reactivetimesheetapp/application/service/TeamService.java +++ b/src/main/java/net/szymonsawicki/reactivetimesheetapp/application/service/TeamService.java @@ -10,6 +10,7 @@ import net.szymonsawicki.reactivetimesheetapp.domain.team.dto.GetTeamDto; import net.szymonsawicki.reactivetimesheetapp.domain.team.repository.TeamRepository; import net.szymonsawicki.reactivetimesheetapp.domain.user.repository.UserRepository; import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @Service @@ -20,28 +21,30 @@ public class TeamService { private final TeamRepository teamRepository; private final UserRepository userRepository; + public Flux findAllTeams() { + return teamRepository.findAll() + .flatMap(team -> Flux.just(team.toGetTeamDto())); + } + public Mono findById(String teamId) { return teamRepository.findById(teamId) .map(Team::toGetTeamDto) - .switchIfEmpty(Mono.error(new TeamServiceException("id doesn't exist"))); + .switchIfEmpty(Mono.error(new TeamServiceException("Team with given id doesn't exist"))); } public Mono findByName(String name) { return teamRepository.findByName(name) .map(Team::toGetTeamDto) - .switchIfEmpty(Mono.error(new TeamServiceException("Username doesn't exist"))); + .switchIfEmpty(Mono.error(new TeamServiceException("Team with given name doesn't exist"))); } public Mono addTeam(Mono createTeamDtoMono) { return createTeamDtoMono .flatMap(createTeamDto -> teamRepository.findByName(createTeamDto.name()) - .map(team -> { - log.error("Team with name " + createTeamDto.name() + " already exists"); - return team.toGetTeamDto(); - }) - .switchIfEmpty(createTeamWithMembers(createTeamDto)) - ); + .doOnEach(team -> log.error("Team with name " + createTeamDto.name() + " already exists")) + .map(Team::toGetTeamDto) + .switchIfEmpty(Mono.defer(() -> createTeamWithMembers(createTeamDto)))); } private Mono createTeamWithMembers(CreateTeamDto createTeamDto) { diff --git a/src/main/java/net/szymonsawicki/reactivetimesheetapp/domain/team/Team.java b/src/main/java/net/szymonsawicki/reactivetimesheetapp/domain/team/Team.java index 9b3bd89..f0353fe 100644 --- a/src/main/java/net/szymonsawicki/reactivetimesheetapp/domain/team/Team.java +++ b/src/main/java/net/szymonsawicki/reactivetimesheetapp/domain/team/Team.java @@ -32,7 +32,7 @@ public class Team { return TeamEntity.builder() .id(id) .name(name) - .members(members) + .members(members.stream().map(User::toEntity).toList()) .build(); } diff --git a/src/main/java/net/szymonsawicki/reactivetimesheetapp/domain/user/User.java b/src/main/java/net/szymonsawicki/reactivetimesheetapp/domain/user/User.java index d788d75..4f4f65c 100644 --- a/src/main/java/net/szymonsawicki/reactivetimesheetapp/domain/user/User.java +++ b/src/main/java/net/szymonsawicki/reactivetimesheetapp/domain/user/User.java @@ -45,6 +45,6 @@ public class User { id, username, password, - role); + role, teamId); } } diff --git a/src/main/java/net/szymonsawicki/reactivetimesheetapp/domain/user/dto/GetUserDto.java b/src/main/java/net/szymonsawicki/reactivetimesheetapp/domain/user/dto/GetUserDto.java index 56422a1..6d569ef 100644 --- a/src/main/java/net/szymonsawicki/reactivetimesheetapp/domain/user/dto/GetUserDto.java +++ b/src/main/java/net/szymonsawicki/reactivetimesheetapp/domain/user/dto/GetUserDto.java @@ -3,13 +3,14 @@ package net.szymonsawicki.reactivetimesheetapp.domain.user.dto; import net.szymonsawicki.reactivetimesheetapp.domain.user.User; import net.szymonsawicki.reactivetimesheetapp.domain.user.type.Role; -public record GetUserDto(String id, String username, String password, Role role) { +public record GetUserDto(String id, String username, String password, Role role, String teamId) { public User toUser() { return User.builder() .id(id) .username(username) .password(password) .role(role) + .teamId(teamId) .build(); } } diff --git a/src/main/java/net/szymonsawicki/reactivetimesheetapp/infrastructure/persistence/entity/TeamEntity.java b/src/main/java/net/szymonsawicki/reactivetimesheetapp/infrastructure/persistence/entity/TeamEntity.java index ebdff0c..4cbd984 100644 --- a/src/main/java/net/szymonsawicki/reactivetimesheetapp/infrastructure/persistence/entity/TeamEntity.java +++ b/src/main/java/net/szymonsawicki/reactivetimesheetapp/infrastructure/persistence/entity/TeamEntity.java @@ -22,13 +22,13 @@ public class TeamEntity { String id; String name; - List members; + List members; public Team toTeam() { return Team.builder() .id(id) .name(name) - .members(members) + .members(members.stream().map(UserEntity::toUser).toList()) .build(); } } diff --git a/src/main/java/net/szymonsawicki/reactivetimesheetapp/web/handler/TeamHandlers.java b/src/main/java/net/szymonsawicki/reactivetimesheetapp/web/handler/TeamHandlers.java index c391034..df5eee9 100644 --- a/src/main/java/net/szymonsawicki/reactivetimesheetapp/web/handler/TeamHandlers.java +++ b/src/main/java/net/szymonsawicki/reactivetimesheetapp/web/handler/TeamHandlers.java @@ -1,13 +1,18 @@ package net.szymonsawicki.reactivetimesheetapp.web.handler; +import jdk.jfr.ContentType; import lombok.RequiredArgsConstructor; import net.szymonsawicki.reactivetimesheetapp.application.service.TeamService; import net.szymonsawicki.reactivetimesheetapp.domain.team.dto.CreateTeamDto; +import net.szymonsawicki.reactivetimesheetapp.domain.team.dto.GetTeamDto; import net.szymonsawicki.reactivetimesheetapp.web.config.GlobalRoutingHandler; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @Component @@ -15,6 +20,13 @@ import reactor.core.publisher.Mono; public class TeamHandlers { private final TeamService teamService; + public Mono findAllTeams(ServerRequest serverRequest) { + return ServerResponse + .status(HttpStatus.OK) + .contentType(MediaType.APPLICATION_JSON) + .body(teamService.findAllTeams(),GetTeamDto.class); + } + public Mono findById(ServerRequest serverRequest) { var teamId = serverRequest.pathVariable("id"); return GlobalRoutingHandler.doRequest(teamService.findById(teamId), HttpStatus.OK); diff --git a/src/test/java/net/szymonsawicki/reactivetimesheetapp/application/service/TeamServiceTest.java b/src/test/java/net/szymonsawicki/reactivetimesheetapp/application/service/TeamServiceTest.java new file mode 100644 index 0000000..4440135 --- /dev/null +++ b/src/test/java/net/szymonsawicki/reactivetimesheetapp/application/service/TeamServiceTest.java @@ -0,0 +1,452 @@ +package net.szymonsawicki.reactivetimesheetapp.application.service; + +import net.szymonsawicki.reactivetimesheetapp.application.service.exception.TeamServiceException; +import net.szymonsawicki.reactivetimesheetapp.domain.team.Team; +import net.szymonsawicki.reactivetimesheetapp.domain.team.TeamUtils; +import net.szymonsawicki.reactivetimesheetapp.domain.team.dto.CreateTeamDto; +import net.szymonsawicki.reactivetimesheetapp.domain.team.dto.GetTeamDto; +import net.szymonsawicki.reactivetimesheetapp.domain.team.repository.TeamRepository; +import net.szymonsawicki.reactivetimesheetapp.domain.user.User; +import net.szymonsawicki.reactivetimesheetapp.domain.user.UserUtils; +import net.szymonsawicki.reactivetimesheetapp.domain.user.dto.GetUserDto; +import net.szymonsawicki.reactivetimesheetapp.domain.user.repository.UserRepository; +import net.szymonsawicki.reactivetimesheetapp.domain.user.type.Role; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InOrder; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Bean; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.List; + +@ExtendWith(SpringExtension.class) +public class TeamServiceTest { + + @TestConfiguration + public static class TeamServiceTestConfiguration { + + @MockBean + public TeamRepository teamRepository; + @MockBean + public UserRepository userRepository; + + @Bean + public TeamService teamService() { + return new TeamService(teamRepository, userRepository); + } + } + + @Autowired + public TeamRepository teamRepository; + @Autowired + public UserRepository userRepository; + + @Autowired + public TeamService teamService; + + @Captor + public ArgumentCaptor> usersCaptor; + + @Test + public void shouldReturnThreeTeamsOnFindAll() { + + String userId1 = "21344r23r34"; + String userId2 = "21344r23r34"; + String teamId1 = "2r872394r578"; + String teamId2 = "2r872394r578"; + String teamName1 = "Some team"; + String teamName2 = "Some team"; + String username1 = "testsusrname"; + String username2 = "testsusrname2"; + + var member1 = new GetUserDto( + userId1, + username1, + "some password", + null, teamId1); + + var member2 = new GetUserDto( + userId2, + username2, + "some password", + null, teamId2); + + var expectedTeam1 = new GetTeamDto( + teamId1, + teamName1, + List.of(member1)); + + var expectedTeam2 = new GetTeamDto( + teamId2, + teamName2, + List.of(member2)); + + var memberFromDb1 = User.builder() + .id(userId1) + .username(username1) + .password("some password") + .teamId(teamId1) + .build(); + + var memberFromDb2 = User.builder() + .id(userId2) + .username(username2) + .password("some password") + .teamId(teamId2) + .build(); + + var teamFromDb1 = Team.builder() + .id(teamId1) + .name(teamName1) + .members(List.of(memberFromDb1)) + .build(); + + var teamFromDb2 = Team.builder() + .id(teamId2) + .name(teamName2) + .members(List.of(memberFromDb2)) + .build(); + + Mockito.when(teamRepository.findAll()) + .thenReturn(Flux.just(teamFromDb1,teamFromDb2)); + + StepVerifier + .create(teamService.findAllTeams()) + .expectNext(expectedTeam1) + .expectNext(expectedTeam2) + .verifyComplete(); + } + + @Test + public void shouldReturnTeamWithOneMemberOnGetById() { + + String userId = "21344r23r34"; + String teamId = "2r872394r578"; + String teamName = "Some team"; + String username = "testsusrname"; + Role role = Role.DEVELOPER; + + var member = User.builder() + .username(username) + .password("some password") + .teamId(teamId) + .build(); + + var teamEntityMono = Mono.just(Team.builder() + .id(teamId) + .name(teamName) + .members(List.of(member)) + .build()); + + Mockito.when(teamRepository.findById(Mockito.anyString())) + .thenReturn(teamEntityMono); + + StepVerifier + .create(teamService.findById(userId)) + .assertNext(team -> { + Assertions.assertThat(team.name().equals(teamName)); + Assertions.assertThat(team.members()).hasSize(1); + Assertions.assertThat(team.members().get(0).username()).isEqualTo(username); + }) + .verifyComplete(); + } + + @Test + public void shouldReturnErrorOnGetByIdWhenTeamMissing() { + + Mockito.when(teamRepository.findById(Mockito.anyString())) + .thenReturn(Mono.empty()); + + StepVerifier + .create(teamService.findById("some id")) + .expectErrorMatches(error -> error instanceof TeamServiceException && error.getMessage().equals("Team with given id doesn't exist")) + .verify(); + } + + @Test + public void shouldFindTeamByName() { + + String userId = "21344r23r34"; + String teamId = "2r872394r578"; + String teamName = "Some team"; + String username = "testsusrname"; + + var member = User.builder() + .id(userId) + .username(username) + .password("some password") + .teamId(teamId) + .build(); + + var expectedMemberDto = new GetUserDto( + userId, + username, + "some password", + null, teamId); + + + var teamMono = Mono.just(Team.builder() + .id(teamId) + .name(teamName) + .members(List.of(member)) + .build()); + + Mockito.when(teamRepository.findByName(Mockito.anyString())) + .thenReturn(teamMono); + + StepVerifier + .create(teamService.findByName(teamName)) + .expectNextMatches(resultingTeam -> resultingTeam.name().equals(teamName)) + .verifyComplete(); + } + + @Test + public void shouldReturnErrorOnGetTeamByNameWhenMissing() { + + Mockito.when(teamRepository.findByName(Mockito.anyString())) + .thenReturn(Mono.empty()); + + StepVerifier + .create(teamService.findByName("some name")) + .expectErrorMatches(error -> error instanceof TeamServiceException && error.getMessage().equals("Team with given name doesn't exist")) + .verify(); + } + + @Test + public void shouldCreateNewTeamWithMembers() { + + String userId1 = "21344r23r34"; + String userId2 = "21344r23r34"; + String teamId = "2r872394r578"; + String teamName = "Some team"; + String username1 = "testsusrname"; + String username2 = "testsusrname2"; + + ArgumentCaptor saveTeamCaptor = ArgumentCaptor.forClass(Team.class); + + var member1 = new GetUserDto( + userId1, + username1, + "some password", + null, null); + + var member2 = new GetUserDto( + userId2, + username2, + "some password", + null, null); + + var savedMember1 = User.builder() + .id(userId1) + .username(username1) + .password("some password") + .teamId(teamId) + .build(); + + var savedMember2 = User.builder() + .id(userId2) + .username(username2) + .password("some password") + .teamId(teamId) + .build(); + + var teamToCreateMono = Mono.just(new CreateTeamDto( + teamName + , List.of(member1, member2))); + + var createdTeamMono = Mono.just(Team.builder() + .id(teamId) + .name(teamName) + .members(List.of(savedMember1, savedMember2)) + .build()); + + Mockito.when(teamRepository.findByName(Mockito.anyString())) + .thenReturn(Mono.empty()); + + Mockito.when(teamRepository.save(saveTeamCaptor.capture())) + .thenReturn(createdTeamMono); + + Mockito.when(userRepository.saveAll(usersCaptor.capture())) + .thenReturn(Flux.just(savedMember1, savedMember2)); + + StepVerifier + .create(teamService.addTeam(teamToCreateMono)) + .assertNext(team -> { + Assertions.assertThat(team.name()).isEqualTo(teamName); + Assertions.assertThat(team.members()).hasSize(2); + Assertions.assertThat(team.members().stream().map(GetUserDto::username).toList()).containsAll(List.of(username1, username2)); + Assertions.assertThat(team.members().stream().filter(member -> !member.teamId().equals(teamId)).toList()).isEmpty(); + // captor assertions + Assertions.assertThat(TeamUtils.toMembers.apply(saveTeamCaptor.getValue())).hasSize(2); + Assertions.assertThat(TeamUtils.toId.apply(saveTeamCaptor.getValue())).isEqualTo(teamId); + Assertions.assertThat(usersCaptor.getValue()).hasSize(2); + }) + .verifyComplete(); + + InOrder inOrder = Mockito.inOrder(teamRepository, userRepository); + + inOrder.verify(teamRepository, Mockito.times(1)).findByName(teamName); + inOrder.verify(teamRepository, Mockito.times(1)).save(Mockito.any(Team.class)); + inOrder.verify(userRepository, Mockito.times(1)).saveAll(Mockito.any()); + inOrder.verify(teamRepository, Mockito.times(1)).save(Mockito.any(Team.class)); + + Mockito.verifyNoMoreInteractions(teamRepository, userRepository); + } + + @Test + public void shouldReturnExistingTeamWhenNameIsTakenOnAddTeam() { + + String userId1 = "21344r23r34"; + String userId2 = "tzuh"; + String teamId = "2r872394r578"; + String teamName = "Some team"; + String username1 = "testsusrname"; + String username2 = "testsusrname2"; + + var member1 = new GetUserDto( + userId1, + username1, + "some password", + null, null); + + var member2 = new GetUserDto( + userId2, + username2, + "some password", + null, null); + + var expectedMember1 = new GetUserDto( + userId1, + username1, + "some password", + null, teamId); + + var expectedMember2 = new GetUserDto( + userId2, + username2, + "some password", + null, teamId); + + var savedMember1 = User.builder() + .id(userId1) + .username(username1) + .password("some password") + .teamId(teamId) + .build(); + + var savedMember2 = User.builder() + .id(userId2) + .username(username2) + .password("some password") + .teamId(teamId) + .build(); + + var teamToCreateMono = Mono.just(new CreateTeamDto( + teamName + , List.of(member1, member2))); + + var expectedTeamDto = new GetTeamDto( + teamId, + teamName + , List.of(expectedMember1, expectedMember2)); + + var existingTeamMono = Mono.just(Team.builder() + .id(teamId) + .name(teamName) + .members(List.of(savedMember1, savedMember2)) + .build()); + + Mockito.when(teamRepository.findByName(Mockito.anyString())) + .thenReturn(existingTeamMono); + + StepVerifier + .create(teamService.addTeam(teamToCreateMono)) + .expectNext(expectedTeamDto) + .verifyComplete(); + + Mockito.verify(teamRepository, Mockito.never()) + .save(Mockito.any()); + Mockito.verify(userRepository, Mockito.never()) + .saveAll(Mockito.any()); + } + + @Test + public void ShouldDeleteTeamOnDelete() { + + String userId1 = "21344r23r34"; + String userId2 = "21344r23r34"; + String teamId = "2r872394r578"; + String teamName = "Some team"; + String username1 = "testsusrname"; + String username2 = "testsusrname2"; + + var existingMember1 = User.builder() + .id(userId1) + .username(username1) + .password("some password") + .teamId(teamId) + .build(); + + var existingMember2 = User.builder() + .id(userId2) + .username(username2) + .password("some password") + .teamId(teamId) + .build(); + + var existingTeam = Team.builder() + .id(teamId) + .name(teamName) + .members(List.of(existingMember1,existingMember2)) + .build(); + + Mockito.when(teamRepository.findById(Mockito.anyString())) + .thenReturn(Mono.just(existingTeam)); + + Mockito.when(userRepository.saveAll(usersCaptor.capture())) + .thenReturn(Flux.just(existingMember1,existingMember2)); + + Mockito.when(teamRepository.delete(Mockito.anyString())) + .thenReturn(Mono.empty()); + + StepVerifier + .create(teamService.deleteTeam(teamId)) + .assertNext(team -> { + Assertions.assertThat(team.name()).isEqualTo(teamName); + Assertions.assertThat(team.members()).hasSize(2); + Assertions.assertThat(team.members().stream().map(GetUserDto::username).toList()).containsAll(List.of(username1, username2)); + Assertions.assertThat(team.members().stream().filter(member -> !member.teamId().equals(teamId)).toList()).isEmpty(); + // captor assertions + Assertions.assertThat(usersCaptor.getValue()).hasSize(2); + Assertions.assertThat(usersCaptor.getValue().stream().filter(user -> UserUtils.toTeamId.apply(user) != null).toList()).isEmpty(); + }) + .verifyComplete(); + + Mockito.verify(teamRepository,Mockito.times(1)) + .delete(teamId); + } + + @Test + public void shouldReturnErrorOnDeleteWhenNotExist() { + + Mockito.when(teamRepository.findById(Mockito.anyString())) + .thenReturn(Mono.empty()); + + StepVerifier + .create(teamService.deleteTeam("some id")) + .expectErrorMatches(error -> error instanceof TeamServiceException && error.getMessage().equals("cannot find team to delete")) + .verify(); + } +} + +