From 1f6beb292bfee04a4de0853e61f5995fa5f6dcac Mon Sep 17 00:00:00 2001 From: Lee Kyoo Min <58134412+kyoo0115@users.noreply.github.com> Date: Fri, 26 Jul 2024 23:08:39 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20JPQL=EB=A1=9C=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EC=BF=BC=EB=A6=AC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/ptContract/PtContractRepository.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/com/project/trainingdiary/repository/ptContract/PtContractRepository.java b/src/main/java/com/project/trainingdiary/repository/ptContract/PtContractRepository.java index 2b14ac3..81f8ef1 100644 --- a/src/main/java/com/project/trainingdiary/repository/ptContract/PtContractRepository.java +++ b/src/main/java/com/project/trainingdiary/repository/ptContract/PtContractRepository.java @@ -2,8 +2,10 @@ import com.project.trainingdiary.entity.PtContractEntity; import java.util.Optional; +import org.springframework.boot.context.properties.bind.BindResult; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface PtContractRepository extends JpaRepository, PtContractRepositoryCustom { @@ -41,4 +43,11 @@ public interface PtContractRepository extends JpaRepository findByTraineeId(long traineeId); + + @Query("SELECT ptc FROM pt_contract ptc " + + "LEFT JOIN FETCH ptc.trainee t " + + "LEFT JOIN FETCH t.inBodyRecords ir " + + "LEFT JOIN FETCH ptc.trainer tr " + + "WHERE t.id = :traineeId AND tr.id = :trainerId AND ptc.isTerminated = false") + Optional findWithTraineeAndTrainer(@Param("traineeId") Long traineeId, @Param("trainerId") Long trainerId); } From 591bad0b759ca6f257df26052a8cc26d09e8178d Mon Sep 17 00:00:00 2001 From: Lee Kyoo Min <58134412+kyoo0115@users.noreply.github.com> Date: Fri, 26 Jul 2024 23:08:55 +0800 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=ED=8A=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EB=8B=88=20=EB=B3=B8=EC=9D=B8=20=EC=A1=B0=ED=9A=8C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/TrainerController.java | 8 ++- .../trainingdiary/service/TrainerService.java | 69 ++++++++++++++++++- 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/project/trainingdiary/controller/TrainerController.java b/src/main/java/com/project/trainingdiary/controller/TrainerController.java index a1d25d8..b492319 100644 --- a/src/main/java/com/project/trainingdiary/controller/TrainerController.java +++ b/src/main/java/com/project/trainingdiary/controller/TrainerController.java @@ -7,6 +7,7 @@ import com.project.trainingdiary.dto.response.trainer.TraineeInfoResponseDto; import com.project.trainingdiary.service.TrainerService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -32,12 +33,13 @@ public class TrainerController { @Operation( summary = "트레이니 정보 조회", - description = "트레이너가 트레이니 정보를 조회합니다." + description = "트레이너 및 트레이니가 트레이니 정보를 조회합니다." ) @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "성공") + @ApiResponse(responseCode = "200", description = "성공"), + @ApiResponse(responseCode = "400", description = "다른 트레이니의 정보를 볼 없습니다.", content = @Content) }) - @PreAuthorize("hasRole('TRAINER')") + @PreAuthorize("hasRole('TRAINER') or hasRole('TRAINEE')") @GetMapping("/trainees/{id}") public ResponseEntity getTraineeInfo( @PathVariable Long id diff --git a/src/main/java/com/project/trainingdiary/service/TrainerService.java b/src/main/java/com/project/trainingdiary/service/TrainerService.java index 4672662..530b70b 100644 --- a/src/main/java/com/project/trainingdiary/service/TrainerService.java +++ b/src/main/java/com/project/trainingdiary/service/TrainerService.java @@ -9,9 +9,12 @@ import com.project.trainingdiary.entity.PtContractEntity; import com.project.trainingdiary.entity.TraineeEntity; import com.project.trainingdiary.entity.TrainerEntity; +import com.project.trainingdiary.exception.diet.DietNotExistException; import com.project.trainingdiary.exception.ptcontract.PtContractNotExistException; import com.project.trainingdiary.exception.user.TraineeNotFoundException; import com.project.trainingdiary.exception.user.TrainerNotFoundException; +import com.project.trainingdiary.exception.user.UnauthorizedTraineeException; +import com.project.trainingdiary.model.type.UserRoleType; import com.project.trainingdiary.repository.InBodyRecordHistoryRepository; import com.project.trainingdiary.repository.TraineeRepository; import com.project.trainingdiary.repository.TrainerRepository; @@ -38,13 +41,36 @@ public class TrainerService { * @return TraineeInfoResponseDto 트레이니의 정보와 남은 세션 수를 포함한 응답 DTO */ public TraineeInfoResponseDto getTraineeInfo(Long id) { + UserRoleType role = getMyRole(); + if (role.equals(UserRoleType.TRAINER)) { + return trainerGetTraineeInfo(id); + } else { + return traineeGetTraineeInfo(id); + } + } + + private TraineeInfoResponseDto trainerGetTraineeInfo(Long id) { TrainerEntity trainer = getAuthenticatedTrainer(); - TraineeEntity trainee = getTraineeById(id); - PtContractEntity ptContract = getPtContract(trainer, trainee); + + PtContractEntity ptContract = ptContractRepository.findWithTraineeAndTrainer(id, trainer.getId()) + .orElseThrow(PtContractNotExistException::new); + + TraineeEntity trainee = ptContract.getTrainee(); return TraineeInfoResponseDto.fromEntity(trainee, ptContract.getRemainingSession()); } + private TraineeInfoResponseDto traineeGetTraineeInfo(Long id) { + TraineeEntity trainee = getAuthenticatedTrainee(); + TrainerEntity trainer = getTrainerById(trainee.getId()); + + if (!trainee.getId().equals(id)) { + throw new UnauthorizedTraineeException(); + } + PtContractEntity ptContract = getPtContract(trainer, trainee); + return TraineeInfoResponseDto.fromEntity(trainee, ptContract.getRemainingSession()); + } + /** * 새로운 인바디 기록을 추가합니다. * @@ -93,6 +119,18 @@ private TraineeEntity getTraineeById(Long id) { .orElseThrow(TraineeNotFoundException::new); } + /** + * 트레이니 ID로 트레이니를 조회합니다. + * + * @param id 트레이니의 ID + * @return 트레이니 엔티티 (존재할 경우) + * @throws TrainerNotFoundException 트레이니가 존재하지 않을 경우 예외 발생 + */ + private TrainerEntity getTrainerById(Long id) { + return trainerRepository.findById(id) + .orElseThrow(TrainerNotFoundException::new); + } + /** * 인증된 트레이너를 조회합니다. * @@ -108,6 +146,21 @@ private TrainerEntity getAuthenticatedTrainer() { .orElseThrow(TrainerNotFoundException::new); } + /** + * 인증된 트레이너를 조회합니다. + * + * @return 트레이너 엔티티 + * @throws TraineeNotFoundException 트레이너가 존재하지 않을 경우 예외 발생 + */ + private TraineeEntity getAuthenticatedTrainee() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || authentication.getName() == null) { + throw new TraineeNotFoundException(); + } + return traineeRepository.findByEmail(authentication.getName()) + .orElseThrow(TraineeNotFoundException::new); + } + /** * 트레이너와 트레이니 간의 PT 계약을 조회합니다. * @@ -159,4 +212,16 @@ private void updateRemainingSession(PtContractEntity ptContract, int remainingSe int addition = remainingSession - ptContract.getRemainingSession(); ptContract.addSession(addition); } + + /** + * 현재 인증된 사용자의 역할을 반환합니다. + * + * @return 인증된 사용자의 역할 + */ + private UserRoleType getMyRole() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + return auth.getAuthorities().stream() + .anyMatch(a -> a.getAuthority().equals("ROLE_TRAINER")) ? UserRoleType.TRAINER + : UserRoleType.TRAINEE; + } } \ No newline at end of file From 30d8ecfc5f7046c536d371a98067ac82a7ed58a6 Mon Sep 17 00:00:00 2001 From: Lee Kyoo Min <58134412+kyoo0115@users.noreply.github.com> Date: Fri, 26 Jul 2024 23:09:06 +0800 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20=ED=8A=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EB=8B=88=20=EB=B3=B8=EC=9D=B8=EC=99=B8=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=8B=9C=20=EC=97=90=EB=9F=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/user/UnauthorizedTraineeException.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/main/java/com/project/trainingdiary/exception/user/UnauthorizedTraineeException.java diff --git a/src/main/java/com/project/trainingdiary/exception/user/UnauthorizedTraineeException.java b/src/main/java/com/project/trainingdiary/exception/user/UnauthorizedTraineeException.java new file mode 100644 index 0000000..9254897 --- /dev/null +++ b/src/main/java/com/project/trainingdiary/exception/user/UnauthorizedTraineeException.java @@ -0,0 +1,11 @@ +package com.project.trainingdiary.exception.user; + +import com.project.trainingdiary.exception.GlobalException; +import org.springframework.http.HttpStatus; + +public class UnauthorizedTraineeException extends GlobalException { + + public UnauthorizedTraineeException() { + super(HttpStatus.BAD_REQUEST, "다른 트레이니 정보를 볼 수 없습니다."); + } +} From 34580061dce0218fb7e67c8797230898ca10ddd6 Mon Sep 17 00:00:00 2001 From: Lee Kyoo Min <58134412+kyoo0115@users.noreply.github.com> Date: Sat, 27 Jul 2024 00:46:07 +0800 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20=ED=8A=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EB=8B=88=20=EB=B3=B8=EC=9D=B8=20=EC=A1=B0=ED=9A=8C=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/ptContract/PtContractRepository.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/project/trainingdiary/repository/ptContract/PtContractRepository.java b/src/main/java/com/project/trainingdiary/repository/ptContract/PtContractRepository.java index 81f8ef1..5c5a77f 100644 --- a/src/main/java/com/project/trainingdiary/repository/ptContract/PtContractRepository.java +++ b/src/main/java/com/project/trainingdiary/repository/ptContract/PtContractRepository.java @@ -2,7 +2,6 @@ import com.project.trainingdiary.entity.PtContractEntity; import java.util.Optional; -import org.springframework.boot.context.properties.bind.BindResult; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -49,5 +48,12 @@ public interface PtContractRepository extends JpaRepository findWithTraineeAndTrainer(@Param("traineeId") Long traineeId, @Param("trainerId") Long trainerId); + Optional findWithTraineeAndTrainer(@Param("traineeId") Long traineeId, + @Param("trainerId") Long trainerId); + + @Query("SELECT ptc FROM pt_contract ptc " + + "LEFT JOIN FETCH ptc.trainee t " + + "LEFT JOIN FETCH t.inBodyRecords ir " + + "WHERE t.id = :traineeId AND ptc.isTerminated = false") + Optional findByTraineeIdWithInBodyRecords(@Param("traineeId") Long traineeId); } From 52806de8af174c023d3b154fa681358f92ecd8fa Mon Sep 17 00:00:00 2001 From: Lee Kyoo Min <58134412+kyoo0115@users.noreply.github.com> Date: Sat, 27 Jul 2024 00:46:27 +0800 Subject: [PATCH 5/7] =?UTF-8?q?refactor:=20reformat=20code=20=EC=A7=84?= =?UTF-8?q?=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../trainingdiary/service/TrainerService.java | 68 ++++++++++++------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/project/trainingdiary/service/TrainerService.java b/src/main/java/com/project/trainingdiary/service/TrainerService.java index 530b70b..eb8473d 100644 --- a/src/main/java/com/project/trainingdiary/service/TrainerService.java +++ b/src/main/java/com/project/trainingdiary/service/TrainerService.java @@ -1,5 +1,6 @@ package com.project.trainingdiary.service; +import com.github.benmanes.caffeine.cache.Cache; import com.project.trainingdiary.dto.request.trainer.AddInBodyInfoRequestDto; import com.project.trainingdiary.dto.request.trainer.EditTraineeInfoRequestDto; import com.project.trainingdiary.dto.response.trainer.AddInBodyInfoResponseDto; @@ -9,17 +10,20 @@ import com.project.trainingdiary.entity.PtContractEntity; import com.project.trainingdiary.entity.TraineeEntity; import com.project.trainingdiary.entity.TrainerEntity; -import com.project.trainingdiary.exception.diet.DietNotExistException; import com.project.trainingdiary.exception.ptcontract.PtContractNotExistException; import com.project.trainingdiary.exception.user.TraineeNotFoundException; import com.project.trainingdiary.exception.user.TrainerNotFoundException; import com.project.trainingdiary.exception.user.UnauthorizedTraineeException; +import com.project.trainingdiary.exception.user.UserNotFoundException; +import com.project.trainingdiary.model.UserPrincipal; import com.project.trainingdiary.model.type.UserRoleType; import com.project.trainingdiary.repository.InBodyRecordHistoryRepository; import com.project.trainingdiary.repository.TraineeRepository; import com.project.trainingdiary.repository.TrainerRepository; import com.project.trainingdiary.repository.ptContract.PtContractRepository; import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; @@ -29,11 +33,14 @@ @RequiredArgsConstructor public class TrainerService { + private static final Logger log = LoggerFactory.getLogger(TrainerService.class); private final TraineeRepository traineeRepository; private final TrainerRepository trainerRepository; private final InBodyRecordHistoryRepository inBodyRecordHistoryRepository; private final PtContractRepository ptContractRepository; + private final Cache userCache; + /** * 트레이니 정보를 조회합니다. * @@ -49,10 +56,11 @@ public TraineeInfoResponseDto getTraineeInfo(Long id) { } } - private TraineeInfoResponseDto trainerGetTraineeInfo(Long id) { + public TraineeInfoResponseDto trainerGetTraineeInfo(Long id) { TrainerEntity trainer = getAuthenticatedTrainer(); - PtContractEntity ptContract = ptContractRepository.findWithTraineeAndTrainer(id, trainer.getId()) + PtContractEntity ptContract = ptContractRepository.findWithTraineeAndTrainer(id, + trainer.getId()) .orElseThrow(PtContractNotExistException::new); TraineeEntity trainee = ptContract.getTrainee(); @@ -60,15 +68,18 @@ private TraineeInfoResponseDto trainerGetTraineeInfo(Long id) { return TraineeInfoResponseDto.fromEntity(trainee, ptContract.getRemainingSession()); } - private TraineeInfoResponseDto traineeGetTraineeInfo(Long id) { + public TraineeInfoResponseDto traineeGetTraineeInfo(Long id) { TraineeEntity trainee = getAuthenticatedTrainee(); - TrainerEntity trainer = getTrainerById(trainee.getId()); if (!trainee.getId().equals(id)) { throw new UnauthorizedTraineeException(); } - PtContractEntity ptContract = getPtContract(trainer, trainee); - return TraineeInfoResponseDto.fromEntity(trainee, ptContract.getRemainingSession()); + PtContractEntity ptContract = ptContractRepository.findByTraineeIdWithInBodyRecords(id) + .orElseThrow(PtContractNotExistException::new); + + TraineeEntity fetchedTrainee = ptContract.getTrainee(); + + return TraineeInfoResponseDto.fromEntity(fetchedTrainee, ptContract.getRemainingSession()); } /** @@ -119,18 +130,6 @@ private TraineeEntity getTraineeById(Long id) { .orElseThrow(TraineeNotFoundException::new); } - /** - * 트레이니 ID로 트레이니를 조회합니다. - * - * @param id 트레이니의 ID - * @return 트레이니 엔티티 (존재할 경우) - * @throws TrainerNotFoundException 트레이니가 존재하지 않을 경우 예외 발생 - */ - private TrainerEntity getTrainerById(Long id) { - return trainerRepository.findById(id) - .orElseThrow(TrainerNotFoundException::new); - } - /** * 인증된 트레이너를 조회합니다. * @@ -139,25 +138,39 @@ private TrainerEntity getTrainerById(Long id) { */ private TrainerEntity getAuthenticatedTrainer() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication == null || authentication.getName() == null) { + if (authentication == null + || !(authentication.getPrincipal() instanceof UserPrincipal userPrincipal)) { throw new TrainerNotFoundException(); } - return trainerRepository.findByEmail(authentication.getName()) + + UserPrincipal cachedUser = userCache.getIfPresent(userPrincipal.getEmail()); + if (cachedUser != null && cachedUser.getTrainer() != null) { + return cachedUser.getTrainer(); + } + + return trainerRepository.findByEmail(userPrincipal.getEmail()) .orElseThrow(TrainerNotFoundException::new); } /** - * 인증된 트레이너를 조회합니다. + * 인증된 트레이니를 조회합니다. * - * @return 트레이너 엔티티 - * @throws TraineeNotFoundException 트레이너가 존재하지 않을 경우 예외 발생 + * @return 트레이니 엔티티 + * @throws TraineeNotFoundException 트레이니가 존재하지 않을 경우 예외 발생 */ private TraineeEntity getAuthenticatedTrainee() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication == null || authentication.getName() == null) { + if (authentication == null + || !(authentication.getPrincipal() instanceof UserPrincipal userPrincipal)) { throw new TraineeNotFoundException(); } - return traineeRepository.findByEmail(authentication.getName()) + + UserPrincipal cachedUser = userCache.getIfPresent(userPrincipal.getEmail()); + if (cachedUser != null && cachedUser.getTrainee() != null) { + return cachedUser.getTrainee(); + } + + return traineeRepository.findByEmail(userPrincipal.getEmail()) .orElseThrow(TraineeNotFoundException::new); } @@ -220,6 +233,9 @@ private void updateRemainingSession(PtContractEntity ptContract, int remainingSe */ private UserRoleType getMyRole() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth == null || auth.getAuthorities() == null) { + throw new UserNotFoundException(); + } return auth.getAuthorities().stream() .anyMatch(a -> a.getAuthority().equals("ROLE_TRAINER")) ? UserRoleType.TRAINER : UserRoleType.TRAINEE; From 4e4e986638e3567b79e5009b545d12f562310e36 Mon Sep 17 00:00:00 2001 From: Lee Kyoo Min <58134412+kyoo0115@users.noreply.github.com> Date: Sat, 27 Jul 2024 00:46:38 +0800 Subject: [PATCH 6/7] =?UTF-8?q?test:=20=ED=8A=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EB=84=88=20API=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/TrainerServiceTest.java | 367 +++++++++++++----- 1 file changed, 268 insertions(+), 99 deletions(-) diff --git a/src/test/java/com/project/trainingdiary/service/TrainerServiceTest.java b/src/test/java/com/project/trainingdiary/service/TrainerServiceTest.java index cd9e311..4f8d7bf 100644 --- a/src/test/java/com/project/trainingdiary/service/TrainerServiceTest.java +++ b/src/test/java/com/project/trainingdiary/service/TrainerServiceTest.java @@ -3,10 +3,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.github.benmanes.caffeine.cache.Cache; import com.project.trainingdiary.dto.request.trainer.AddInBodyInfoRequestDto; import com.project.trainingdiary.dto.request.trainer.EditTraineeInfoRequestDto; import com.project.trainingdiary.dto.response.trainer.AddInBodyInfoResponseDto; @@ -19,14 +22,21 @@ import com.project.trainingdiary.exception.ptcontract.PtContractNotExistException; import com.project.trainingdiary.exception.user.TraineeNotFoundException; import com.project.trainingdiary.exception.user.TrainerNotFoundException; +import com.project.trainingdiary.exception.user.UnauthorizedTraineeException; +import com.project.trainingdiary.exception.user.UserNotFoundException; +import com.project.trainingdiary.model.UserPrincipal; import com.project.trainingdiary.model.type.GenderType; import com.project.trainingdiary.model.type.TargetType; +import com.project.trainingdiary.model.type.UserRoleType; import com.project.trainingdiary.repository.InBodyRecordHistoryRepository; import com.project.trainingdiary.repository.TraineeRepository; import com.project.trainingdiary.repository.TrainerRepository; import com.project.trainingdiary.repository.ptContract.PtContractRepository; import java.time.LocalDate; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.Optional; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -37,8 +47,11 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; @ExtendWith(MockitoExtension.class) public class TrainerServiceTest { @@ -61,13 +74,95 @@ public class TrainerServiceTest { @Mock private SecurityContext securityContext; + @Mock + private Cache userCache; + @InjectMocks private TrainerService trainerService; + private TrainerEntity trainer; + private TraineeEntity trainee; + @BeforeEach - public void setUp() { + public void setup() { + setupTrainee(); + setupTrainer(); + } + + private void setupTrainee() { + trainee = TraineeEntity.builder() + .id(10L) + .email("trainee@example.com") + .name("김트레이니") + .role(UserRoleType.TRAINEE) + .build(); + } + + private void setupTrainer() { + trainer = TrainerEntity.builder() + .id(1L) + .email("trainer@example.com") + .name("이트레이너") + .role(UserRoleType.TRAINER) + .build(); + } + + private PtContractEntity createPtContract(TrainerEntity trainer, TraineeEntity trainee) { + PtContractEntity contract = new PtContractEntity(); + contract.setTrainer(trainer); + contract.setTrainee(trainee); + contract.setTotalSession(10); + contract.setUsedSession(5); + return contract; + } + + private List createInBodyRecords(TraineeEntity trainee) { + List inBodyRecords = new ArrayList<>(); + InBodyRecordHistoryEntity inBodyRecordHistory = new InBodyRecordHistoryEntity(); + inBodyRecordHistory.setTrainee(trainee); + inBodyRecordHistory.setWeight(10); + inBodyRecordHistory.setSkeletalMuscleMass(5); + inBodyRecordHistory.setBodyFatPercentage(80.0); + inBodyRecords.add(inBodyRecordHistory); + return inBodyRecords; + } + + private void setupTrainerAuth() { + GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_TRAINER"); + Collection authorities = Collections.singleton(authority); + + Authentication authentication = mock(Authentication.class); + lenient().when(authentication.getAuthorities()).thenReturn(authorities); + + UserDetails userDetails = UserPrincipal.create(trainer); + lenient().when(authentication.getPrincipal()).thenReturn(userDetails); + lenient().when(authentication.getName()).thenReturn(trainer.getEmail()); + + SecurityContext securityContext = mock(SecurityContext.class); + lenient().when(securityContext.getAuthentication()).thenReturn(authentication); + SecurityContextHolder.setContext(securityContext); + + lenient().when(trainerRepository.findByEmail(trainer.getEmail())) + .thenReturn(Optional.of(trainer)); + } + + private void setupTraineeAuth() { + GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_TRAINEE"); + Collection authorities = Collections.singleton(authority); + + Authentication authentication = mock(Authentication.class); + lenient().when(authentication.getAuthorities()).thenReturn(authorities); + + UserDetails userDetails = UserPrincipal.create(trainee); + lenient().when(authentication.getPrincipal()).thenReturn(userDetails); + lenient().when(authentication.getName()).thenReturn(trainee.getEmail()); + + SecurityContext securityContext = mock(SecurityContext.class); + lenient().when(securityContext.getAuthentication()).thenReturn(authentication); SecurityContextHolder.setContext(securityContext); - when(securityContext.getAuthentication()).thenReturn(authentication); + + lenient().when(traineeRepository.findByEmail(trainee.getEmail())) + .thenReturn(Optional.of(trainee)); } @AfterEach @@ -76,12 +171,12 @@ public void cleanup() { } @Test - @DisplayName("인증되지 않은 트레이너가 트레이니 정보를 요청할 때 예외 발생") + @DisplayName("인증되지 않은 유저가 트레이니 정보를 요청할 때 예외 발생") void testGetTraineeInfo_AuthenticationFailure() { // given - when(securityContext.getAuthentication()).thenReturn(null); + lenient().when(securityContext.getAuthentication()).thenReturn(null); // when / then - assertThrows(TrainerNotFoundException.class, () -> trainerService.getTraineeInfo(1L)); + assertThrows(UserNotFoundException.class, () -> trainerService.getTraineeInfo(1L)); } @Test @@ -91,7 +186,7 @@ void testAddInBodyRecord_AuthenticationFailure() { AddInBodyInfoRequestDto dto = new AddInBodyInfoRequestDto(); dto.setTraineeId(1L); - when(securityContext.getAuthentication()).thenReturn(null); + lenient().when(securityContext.getAuthentication()).thenReturn(null); // when / then assertThrows(TrainerNotFoundException.class, () -> trainerService.addInBodyRecord(dto)); @@ -104,80 +199,106 @@ void testEditTraineeInfo_AuthenticationFailure() { EditTraineeInfoRequestDto dto = new EditTraineeInfoRequestDto(); dto.setTraineeId(1L); - when(securityContext.getAuthentication()).thenReturn(null); + lenient().when(securityContext.getAuthentication()).thenReturn(null); // when / then assertThrows(TrainerNotFoundException.class, () -> trainerService.editTraineeInfo(dto)); } @Test - @DisplayName("트레이니 정보 조회 성공") - void testGetTraineeInfo_Success() { + @DisplayName("트레이너 트레이니 정보 조회 성공") + void testTrainerGetTraineeInfo_Success() { // given - Long traineeId = 1L; - Long trainerId = 1L; - String trainerEmail = "trainer@example.com"; + setupTrainer(); + setupTrainee(); + setupTrainerAuth(); - TrainerEntity trainer = new TrainerEntity(); - trainer.setId(trainerId); + PtContractEntity contract = createPtContract(trainer, trainee); + List inBodyRecords = createInBodyRecords(trainee); + trainee.setInBodyRecords(inBodyRecords); - TraineeEntity trainee = new TraineeEntity(); - trainee.setId(traineeId); - trainee.setInBodyRecords(new ArrayList<>()); + when(trainerRepository.findByEmail(trainer.getEmail())).thenReturn(Optional.of(trainer)); + when(ptContractRepository.findWithTraineeAndTrainer(trainee.getId(), + trainer.getId())).thenReturn(Optional.of(contract)); - PtContractEntity contract = new PtContractEntity(); - contract.setTrainer(trainer); - contract.setTrainee(trainee); - contract.setTotalSession(10); - contract.setUsedSession(5); + // when + TraineeInfoResponseDto responseDto = trainerService.getTraineeInfo(trainee.getId()); - when(authentication.getName()).thenReturn(trainerEmail); - when(trainerRepository.findByEmail(trainerEmail)).thenReturn(Optional.of(trainer)); - when(traineeRepository.findById(traineeId)).thenReturn(Optional.of(trainee)); - when(ptContractRepository.findByTrainerIdAndTraineeId(trainerId, traineeId)).thenReturn( + // then + assertEquals(5, responseDto.getRemainingSession()); + assertEquals(trainee.getId(), responseDto.getTraineeId()); + } + + @Test + @DisplayName("트레이니가 자신의 정보를 조회할 때 성공") + void testTraineeViewingOwnInfo_Success() { + // given + setupTrainee(); + setupTraineeAuth(); + + PtContractEntity contract = createPtContract(trainer, trainee); + List inBodyRecords = createInBodyRecords(trainee); + trainee.setInBodyRecords(inBodyRecords); + + when(traineeRepository.findByEmail(trainee.getEmail())).thenReturn(Optional.of(trainee)); + when(ptContractRepository.findByTraineeIdWithInBodyRecords(trainee.getId())).thenReturn( Optional.of(contract)); // when - TraineeInfoResponseDto responseDto = trainerService.getTraineeInfo(traineeId); + TraineeInfoResponseDto responseDto = trainerService.getTraineeInfo(trainee.getId()); // then assertEquals(5, responseDto.getRemainingSession()); - assertEquals(traineeId, responseDto.getTraineeId()); - verify(trainerRepository, times(1)).findByEmail(trainerEmail); - verify(traineeRepository, times(1)).findById(traineeId); - verify(ptContractRepository, times(1)).findByTrainerIdAndTraineeId(trainerId, traineeId); + assertEquals(trainee.getId(), responseDto.getTraineeId()); + } + + @Test + @DisplayName("트레이니가 다른 트레이니의 정보를 조회할 때 실패") + void testTraineeViewingOtherTraineeInfo_Fail() { + // given + setupTrainee(); + setupTraineeAuth(); + + TraineeEntity otherTrainee = TraineeEntity.builder() + .id(20L) + .email("othertrainee@example.com") + .name("다른트레이니") + .role(UserRoleType.TRAINEE) + .build(); + + List inBodyRecords = createInBodyRecords(otherTrainee); + otherTrainee.setInBodyRecords(inBodyRecords); + + when(traineeRepository.findByEmail(trainee.getEmail())).thenReturn(Optional.of(trainee)); + // when / then + assertThrows(UnauthorizedTraineeException.class, + () -> trainerService.getTraineeInfo(otherTrainee.getId())); } @Test @DisplayName("트레이니가 존재하지 않을 때 예외 발생") void testGetTraineeInfo_TraineeNotExistException() { // given - Long traineeId = 1L; - String trainerEmail = "trainer@example.com"; + setupTrainer(); + setupTrainerAuth(); - TrainerEntity trainer = new TrainerEntity(); - trainer.setId(1L); - - when(authentication.getName()).thenReturn(trainerEmail); - when(trainerRepository.findByEmail(trainerEmail)).thenReturn(Optional.of(trainer)); - when(traineeRepository.findById(traineeId)).thenReturn(Optional.empty()); + Long traineeId = 1L; + when(ptContractRepository.findWithTraineeAndTrainer(traineeId, trainer.getId())).thenReturn( + Optional.empty()); // when / then - assertThrows(TraineeNotFoundException.class, () -> trainerService.getTraineeInfo(traineeId)); - verify(trainerRepository, times(1)).findByEmail(trainerEmail); - verify(traineeRepository, times(1)).findById(traineeId); + assertThrows(PtContractNotExistException.class, () -> trainerService.getTraineeInfo(traineeId)); } @Test @DisplayName("트레이니 정보 수정 성공") void testEditTraineeInfo_Success() { // given - Long traineeId = 1L; - Long trainerId = 1L; - String trainerEmail = "trainer@example.com"; + setupTrainer(); + setupTrainerAuth(); - TrainerEntity trainer = new TrainerEntity(); - trainer.setId(trainerId); + Long traineeId = 1L; + Long trainerId = trainer.getId(); TraineeEntity trainee = new TraineeEntity(); trainee.setId(traineeId); @@ -192,8 +313,7 @@ void testEditTraineeInfo_Success() { dto.setTargetReward("Reward"); dto.setRemainingSession(20); - when(authentication.getName()).thenReturn(trainerEmail); - when(trainerRepository.findByEmail(trainerEmail)).thenReturn(Optional.of(trainer)); + when(trainerRepository.findByEmail(trainer.getEmail())).thenReturn(Optional.of(trainer)); when(traineeRepository.findById(traineeId)).thenReturn(Optional.of(trainee)); when(ptContractRepository.findByTrainerIdAndTraineeId(trainerId, traineeId)).thenReturn( Optional.of( @@ -217,7 +337,7 @@ void testEditTraineeInfo_Success() { assertEquals(dto.getTargetReward(), responseDto.getTargetReward()); assertEquals(dto.getRemainingSession(), responseDto.getRemainingSession()); - verify(trainerRepository, times(1)).findByEmail(trainerEmail); + verify(trainerRepository, times(1)).findByEmail(trainer.getEmail()); verify(traineeRepository, times(1)).findById(traineeId); verify(ptContractRepository, times(1)).findByTrainerIdAndTraineeId(trainerId, traineeId); } @@ -226,16 +346,15 @@ void testEditTraineeInfo_Success() { @DisplayName("트레이니 정보 수정 시 트레이니가 존재하지 않을 때 예외 발생") void testEditTraineeInfo_TraineeNotExistException() { // given - Long traineeId = 1L; - String trainerEmail = "trainer@example.com"; + setupTrainer(); + setupTrainerAuth(); - TrainerEntity trainer = new TrainerEntity(); - trainer.setId(1L); + Long traineeId = 1L; + String trainerEmail = trainer.getEmail(); EditTraineeInfoRequestDto dto = new EditTraineeInfoRequestDto(); dto.setTraineeId(traineeId); - when(authentication.getName()).thenReturn(trainerEmail); when(trainerRepository.findByEmail(trainerEmail)).thenReturn(Optional.of(trainer)); when(traineeRepository.findById(traineeId)).thenReturn(Optional.empty()); @@ -249,15 +368,12 @@ void testEditTraineeInfo_TraineeNotExistException() { @DisplayName("인바디 기록 추가 성공") void testAddInBodyRecord_Success() { // given - Long traineeId = 1L; - Long trainerId = 1L; - String trainerEmail = "trainer@example.com"; + setupTrainer(); + setupTrainee(); + setupTrainerAuth(); - TrainerEntity trainer = new TrainerEntity(); - trainer.setId(trainerId); - - TraineeEntity trainee = new TraineeEntity(); - trainee.setId(traineeId); + Long traineeId = trainee.getId(); + Long trainerId = trainer.getId(); AddInBodyInfoRequestDto dto = new AddInBodyInfoRequestDto(); dto.setTraineeId(traineeId); @@ -271,8 +387,7 @@ void testAddInBodyRecord_Success() { inBodyRecord.setSkeletalMuscleMass(dto.getSkeletalMuscleMass()); inBodyRecord.setBodyFatPercentage(dto.getBodyFatPercentage()); - when(authentication.getName()).thenReturn(trainerEmail); - when(trainerRepository.findByEmail(trainerEmail)).thenReturn(Optional.of(trainer)); + when(trainerRepository.findByEmail(trainer.getEmail())).thenReturn(Optional.of(trainer)); when(traineeRepository.findById(traineeId)).thenReturn(Optional.of(trainee)); when(ptContractRepository.existsByTrainerIdAndTraineeId(trainerId, traineeId)).thenReturn(true); when(inBodyRecordHistoryRepository.save(any(InBodyRecordHistoryEntity.class))).thenReturn( @@ -286,7 +401,7 @@ void testAddInBodyRecord_Success() { assertEquals(dto.getSkeletalMuscleMass(), responseDto.getSkeletalMuscleMass()); assertEquals(dto.getBodyFatPercentage(), responseDto.getBodyFatPercentage()); - verify(trainerRepository, times(1)).findByEmail(trainerEmail); + verify(trainerRepository, times(1)).findByEmail(trainer.getEmail()); verify(traineeRepository, times(1)).findById(traineeId); verify(ptContractRepository, times(1)).existsByTrainerIdAndTraineeId(trainerId, traineeId); verify(inBodyRecordHistoryRepository, times(1)).save(any(InBodyRecordHistoryEntity.class)); @@ -296,22 +411,18 @@ void testAddInBodyRecord_Success() { @DisplayName("인바디 기록 추가 시 트레이니가 존재하지 않을 때 예외 발생") void testAddInBodyRecord_TraineeNotExistException() { // given + setupTrainerAuth(); Long traineeId = 1L; - String trainerEmail = "trainer@example.com"; - - TrainerEntity trainer = new TrainerEntity(); - trainer.setId(1L); AddInBodyInfoRequestDto dto = new AddInBodyInfoRequestDto(); dto.setTraineeId(traineeId); - when(authentication.getName()).thenReturn(trainerEmail); - when(trainerRepository.findByEmail(trainerEmail)).thenReturn(Optional.of(trainer)); + when(trainerRepository.findByEmail(trainer.getEmail())).thenReturn(Optional.of(trainer)); when(traineeRepository.findById(traineeId)).thenReturn(Optional.empty()); // when / then assertThrows(TraineeNotFoundException.class, () -> trainerService.addInBodyRecord(dto)); - verify(trainerRepository, times(1)).findByEmail(trainerEmail); + verify(trainerRepository, times(1)).findByEmail(trainer.getEmail()); verify(traineeRepository, times(1)).findById(traineeId); } @@ -320,20 +431,10 @@ void testAddInBodyRecord_TraineeNotExistException() { @DisplayName("트레이너와 트레이니 사이에 계약이 존재하지 않을 때 예외 발생") void testGetTraineeInfo_ContractNotExist() { // given + setupTrainerAuth(); Long traineeId = 1L; - Long trainerId = 1L; - String trainerEmail = "trainer@example.com"; - - TrainerEntity trainer = new TrainerEntity(); - trainer.setId(trainerId); - - TraineeEntity trainee = new TraineeEntity(); - trainee.setId(traineeId); - when(authentication.getName()).thenReturn(trainerEmail); - when(trainerRepository.findByEmail(trainerEmail)).thenReturn(Optional.of(trainer)); - when(traineeRepository.findById(traineeId)).thenReturn(Optional.of(trainee)); - when(ptContractRepository.findByTrainerIdAndTraineeId(trainerId, traineeId)).thenReturn( + when(ptContractRepository.findWithTraineeAndTrainer(traineeId, trainer.getId())).thenReturn( Optional.empty()); // when / then @@ -344,12 +445,8 @@ void testGetTraineeInfo_ContractNotExist() { @DisplayName("트레이너와 트레이니 사이에 계약이 존재하지 않을 때 인바디 기록 추가 시 예외 발생") void testAddInBodyRecord_ContractNotExist() { // given + setupTrainerAuth(); Long traineeId = 1L; - Long trainerId = 1L; - String trainerEmail = "trainer@example.com"; - - TrainerEntity trainer = new TrainerEntity(); - trainer.setId(trainerId); TraineeEntity trainee = new TraineeEntity(); trainee.setId(traineeId); @@ -357,10 +454,9 @@ void testAddInBodyRecord_ContractNotExist() { AddInBodyInfoRequestDto dto = new AddInBodyInfoRequestDto(); dto.setTraineeId(traineeId); - when(authentication.getName()).thenReturn(trainerEmail); - when(trainerRepository.findByEmail(trainerEmail)).thenReturn(Optional.of(trainer)); + when(trainerRepository.findByEmail(trainer.getEmail())).thenReturn(Optional.of(trainer)); when(traineeRepository.findById(traineeId)).thenReturn(Optional.of(trainee)); - when(ptContractRepository.existsByTrainerIdAndTraineeId(trainerId, traineeId)).thenReturn( + when(ptContractRepository.existsByTrainerIdAndTraineeId(trainer.getId(), traineeId)).thenReturn( false); // when / then @@ -371,23 +467,96 @@ void testAddInBodyRecord_ContractNotExist() { @DisplayName("트레이너와 트레이니 사이에 계약이 존재하지 않을 때 트레이니 정보 수정 시 예외 발생") void testEditTraineeInfo_ContractNotExist() { // given + setupTrainerAuth(); + Long traineeId = 1L; - Long trainerId = 1L; String trainerEmail = "trainer@example.com"; - TrainerEntity trainer = new TrainerEntity(); - trainer.setId(trainerId); - TraineeEntity trainee = new TraineeEntity(); trainee.setId(traineeId); EditTraineeInfoRequestDto dto = new EditTraineeInfoRequestDto(); dto.setTraineeId(traineeId); - when(authentication.getName()).thenReturn(trainerEmail); when(trainerRepository.findByEmail(trainerEmail)).thenReturn(Optional.of(trainer)); when(traineeRepository.findById(traineeId)).thenReturn(Optional.of(trainee)); - when(ptContractRepository.findByTrainerIdAndTraineeId(trainerId, traineeId)).thenReturn( + when(ptContractRepository.findByTrainerIdAndTraineeId(trainer.getId(), traineeId)).thenReturn( + Optional.empty()); + + // when / then + assertThrows(PtContractNotExistException.class, () -> trainerService.editTraineeInfo(dto)); + } + + @Test + @DisplayName("캐시된 트레이니가 자신의 정보를 조회할 때 성공") + void testTraineeGetOwnInfo_CachedSuccess() { + // given + setupTrainee(); + setupTraineeAuth(); + + PtContractEntity contract = createPtContract(trainer, trainee); + List inBodyRecords = createInBodyRecords(trainee); + trainee.setInBodyRecords(inBodyRecords); + + UserPrincipal cachedUser = UserPrincipal.create(trainee); + when(userCache.getIfPresent(trainee.getEmail())).thenReturn(cachedUser); + when(ptContractRepository.findByTraineeIdWithInBodyRecords(trainee.getId())).thenReturn( + Optional.of(contract)); + + // when + TraineeInfoResponseDto responseDto = trainerService.getTraineeInfo(trainee.getId()); + + // then + assertEquals(5, responseDto.getRemainingSession()); + assertEquals(trainee.getId(), responseDto.getTraineeId()); + verify(userCache, times(1)).getIfPresent(trainee.getEmail()); + verify(ptContractRepository, times(1)).findByTraineeIdWithInBodyRecords(trainee.getId()); + } + + @Test + @DisplayName("캐시된 트레이너가 트레이니 정보를 조회할 때 성공") + void testTrainerGetTraineeInfo_CachedSuccess() { + // given + setupTrainer(); + setupTrainee(); + setupTrainerAuth(); + + PtContractEntity contract = createPtContract(trainer, trainee); + List inBodyRecords = createInBodyRecords(trainee); + trainee.setInBodyRecords(inBodyRecords); + + UserPrincipal cachedUser = UserPrincipal.create(trainer); + when(userCache.getIfPresent(trainer.getEmail())).thenReturn(cachedUser); + when(ptContractRepository.findWithTraineeAndTrainer(trainee.getId(), + trainer.getId())).thenReturn(Optional.of(contract)); + + // when + TraineeInfoResponseDto responseDto = trainerService.getTraineeInfo(trainee.getId()); + + // then + assertEquals(5, responseDto.getRemainingSession()); + assertEquals(trainee.getId(), responseDto.getTraineeId()); + verify(userCache, times(1)).getIfPresent(trainer.getEmail()); + verify(ptContractRepository, times(1)).findWithTraineeAndTrainer(trainee.getId(), + trainer.getId()); + } + + @Test + @DisplayName("트레이니 정보 수정 시 계약이 유효하지 않을 때 예외 발생") + void testEditTraineeInfo_InvalidContract() { + // given + setupTrainerAuth(); + Long traineeId = 1L; + + EditTraineeInfoRequestDto dto = new EditTraineeInfoRequestDto(); + dto.setTraineeId(traineeId); + + TraineeEntity trainee = new TraineeEntity(); + trainee.setId(traineeId); + + when(trainerRepository.findByEmail(trainer.getEmail())).thenReturn(Optional.of(trainer)); + when(traineeRepository.findById(traineeId)).thenReturn(Optional.of(trainee)); + when(ptContractRepository.findByTrainerIdAndTraineeId(trainer.getId(), traineeId)).thenReturn( Optional.empty()); // when / then From aa9d4e9c470a5f4b07234cd759f03fe1034cbf49 Mon Sep 17 00:00:00 2001 From: Lee Kyoo Min <58134412+kyoo0115@users.noreply.github.com> Date: Sat, 27 Jul 2024 00:59:24 +0800 Subject: [PATCH 7/7] =?UTF-8?q?refactor:=20Swagger=20docs=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/trainingdiary/controller/TrainerController.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/project/trainingdiary/controller/TrainerController.java b/src/main/java/com/project/trainingdiary/controller/TrainerController.java index b492319..f9abf21 100644 --- a/src/main/java/com/project/trainingdiary/controller/TrainerController.java +++ b/src/main/java/com/project/trainingdiary/controller/TrainerController.java @@ -37,7 +37,8 @@ public class TrainerController { ) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "성공"), - @ApiResponse(responseCode = "400", description = "다른 트레이니의 정보를 볼 없습니다.", content = @Content) + @ApiResponse(responseCode = "400", description = "다른 트레이니의 정보를 볼 없습니다.", content = @Content), + @ApiResponse(responseCode = "404", description = "계약이 없습니다.", content = @Content) }) @PreAuthorize("hasRole('TRAINER') or hasRole('TRAINEE')") @GetMapping("/trainees/{id}")