diff --git a/.github/workflows/master_weekly_cicd.yml b/.github/workflows/master_weekly_cicd.yml index 8d38d13..c0e3672 100644 --- a/.github/workflows/master_weekly_cicd.yml +++ b/.github/workflows/master_weekly_cicd.yml @@ -14,6 +14,16 @@ jobs: - name: 프로젝트 코드를 CI 서버로 옮겨오기 uses: actions/checkout@v4 + - name: Gradle 캐시 설정 + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: JDK 21 설치 uses: actions/setup-java@v4 with: @@ -35,12 +45,13 @@ jobs: aws.s3.bucket: ${{ secrets.AWS_S3_BUCKET }} aws.s3.accessKey: ${{ secrets.AWS_S3_ACCESS_KEY }} aws.s3.secretKey: ${{ secrets.AWS_S3_SECRET_KEY }} + jwt.secret: ${{ secrets.JWT_SECRET }} - name: 빌드로 테스트 수행 및 Jar 파일 생성 run: | chmod +x ./gradlew - ./gradlew clean build - mv build/libs/*SNAPSHOT.jar ./app.jar + ./gradlew clean build --build-cache + - run: mv build/libs/*SNAPSHOT.jar ./app.jar - name: 생성된 Jar 파일 EC2 서버로 전송하기 uses: appleboy/scp-action@v0.1.7 diff --git a/.github/workflows/pr_weekly_ci.yml b/.github/workflows/pr_weekly_ci.yml index 2478d21..f54701f 100644 --- a/.github/workflows/pr_weekly_ci.yml +++ b/.github/workflows/pr_weekly_ci.yml @@ -17,6 +17,16 @@ jobs: - name: 프로젝트 코드를 CI 서버로 옮겨오기 uses: actions/checkout@v4 + - name: Gradle 캐시 설정 + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: JDK 21 설치 uses: actions/setup-java@v4 with: @@ -38,11 +48,12 @@ jobs: aws.s3.bucket: ${{ secrets.AWS_S3_BUCKET }} aws.s3.accessKey: ${{ secrets.AWS_S3_ACCESS_KEY }} aws.s3.secretKey: ${{ secrets.AWS_S3_SECRET_KEY }} + jwt.secret: ${{ secrets.JWT_SECRET }} - name: 빌드 테스트 수행 run: | chmod +x ./gradlew - ./gradlew clean build --stacktrace + ./gradlew clean build --build-cache --stacktrace - name: 테스트 수행 결과 보고 uses: EnricoMi/publish-unit-test-result-action@v2 diff --git a/src/main/java/com/potatocake/everymoment/config/SecurityConfig.java b/src/main/java/com/potatocake/everymoment/config/SecurityConfig.java index 97955c9..a921809 100644 --- a/src/main/java/com/potatocake/everymoment/config/SecurityConfig.java +++ b/src/main/java/com/potatocake/everymoment/config/SecurityConfig.java @@ -45,7 +45,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/members/login", "/h2-console/**", "/error", "/favicon.ico").permitAll() + .requestMatchers("/api/members/login", "/api/members/anonymous-login", "/h2-console/**", + "/error", "/favicon.ico").permitAll() .requestMatchers("/swagger-resources/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll() .anyRequest().authenticated()); diff --git a/src/main/java/com/potatocake/everymoment/config/SwaggerConfig.java b/src/main/java/com/potatocake/everymoment/config/SwaggerConfig.java index bfac4c8..9acbedd 100644 --- a/src/main/java/com/potatocake/everymoment/config/SwaggerConfig.java +++ b/src/main/java/com/potatocake/everymoment/config/SwaggerConfig.java @@ -39,7 +39,7 @@ private OpenApiCustomizer addSecurityItemToAllEndpointsExceptLogin() { return openApi -> { SecurityRequirement securityRequirement = new SecurityRequirement().addList("bearerAuth"); openApi.getPaths().forEach((path, item) -> { - if (!"/api/members/login".equals(path)) { + if (!"/api/members/login".equals(path) && !"/api/members/anonymous-login".equals(path)) { item.readOperations().forEach(operation -> { operation.addSecurityItem(securityRequirement); }); diff --git a/src/main/java/com/potatocake/everymoment/controller/CommentController.java b/src/main/java/com/potatocake/everymoment/controller/CommentController.java index a73d7ce..31dbef2 100644 --- a/src/main/java/com/potatocake/everymoment/controller/CommentController.java +++ b/src/main/java/com/potatocake/everymoment/controller/CommentController.java @@ -13,6 +13,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -60,4 +61,18 @@ public ResponseEntity> deleteComment( return ResponseEntity.ok() .body(SuccessResponse.ok()); } + + @Operation(summary = "댓글 개수", description = "특정 일기의 댓글 개수를 조회합니다.") + @ApiResponse(responseCode = "200", description = "댓글 개수 조회 성공") + @GetMapping("/{diaryId}/count") + public ResponseEntity> getCommentCount( + @Parameter(description = "인증된 사용자 정보", hidden = true) + @AuthenticationPrincipal MemberDetails memberDetails, + @Parameter(description = "댓글 개수를 조회할 일기 ID", required = true) + @PathVariable Long diaryId) { + Long commentCount = commentService.getCommentCountByDiary(diaryId); + + return ResponseEntity.ok() + .body(SuccessResponse.ok(commentCount)); + } } diff --git a/src/main/java/com/potatocake/everymoment/controller/DiaryController.java b/src/main/java/com/potatocake/everymoment/controller/DiaryController.java index 26eb84e..41579ac 100644 --- a/src/main/java/com/potatocake/everymoment/controller/DiaryController.java +++ b/src/main/java/com/potatocake/everymoment/controller/DiaryController.java @@ -72,7 +72,7 @@ public ResponseEntity> createDiaryManual( @Parameter(description = "인증된 사용자 정보", hidden = true) @AuthenticationPrincipal MemberDetails memberDetails, @Parameter(description = "수기 일기 작성 정보", required = true) - @RequestBody DiaryManualCreateRequest diaryManualCreateRequest) { + @RequestBody @Valid DiaryManualCreateRequest diaryManualCreateRequest) { Long memberId = memberDetails.getId(); diaryService.createDiaryManual(memberId, diaryManualCreateRequest); diff --git a/src/main/java/com/potatocake/everymoment/controller/MemberController.java b/src/main/java/com/potatocake/everymoment/controller/MemberController.java index 894c9d6..67cf2ad 100644 --- a/src/main/java/com/potatocake/everymoment/controller/MemberController.java +++ b/src/main/java/com/potatocake/everymoment/controller/MemberController.java @@ -2,6 +2,7 @@ import com.potatocake.everymoment.dto.SuccessResponse; import com.potatocake.everymoment.dto.request.MemberLoginRequest; +import com.potatocake.everymoment.dto.response.AnonymousLoginResponse; import com.potatocake.everymoment.dto.response.JwtResponse; import com.potatocake.everymoment.dto.response.MemberDetailResponse; import com.potatocake.everymoment.dto.response.MemberMyResponse; @@ -39,6 +40,15 @@ public class MemberController { private final MemberService memberService; + @Operation(summary = "익명 로그인", description = "회원번호로 로그인하거나 새로운 익명 계정을 생성합니다.") + @ApiResponse(responseCode = "200", description = "익명 로그인 성공") + @GetMapping("/anonymous-login") + public ResponseEntity anonymousLogin(@Parameter(description = "기기에 저장된 회원번호 (없을 경우 새로운 계정 생성)") + @RequestParam(required = false) Long number) { + AnonymousLoginResponse response = memberService.anonymousLogin(number); + return ResponseEntity.ok(SuccessResponse.ok(response)); + } + @Operation(summary = "로그인", description = "회원 번호와 닉네임으로 로그인합니다.") @ApiResponse(responseCode = "200", description = "로그인 성공", content = @Content(schema = @Schema(implementation = JwtResponse.class))) @PostMapping("/login") diff --git a/src/main/java/com/potatocake/everymoment/dto/request/CategoryCreateRequest.java b/src/main/java/com/potatocake/everymoment/dto/request/CategoryCreateRequest.java index 3917850..abb9b47 100644 --- a/src/main/java/com/potatocake/everymoment/dto/request/CategoryCreateRequest.java +++ b/src/main/java/com/potatocake/everymoment/dto/request/CategoryCreateRequest.java @@ -4,8 +4,12 @@ import com.potatocake.everymoment.entity.Member; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; +@NoArgsConstructor +@AllArgsConstructor @Getter public class CategoryCreateRequest { diff --git a/src/main/java/com/potatocake/everymoment/dto/request/CategoryRequest.java b/src/main/java/com/potatocake/everymoment/dto/request/CategoryRequest.java index a397119..cb9b8bf 100644 --- a/src/main/java/com/potatocake/everymoment/dto/request/CategoryRequest.java +++ b/src/main/java/com/potatocake/everymoment/dto/request/CategoryRequest.java @@ -1,7 +1,11 @@ package com.potatocake.everymoment.dto.request; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; +@NoArgsConstructor +@AllArgsConstructor @Getter public class CategoryRequest { private Long categoryId; diff --git a/src/main/java/com/potatocake/everymoment/dto/request/CommentRequest.java b/src/main/java/com/potatocake/everymoment/dto/request/CommentRequest.java index ccb8e47..5798898 100644 --- a/src/main/java/com/potatocake/everymoment/dto/request/CommentRequest.java +++ b/src/main/java/com/potatocake/everymoment/dto/request/CommentRequest.java @@ -3,7 +3,9 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import lombok.Getter; +import lombok.Setter; +@Setter @Getter public class CommentRequest { diff --git a/src/main/java/com/potatocake/everymoment/dto/request/DiaryAutoCreateRequest.java b/src/main/java/com/potatocake/everymoment/dto/request/DiaryAutoCreateRequest.java index d1dbede..ba6f3c0 100644 --- a/src/main/java/com/potatocake/everymoment/dto/request/DiaryAutoCreateRequest.java +++ b/src/main/java/com/potatocake/everymoment/dto/request/DiaryAutoCreateRequest.java @@ -1,8 +1,10 @@ package com.potatocake.everymoment.dto.request; import com.potatocake.everymoment.dto.LocationPoint; +import lombok.Builder; import lombok.Getter; +@Builder @Getter public class DiaryAutoCreateRequest { private LocationPoint locationPoint; diff --git a/src/main/java/com/potatocake/everymoment/dto/request/DiaryManualCreateRequest.java b/src/main/java/com/potatocake/everymoment/dto/request/DiaryManualCreateRequest.java index 1fd5442..7659c2b 100644 --- a/src/main/java/com/potatocake/everymoment/dto/request/DiaryManualCreateRequest.java +++ b/src/main/java/com/potatocake/everymoment/dto/request/DiaryManualCreateRequest.java @@ -1,10 +1,13 @@ package com.potatocake.everymoment.dto.request; +import com.fasterxml.jackson.annotation.JsonProperty; import com.potatocake.everymoment.dto.LocationPoint; import jakarta.validation.constraints.Size; import java.util.List; +import lombok.Builder; import lombok.Getter; +@Builder @Getter public class DiaryManualCreateRequest { @@ -18,7 +21,10 @@ public class DiaryManualCreateRequest { @Size(max = 250, message = "주소는 250자를 초과할 수 없습니다") private String address; + @JsonProperty("bookmark") private boolean isBookmark; + + @JsonProperty("public") private boolean isPublic; @Size(max = 10, message = "이모지는 10자를 초과할 수 없습니다") diff --git a/src/main/java/com/potatocake/everymoment/dto/request/DiaryPatchRequest.java b/src/main/java/com/potatocake/everymoment/dto/request/DiaryPatchRequest.java index e6a965d..846798b 100644 --- a/src/main/java/com/potatocake/everymoment/dto/request/DiaryPatchRequest.java +++ b/src/main/java/com/potatocake/everymoment/dto/request/DiaryPatchRequest.java @@ -2,14 +2,18 @@ import jakarta.validation.constraints.Size; import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; +@Builder +@NoArgsConstructor +@AllArgsConstructor @Getter public class DiaryPatchRequest { private Boolean deleteAllCategories; - private Boolean locationNameDelete; - private Boolean addressDelete; private Boolean emojiDelete; private Boolean contentDelete; diff --git a/src/main/java/com/potatocake/everymoment/dto/request/FcmTokenRequest.java b/src/main/java/com/potatocake/everymoment/dto/request/FcmTokenRequest.java index 4a9827a..56ad1b9 100644 --- a/src/main/java/com/potatocake/everymoment/dto/request/FcmTokenRequest.java +++ b/src/main/java/com/potatocake/everymoment/dto/request/FcmTokenRequest.java @@ -2,8 +2,12 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; +@NoArgsConstructor +@AllArgsConstructor @Getter public class FcmTokenRequest { diff --git a/src/main/java/com/potatocake/everymoment/dto/response/AnonymousLoginResponse.java b/src/main/java/com/potatocake/everymoment/dto/response/AnonymousLoginResponse.java new file mode 100644 index 0000000..c60203b --- /dev/null +++ b/src/main/java/com/potatocake/everymoment/dto/response/AnonymousLoginResponse.java @@ -0,0 +1,15 @@ +package com.potatocake.everymoment.dto.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Getter; + +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +@Getter +public class AnonymousLoginResponse { + + private Long number; + private String token; + +} diff --git a/src/main/java/com/potatocake/everymoment/dto/response/CommentResponse.java b/src/main/java/com/potatocake/everymoment/dto/response/CommentResponse.java index 7fe835d..ff9957f 100644 --- a/src/main/java/com/potatocake/everymoment/dto/response/CommentResponse.java +++ b/src/main/java/com/potatocake/everymoment/dto/response/CommentResponse.java @@ -8,6 +8,7 @@ @Builder public class CommentResponse { private Long id; + private Long memberId; private CommentFriendResponse commentFriendResponse; private String content; private LocalDateTime createdAt; diff --git a/src/main/java/com/potatocake/everymoment/dto/response/FriendDiaryResponse.java b/src/main/java/com/potatocake/everymoment/dto/response/FriendDiaryResponse.java index 47ea8da..d533783 100644 --- a/src/main/java/com/potatocake/everymoment/dto/response/FriendDiaryResponse.java +++ b/src/main/java/com/potatocake/everymoment/dto/response/FriendDiaryResponse.java @@ -18,5 +18,6 @@ public class FriendDiaryResponse { private String emoji; private String content; private LikeCountResponse likeCount; + private boolean isLiked; private LocalDateTime createAt; } diff --git a/src/main/java/com/potatocake/everymoment/dto/response/MyDiaryResponse.java b/src/main/java/com/potatocake/everymoment/dto/response/MyDiaryResponse.java index a92bc41..c8c8110 100644 --- a/src/main/java/com/potatocake/everymoment/dto/response/MyDiaryResponse.java +++ b/src/main/java/com/potatocake/everymoment/dto/response/MyDiaryResponse.java @@ -15,5 +15,6 @@ public class MyDiaryResponse { private boolean isBookmark; private String emoji; private String content; + private boolean isLiked; private LocalDateTime createAt; } diff --git a/src/main/java/com/potatocake/everymoment/entity/Diary.java b/src/main/java/com/potatocake/everymoment/entity/Diary.java index ab83a6d..c139ff3 100644 --- a/src/main/java/com/potatocake/everymoment/entity/Diary.java +++ b/src/main/java/com/potatocake/everymoment/entity/Diary.java @@ -91,6 +91,15 @@ public void updateEmoji(String emoji) { } } + public void updateContentNull() { + this.content = null; + } + + public void updateEmojiNull() { + this.emoji = null; + + } + public void updateBookmark(boolean isBookmark) { this.isBookmark = isBookmark; } @@ -112,3 +121,4 @@ public boolean checkOwner(Long memberId) { } } + diff --git a/src/main/java/com/potatocake/everymoment/entity/Like.java b/src/main/java/com/potatocake/everymoment/entity/Like.java index 8f8da95..ff8ecfb 100644 --- a/src/main/java/com/potatocake/everymoment/entity/Like.java +++ b/src/main/java/com/potatocake/everymoment/entity/Like.java @@ -9,10 +9,12 @@ import jakarta.persistence.Table; import lombok.AccessLevel; import lombok.Builder; +import lombok.Getter; import lombok.NoArgsConstructor; @Table(name = "likes") @NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter @Entity public class Like { diff --git a/src/main/java/com/potatocake/everymoment/exception/ErrorCode.java b/src/main/java/com/potatocake/everymoment/exception/ErrorCode.java index 043f2ae..225367f 100644 --- a/src/main/java/com/potatocake/everymoment/exception/ErrorCode.java +++ b/src/main/java/com/potatocake/everymoment/exception/ErrorCode.java @@ -35,7 +35,7 @@ public enum ErrorCode { /* File */ FILE_NOT_FOUND("존재하지 않는 파일입니다.", NOT_FOUND), - FILE_SIZE_EXCEEDED("각 파일은 1MB 이하로, 전체 파일 크기는 10MB 이하로 첨부해 주세요.", PAYLOAD_TOO_LARGE), + FILE_SIZE_EXCEEDED("각 파일은 5MB 이하로, 전체 파일 크기는 25MB 이하로 첨부해 주세요.", PAYLOAD_TOO_LARGE), /* Comment */ COMMENT_NOT_FOUND("존재하지 않는 댓글입니다.", NOT_FOUND), diff --git a/src/main/java/com/potatocake/everymoment/repository/CommentRepository.java b/src/main/java/com/potatocake/everymoment/repository/CommentRepository.java index 8129c6d..73c26ce 100644 --- a/src/main/java/com/potatocake/everymoment/repository/CommentRepository.java +++ b/src/main/java/com/potatocake/everymoment/repository/CommentRepository.java @@ -7,4 +7,6 @@ public interface CommentRepository extends JpaRepository { Page findAllByDiaryId(Long diaryId, Pageable pageable); + + Long countByDiaryId(Long diaryId); } diff --git a/src/main/java/com/potatocake/everymoment/repository/LikeRepository.java b/src/main/java/com/potatocake/everymoment/repository/LikeRepository.java index dc383b1..367cd00 100644 --- a/src/main/java/com/potatocake/everymoment/repository/LikeRepository.java +++ b/src/main/java/com/potatocake/everymoment/repository/LikeRepository.java @@ -11,4 +11,6 @@ public interface LikeRepository extends JpaRepository { Long countByDiary(Diary diary); + boolean existsByMemberIdAndDiaryId(Long memberId, Long diaryId); + } diff --git a/src/main/java/com/potatocake/everymoment/repository/MemberRepository.java b/src/main/java/com/potatocake/everymoment/repository/MemberRepository.java index 135e588..7f318ed 100644 --- a/src/main/java/com/potatocake/everymoment/repository/MemberRepository.java +++ b/src/main/java/com/potatocake/everymoment/repository/MemberRepository.java @@ -1,11 +1,14 @@ package com.potatocake.everymoment.repository; import com.potatocake.everymoment.entity.Member; +import jakarta.persistence.LockModeType; import java.util.Optional; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Window; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; public interface MemberRepository extends JpaRepository { @@ -15,4 +18,8 @@ public interface MemberRepository extends JpaRepository { Window findByNicknameContaining(String nickname, ScrollPosition position, Pageable pageable); + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT CASE WHEN MIN(m.number) > 0 OR MIN(m.number) IS NULL THEN -1 ELSE MIN(m.number) - 1 END FROM Member m") + Long findNextAnonymousNumber(); + } diff --git a/src/main/java/com/potatocake/everymoment/service/CommentService.java b/src/main/java/com/potatocake/everymoment/service/CommentService.java index e138bfc..762b5d9 100644 --- a/src/main/java/com/potatocake/everymoment/service/CommentService.java +++ b/src/main/java/com/potatocake/everymoment/service/CommentService.java @@ -121,10 +121,14 @@ private CommentFriendResponse convertToCommentFriendResponseDTO(Member member) { private CommentResponse convertToCommentResponseDTO(Comment comment) { return CommentResponse.builder() .id(comment.getId()) + .memberId(comment.getMember().getId()) .commentFriendResponse(convertToCommentFriendResponseDTO(comment.getMember())) .content(comment.getContent()) .createdAt(comment.getCreateAt()) .build(); } + public Long getCommentCountByDiary(Long diaryId) { + return commentRepository.countByDiaryId(diaryId); + } } diff --git a/src/main/java/com/potatocake/everymoment/service/DiaryService.java b/src/main/java/com/potatocake/everymoment/service/DiaryService.java index 253f4a0..3378c30 100644 --- a/src/main/java/com/potatocake/everymoment/service/DiaryService.java +++ b/src/main/java/com/potatocake/everymoment/service/DiaryService.java @@ -22,6 +22,7 @@ import com.potatocake.everymoment.repository.DiaryCategoryRepository; import com.potatocake.everymoment.repository.DiaryRepository; import com.potatocake.everymoment.repository.FileRepository; +import com.potatocake.everymoment.repository.LikeRepository; import com.potatocake.everymoment.repository.MemberRepository; import java.time.LocalDate; import java.util.List; @@ -33,6 +34,7 @@ import org.locationtech.jts.geom.Point; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -47,6 +49,7 @@ public class DiaryService { private final MemberRepository memberRepository; private final CategoryRepository categoryRepository; private final FileRepository fileRepository; + private final LikeRepository likeRepository; private final GeometryFactory geometryFactory; private final NotificationService notificationService; @@ -137,7 +140,7 @@ public MyDiariesResponse getMyDiaries(Long memberId, DiaryFilterRequest diaryFil Specification spec; - if(!diaryFilterRequest.hasFilter()){ + if (!diaryFilterRequest.hasFilter()) { LocalDate today = LocalDate.now(); spec = DiarySpecification.filterDiaries( @@ -149,9 +152,10 @@ public MyDiariesResponse getMyDiaries(Long memberId, DiaryFilterRequest diaryFil diaryFilterRequest.getUntil(), diaryFilterRequest.getIsBookmark()) .and((root, query, builder) -> builder.equal(root.get("member"), currentMember)); - } - else{ + diaryPage = diaryRepository.findAll(spec, + PageRequest.of(diaryFilterRequest.getKey(), diaryFilterRequest.getSize())); + } else { spec = DiarySpecification.filterDiaries( diaryFilterRequest.getKeyword(), emojis, @@ -161,10 +165,11 @@ public MyDiariesResponse getMyDiaries(Long memberId, DiaryFilterRequest diaryFil diaryFilterRequest.getUntil(), diaryFilterRequest.getIsBookmark()) .and((root, query, builder) -> builder.equal(root.get("member"), currentMember)); - } - diaryPage = diaryRepository.findAll(spec, - PageRequest.of(diaryFilterRequest.getKey(), diaryFilterRequest.getSize())); + diaryPage = diaryRepository.findAll(spec, + PageRequest.of(diaryFilterRequest.getKey(), diaryFilterRequest.getSize(), + Sort.by(Sort.Direction.DESC, "createAt"))); + } List diaryDTOs = diaryPage.getContent().stream() .map(this::convertToMyDiarySimpleResponseDto) @@ -182,7 +187,7 @@ public MyDiariesResponse getMyDiaries(Long memberId, DiaryFilterRequest diaryFil @Transactional(readOnly = true) public MyDiaryResponse getMyDiary(Long memberId, Long diaryId) { Diary diary = getExistDiary(memberId, diaryId); - return convertToMyDiaryResponseDto(diary); + return convertToMyDiaryResponseDto(diary, memberId); } // 내 일기 위치 조회 @@ -205,7 +210,10 @@ public void updateDiary(Long memberId, Long diaryId, DiaryPatchRequest diaryPatc diaryCategoryRepository.deleteByDiary(existingDiary); } else { List categoryRequestList = diaryPatchRequest.getCategories(); + if (categoryRequestList != null && !categoryRequestList.isEmpty()) { + diaryCategoryRepository.deleteByDiary(existingDiary); + for (CategoryRequest categoryRequest : categoryRequestList) { Long categoryId = categoryRequest.getCategoryId(); @@ -226,25 +234,16 @@ public void updateDiary(Long memberId, Long diaryId, DiaryPatchRequest diaryPatc // 다이어리 업데이트 if (diaryPatchRequest.getContentDelete() != null && diaryPatchRequest.getContentDelete()) { - existingDiary.updateContent(null); + existingDiary.updateContentNull(); } else { existingDiary.updateContent(diaryPatchRequest.getContent()); } - if (diaryPatchRequest.getLocationNameDelete() != null && diaryPatchRequest.getLocationNameDelete()) { - existingDiary.updateLocationName(null); - } else { - existingDiary.updateLocationName(diaryPatchRequest.getLocationName()); - } - - if (diaryPatchRequest.getAddressDelete() != null && diaryPatchRequest.getAddressDelete()) { - existingDiary.updateAddress(null); - } else { - existingDiary.updateAddress(diaryPatchRequest.getAddress()); - } + existingDiary.updateLocationName(diaryPatchRequest.getLocationName()); + existingDiary.updateAddress(diaryPatchRequest.getAddress()); if (diaryPatchRequest.getEmojiDelete() != null && diaryPatchRequest.getEmojiDelete()) { - existingDiary.updateEmoji(null); + existingDiary.updateEmojiNull(); } else { existingDiary.updateEmoji(diaryPatchRequest.getEmoji()); } @@ -284,7 +283,7 @@ private Diary getExistDiary(Long memberId, Long diaryId) { } //상세 조회시 일기DTO 변환 - private MyDiaryResponse convertToMyDiaryResponseDto(Diary savedDiary) { + private MyDiaryResponse convertToMyDiaryResponseDto(Diary savedDiary, Long memberId) { // 카테고리 찾음 List diaryCategories = diaryCategoryRepository.findByDiary(savedDiary); List categoryResponseList = diaryCategories.stream() @@ -294,6 +293,8 @@ private MyDiaryResponse convertToMyDiaryResponseDto(Diary savedDiary) { .build()) .collect(Collectors.toList()); + boolean isLiked = likeRepository.existsByMemberIdAndDiaryId(memberId, savedDiary.getId()); + return MyDiaryResponse.builder() .id(savedDiary.getId()) .categories(categoryResponseList) @@ -302,6 +303,7 @@ private MyDiaryResponse convertToMyDiaryResponseDto(Diary savedDiary) { .isBookmark(savedDiary.isBookmark()) .emoji(savedDiary.getEmoji()) .content(savedDiary.getContent()) + .isLiked(isLiked) .createAt(savedDiary.getCreateAt()) .build(); } diff --git a/src/main/java/com/potatocake/everymoment/service/FriendDiaryService.java b/src/main/java/com/potatocake/everymoment/service/FriendDiaryService.java index 2d6ad53..14daba2 100644 --- a/src/main/java/com/potatocake/everymoment/service/FriendDiaryService.java +++ b/src/main/java/com/potatocake/everymoment/service/FriendDiaryService.java @@ -25,6 +25,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -56,18 +57,18 @@ public FriendDiariesResponse getFriendDiaries(Long memberId, DiaryFilterRequest List categories = diaryFilterRequest.getCategories(); List emojis = diaryFilterRequest.getEmojis(); - Specification spec = DiarySpecification.filterDiaries( + Specification spec = FriendDiarySpecification.filterDiaries( diaryFilterRequest.getKeyword(), emojis, categories, diaryFilterRequest.getDate(), diaryFilterRequest.getFrom(), - diaryFilterRequest.getUntil(), - diaryFilterRequest.getIsBookmark()) + diaryFilterRequest.getUntil()) .and((root, query, builder) -> root.get("member").get("id").in(friendIdList)); diaryPage = diaryRepository.findAll(spec, - PageRequest.of(diaryFilterRequest.getKey(), diaryFilterRequest.getSize())); + PageRequest.of(diaryFilterRequest.getKey(), diaryFilterRequest.getSize(), + Sort.by(Sort.Direction.DESC, "createAt"))); List friendDiarySimpleResponseList = diaryPage.getContent().stream() .map(this::convertToFriendDiariesResponseDTO) @@ -86,6 +87,10 @@ public FriendDiaryResponse getFriendDiary(Long memberId, Long diaryId) { Diary diary = diaryRepository.findById(diaryId) .orElseThrow(() -> new GlobalException(ErrorCode.DIARY_NOT_FOUND)); + if (!diary.isPublic()) { + throw new GlobalException(ErrorCode.DIARY_NOT_PUBLIC); + } + //글쓴사람이 친구인지 확인 Member currentMember = memberRepository.findById(memberId) .orElseThrow(() -> new GlobalException(ErrorCode.MEMBER_NOT_FOUND)); @@ -110,6 +115,7 @@ public FriendDiaryResponse getFriendDiary(Long memberId, Long diaryId) { //like 갯수 반환 Long likeCount = likeRepository.countByDiary(diary); + boolean isLiked = likeRepository.existsByMemberIdAndDiaryId(memberId, diary.getId()); LikeCountResponse count = LikeCountResponse.builder() .likeCount(likeCount) @@ -122,6 +128,7 @@ public FriendDiaryResponse getFriendDiary(Long memberId, Long diaryId) { .emoji(diary.getEmoji()) .content(diary.getContent()) .likeCount(count) + .isLiked(isLiked) .createAt(diary.getCreateAt()) .build(); @@ -151,5 +158,4 @@ private FriendDiarySimpleResponse convertToFriendDiariesResponseDTO(Diary savedD .createAt(savedDiary.getCreateAt()) .build(); } - } diff --git a/src/main/java/com/potatocake/everymoment/service/FriendDiarySpecification.java b/src/main/java/com/potatocake/everymoment/service/FriendDiarySpecification.java new file mode 100644 index 0000000..a075cba --- /dev/null +++ b/src/main/java/com/potatocake/everymoment/service/FriendDiarySpecification.java @@ -0,0 +1,52 @@ +package com.potatocake.everymoment.service; + +import com.potatocake.everymoment.entity.Category; +import com.potatocake.everymoment.entity.Diary; +import com.potatocake.everymoment.entity.DiaryCategory; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.JoinType; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; +import java.time.LocalDate; +import java.util.List; +import org.springframework.data.jpa.domain.Specification; + +public class FriendDiarySpecification { + + public static Specification filterDiaries( + String keyword, List emojis, List categories, + LocalDate date, LocalDate from, LocalDate until) { + return (Root root, CriteriaQuery query, CriteriaBuilder builder) -> { + Predicate predicate = builder.conjunction(); + + if (keyword != null) { + predicate = builder.and(predicate, builder.like(root.get("content"), "%" + keyword + "%")); + } + + if (emojis != null && !emojis.isEmpty()) { + predicate = builder.and(predicate, root.get("emoji").in(emojis)); + } + + if (categories != null && !categories.isEmpty()) { + Join diaryCategoryJoin = root.join("diaryCategories", JoinType.LEFT); + Join categoryJoin = diaryCategoryJoin.join("category", JoinType.LEFT); + predicate = builder.and(predicate, categoryJoin.get("categoryName").in(categories)); + } + + if(date != null){ + predicate = builder.and(predicate, builder.equal(root.get("createAt").as(LocalDate.class), date)); + } + + if (from != null && until != null) { + predicate = builder.and(predicate, + builder.between(root.get("createAt"), from, until.plusDays(1))); + } + + predicate = builder.and(predicate, builder.isTrue(root.get("isPublic"))); + + return predicate; + }; + } +} diff --git a/src/main/java/com/potatocake/everymoment/service/FriendService.java b/src/main/java/com/potatocake/everymoment/service/FriendService.java index f21dead..852e850 100644 --- a/src/main/java/com/potatocake/everymoment/service/FriendService.java +++ b/src/main/java/com/potatocake/everymoment/service/FriendService.java @@ -22,6 +22,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -49,10 +50,10 @@ public OneFriendDiariesResponse OneFriendDiariesResponse(Long memberid, Long fri friendRepository.findByMemberAndFriend(currentMember, friend) .orElseThrow(() -> new GlobalException(ErrorCode.FRIEND_NOT_FOUND)); - Pageable pageable = PageRequest.of(key, size); + Pageable pageable = PageRequest.of(key, size, Sort.by(Sort.Direction.DESC, "createAt")); Page diaries = diaryRepository.findAll( - DiarySpecification.filterDiaries(null, null, null, date, null, null, null) + FriendDiarySpecification.filterDiaries(null, null, null, date, null, null) .and((root, query, builder) -> builder.equal(root.get("member").get("id"), friendId)), pageable); diff --git a/src/main/java/com/potatocake/everymoment/service/LikeService.java b/src/main/java/com/potatocake/everymoment/service/LikeService.java index 6f9d90f..d2c5586 100644 --- a/src/main/java/com/potatocake/everymoment/service/LikeService.java +++ b/src/main/java/com/potatocake/everymoment/service/LikeService.java @@ -41,7 +41,7 @@ public void toggleLike(Long memberId, Long diaryId) { Diary diary = diaryRepository.findById(diaryId) .orElseThrow(() -> new GlobalException(ErrorCode.DIARY_NOT_FOUND)); - if (!diary.isPublic()) { + if (!diary.isPublic() && !diary.checkOwner(memberId)) { throw new GlobalException(ErrorCode.DIARY_NOT_PUBLIC); } diff --git a/src/main/java/com/potatocake/everymoment/service/MemberService.java b/src/main/java/com/potatocake/everymoment/service/MemberService.java index faad94e..bf60761 100644 --- a/src/main/java/com/potatocake/everymoment/service/MemberService.java +++ b/src/main/java/com/potatocake/everymoment/service/MemberService.java @@ -2,6 +2,7 @@ import static org.springframework.data.domain.Sort.Direction.ASC; +import com.potatocake.everymoment.dto.response.AnonymousLoginResponse; import com.potatocake.everymoment.dto.response.FriendRequestStatus; import com.potatocake.everymoment.dto.response.MemberDetailResponse; import com.potatocake.everymoment.dto.response.MemberMyResponse; @@ -13,6 +14,7 @@ import com.potatocake.everymoment.repository.FriendRepository; import com.potatocake.everymoment.repository.FriendRequestRepository; import com.potatocake.everymoment.repository.MemberRepository; +import com.potatocake.everymoment.util.JwtUtil; import com.potatocake.everymoment.util.PagingUtil; import com.potatocake.everymoment.util.S3FileUploader; import java.util.List; @@ -35,6 +37,7 @@ public class MemberService { private final FriendRepository friendRepository; private final PagingUtil pagingUtil; private final S3FileUploader s3FileUploader; + private final JwtUtil jwtUtil; @Transactional(readOnly = true) public MemberSearchResponse searchMembers(String nickname, Long key, int size, Long currentMemberId) { @@ -72,6 +75,39 @@ public MemberMyResponse getMemberInfo(Long memberId) { .build(); } + public AnonymousLoginResponse anonymousLogin(Long memberNumber) { + if (memberNumber != null) { + // 기존 회원번호로 로그인 시도 + return memberRepository.findByNumber(memberNumber) + .map(member -> AnonymousLoginResponse.builder() + .token(jwtUtil.create(member.getId())) + .build()) + .orElseGet(this::createAnonymousLoginResponse); + } + + // 새로운 익명 회원 생성 및 응답 + return createAnonymousLoginResponse(); + } + + private AnonymousLoginResponse createAnonymousLoginResponse() { + Member newMember = createAnonymousMember(); + return AnonymousLoginResponse.builder() + .number(newMember.getNumber()) + .token(jwtUtil.create(newMember.getId())) + .build(); + } + + private Member createAnonymousMember() { + Long nextNumber = memberRepository.findNextAnonymousNumber(); + + Member member = Member.builder() + .nickname("Anonymous") + .number(nextNumber) + .build(); + + return memberRepository.save(member); + } + public void updateMemberInfo(Long id, MultipartFile profileImage, String nickname) { Member member = memberRepository.findById(id) .orElseThrow(() -> new GlobalException(ErrorCode.MEMBER_NOT_FOUND)); diff --git a/src/main/java/com/potatocake/everymoment/util/JwtUtil.java b/src/main/java/com/potatocake/everymoment/util/JwtUtil.java index 483aad3..54bb7c6 100644 --- a/src/main/java/com/potatocake/everymoment/util/JwtUtil.java +++ b/src/main/java/com/potatocake/everymoment/util/JwtUtil.java @@ -2,20 +2,33 @@ import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; import jakarta.servlet.http.HttpServletRequest; +import java.util.Base64; import java.util.Date; import java.util.Optional; import javax.crypto.SecretKey; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Component; @Component public class JwtUtil { - private final SecretKey SECRET_KEY = Jwts.SIG.HS256.key().build(); + @Value("${jwt.secret}") + private String secret; + + private SecretKey SECRET_KEY; private final Long EXPIRE = 1000L * 60 * 60 * 48; public final String PREFIX = "Bearer "; + @PostConstruct + public void init() { + byte[] keyBytes = Base64.getDecoder().decode(secret); + this.SECRET_KEY = Keys.hmacShaKeyFor(keyBytes); + } + public Long getId(String token) { return Jwts.parser().verifyWith(SECRET_KEY).build() .parseSignedClaims(token) diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index ec77b50..0c6cad9 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -3,25 +3,23 @@ spring: url: ${RDS_URL} username: ${RDS_USERNAME} password: ${RDS_PASSWORD} - driver-class-name: com.mysql.cj.jdbc.Driver jpa: properties: hibernate: - dialect: org.hibernate.dialect.MySQLDialect jdbc: time_zone: Asia/Seoul hibernate: - ddl-auto: update + ddl-auto: none h2: console: - enabled: false + enabled: true servlet: multipart: - max-file-size: 1MB - max-request-size: 10MB + max-file-size: 5MB + max-request-size: 25MB resolve-lazily: true aws: @@ -31,3 +29,6 @@ aws: bucket: ${AWS_S3_BUCKET} accessKey: ${AWS_S3_ACCESS_KEY} secretKey: ${AWS_S3_SECRET_KEY} + +jwt: + secret: ${JWT_SECRET} diff --git a/src/test/java/com/potatocake/everymoment/controller/CategoryControllerTest.java b/src/test/java/com/potatocake/everymoment/controller/CategoryControllerTest.java new file mode 100644 index 0000000..f7775ba --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/controller/CategoryControllerTest.java @@ -0,0 +1,218 @@ +package com.potatocake.everymoment.controller; + +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willDoNothing; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.potatocake.everymoment.dto.request.CategoryCreateRequest; +import com.potatocake.everymoment.dto.response.CategoryResponse; +import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.security.MemberDetails; +import com.potatocake.everymoment.service.CategoryService; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +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.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +@WebMvcTest(CategoryController.class) +class CategoryControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private CategoryService categoryService; + + @Test + @DisplayName("카테고리 목록이 성공적으로 조회된다.") + void should_ReturnCategories_When_RequestCategories() throws Exception { + // given + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + List responses = List.of( + CategoryResponse.builder() + .id(1L) + .categoryName("Category 1") + .build(), + CategoryResponse.builder() + .id(2L) + .categoryName("Category 2") + .build() + ); + + given(categoryService.getCategories(memberId)).willReturn(responses); + + // when + ResultActions result = mockMvc.perform(get("/api/categories") + .with(user(memberDetails))); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")) + .andExpect(jsonPath("$.info").isArray()) + .andExpect(jsonPath("$.info[0].categoryName").value("Category 1")) + .andExpect(jsonPath("$.info[1].categoryName").value("Category 2")); + + then(categoryService).should().getCategories(memberId); + } + + @Test + @DisplayName("카테고리가 성공적으로 추가된다.") + void should_CreateCategory_When_ValidInput() throws Exception { + // given + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + CategoryCreateRequest request = new CategoryCreateRequest("New Category"); + + willDoNothing().given(categoryService).addCategory( + eq(memberId), + argThat(req -> req.getCategoryName().equals("New Category")) + ); + + // when + ResultActions result = mockMvc.perform(post("/api/categories") + .with(user(memberDetails)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")); + + then(categoryService).should().addCategory( + eq(memberId), + argThat(req -> req.getCategoryName().equals("New Category")) + ); + } + + @Test + @DisplayName("카테고리가 성공적으로 수정된다.") + void should_UpdateCategory_When_ValidInput() throws Exception { + // given + Long memberId = 1L; + Long categoryId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + CategoryCreateRequest request = new CategoryCreateRequest("Updated Category"); + + willDoNothing().given(categoryService).updateCategory( + eq(categoryId), + eq(memberId), + argThat(req -> req.getCategoryName().equals("Updated Category")) + ); + + // when + ResultActions result = mockMvc.perform(patch("/api/categories/{categoryId}", categoryId) + .with(user(memberDetails)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")); + + then(categoryService).should().updateCategory( + eq(categoryId), + eq(memberId), + argThat(req -> req.getCategoryName().equals("Updated Category")) + ); + } + + @Test + @DisplayName("카테고리가 성공적으로 삭제된다.") + void should_DeleteCategory_When_ValidId() throws Exception { + // given + Long memberId = 1L; + Long categoryId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + willDoNothing().given(categoryService).deleteCategory(categoryId, memberId); + + // when + ResultActions result = mockMvc.perform(delete("/api/categories/{categoryId}", categoryId) + .with(user(memberDetails)) + .with(csrf())); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")); + + then(categoryService).should().deleteCategory(categoryId, memberId); + } + + @Test + @DisplayName("카테고리명이 누락되면 생성에 실패한다.") + void should_FailToCreate_When_CategoryNameMissing() throws Exception { + // given + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + CategoryCreateRequest request = new CategoryCreateRequest(); // categoryName 누락 + + // when + ResultActions result = mockMvc.perform(post("/api/categories") + .with(user(memberDetails)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + // then + result.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(400)); + + then(categoryService).shouldHaveNoInteractions(); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/controller/CommentControllerTest.java b/src/test/java/com/potatocake/everymoment/controller/CommentControllerTest.java new file mode 100644 index 0000000..7130077 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/controller/CommentControllerTest.java @@ -0,0 +1,139 @@ +package com.potatocake.everymoment.controller; + +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willDoNothing; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.potatocake.everymoment.dto.request.CommentRequest; +import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.security.MemberDetails; +import com.potatocake.everymoment.service.CommentService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +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.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +@WebMvcTest(CommentController.class) +class CommentControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private CommentService commentService; + + @Test + @DisplayName("댓글이 성공적으로 수정된다.") + void should_UpdateComment_When_ValidInput() throws Exception { + // given + Long memberId = 1L; + Long commentId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + CommentRequest request = new CommentRequest(); + request.setContent("Updated comment"); + + willDoNothing().given(commentService).updateComment( + eq(memberId), + eq(commentId), + argThat(req -> req.getContent().equals("Updated comment")) + ); + + // when + ResultActions result = mockMvc.perform(patch("/api/comments/{commentId}", commentId) + .with(user(memberDetails)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")); + + then(commentService).should().updateComment( + eq(memberId), + eq(commentId), + argThat(req -> req.getContent().equals("Updated comment")) + ); + } + + @Test + @DisplayName("댓글이 성공적으로 삭제된다.") + void should_DeleteComment_When_ValidId() throws Exception { + // given + Long memberId = 1L; + Long commentId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + willDoNothing().given(commentService).deleteComment(memberId, commentId); + + // when + ResultActions result = mockMvc.perform(delete("/api/comments/{commentId}", commentId) + .with(user(memberDetails)) + .with(csrf())); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")); + + then(commentService).should().deleteComment(memberId, commentId); + } + + @Test + @DisplayName("댓글 내용이 누락되면 수정에 실패한다.") + void should_FailToUpdate_When_ContentMissing() throws Exception { + // given + Long memberId = 1L; + Long commentId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + CommentRequest request = new CommentRequest(); + // content 누락 + + // when + ResultActions result = mockMvc.perform(patch("/api/comments/{commentId}", commentId) + .with(user(memberDetails)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + // then + result.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(400)); + + then(commentService).shouldHaveNoInteractions(); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/controller/DiaryControllerTest.java b/src/test/java/com/potatocake/everymoment/controller/DiaryControllerTest.java new file mode 100644 index 0000000..c52d79b --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/controller/DiaryControllerTest.java @@ -0,0 +1,572 @@ +package com.potatocake.everymoment.controller; + +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.BDDMockito.any; +import static org.mockito.BDDMockito.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willDoNothing; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.potatocake.everymoment.dto.LocationPoint; +import com.potatocake.everymoment.dto.request.CategoryRequest; +import com.potatocake.everymoment.dto.request.CommentRequest; +import com.potatocake.everymoment.dto.request.DiaryAutoCreateRequest; +import com.potatocake.everymoment.dto.request.DiaryFilterRequest; +import com.potatocake.everymoment.dto.request.DiaryManualCreateRequest; +import com.potatocake.everymoment.dto.request.DiaryPatchRequest; +import com.potatocake.everymoment.dto.response.CommentResponse; +import com.potatocake.everymoment.dto.response.CommentsResponse; +import com.potatocake.everymoment.dto.response.FriendDiariesResponse; +import com.potatocake.everymoment.dto.response.FriendDiaryResponse; +import com.potatocake.everymoment.dto.response.FriendDiarySimpleResponse; +import com.potatocake.everymoment.dto.response.MyDiariesResponse; +import com.potatocake.everymoment.dto.response.MyDiaryResponse; +import com.potatocake.everymoment.dto.response.MyDiarySimpleResponse; +import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.security.MemberDetails; +import com.potatocake.everymoment.service.CommentService; +import com.potatocake.everymoment.service.DiaryService; +import com.potatocake.everymoment.service.FriendDiaryService; +import java.util.List; +import java.util.Objects; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +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.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +@WithMockUser +@WebMvcTest(DiaryController.class) +class DiaryControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private DiaryService diaryService; + + @MockBean + private FriendDiaryService friendDiaryService; + + @MockBean + private CommentService commentService; + + @Test + @DisplayName("유효한 입력으로 자동 일기가 성공적으로 작성된다.") + void should_CreateAutoEntry_When_ValidInput() throws Exception { + // given + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + DiaryAutoCreateRequest request = DiaryAutoCreateRequest.builder() + .locationPoint(new LocationPoint(37.5665, 126.9780)) + .locationName("Seoul") + .address("Seoul, South Korea") + .build(); + + willDoNothing().given(diaryService).createDiaryAuto(memberId, request); + + // when + ResultActions result = mockMvc.perform(post("/api/diaries/auto") + .with(user(memberDetails)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")); + + then(diaryService).should().createDiaryAuto( + eq(memberId), + argThat(req -> + Objects.equals(req.getLocationName(), "Seoul") && + Objects.equals(req.getAddress(), "Seoul, South Korea") && + Objects.equals(req.getLocationPoint().getLatitude(), 37.5665) && + Objects.equals(req.getLocationPoint().getLongitude(), 126.978) + ) + ); + } + + @Test + @DisplayName("유효한 입력으로 수동 일기가 성공적으로 작성된다.") + void should_CreateManualEntry_When_ValidInput() throws Exception { + // given + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + LocationPoint locationPoint = new LocationPoint(37.5665, 126.978); + + DiaryManualCreateRequest request = DiaryManualCreateRequest.builder() + .locationPoint(locationPoint) + .locationName("Seoul") + .address("Seoul, South Korea") + .content("Test content") + .emoji("😊") + .categories(List.of(new CategoryRequest(1L))) + .isBookmark(false) + .isPublic(true) + .build(); + + willDoNothing().given(diaryService).createDiaryManual(memberId, request); + + // when + ResultActions result = mockMvc.perform(post("/api/diaries/manual") + .with(user(memberDetails)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")); + + then(diaryService).should().createDiaryManual( + eq(memberId), + argThat(req -> + Objects.equals(req.getContent(), "Test content") && + Objects.equals(req.getLocationName(), "Seoul") && + Objects.equals(req.getAddress(), "Seoul, South Korea") && + Objects.equals(req.getLocationPoint().getLatitude(), 37.5665) && + Objects.equals(req.getLocationPoint().getLongitude(), 126.978) && + Objects.equals(req.getEmoji(), "😊") && + Objects.equals(req.isPublic(), true) && + Objects.equals(req.isBookmark(), false) && + req.getCategories().size() == 1 && + Objects.equals(req.getCategories().get(0).getCategoryId(), 1L) + ) + ); + } + + @Test + @DisplayName("내 일기 목록이 성공적으로 조회된다.") + void should_ReturnMyDiaries_When_ValidRequest() throws Exception { + // given + Member member = Member.builder() + .id(1L) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + MyDiariesResponse response = MyDiariesResponse.builder() + .diaries(List.of(MyDiarySimpleResponse.builder() + .id(1L) + .content("Test content") + .locationName("Seoul") + .build())) + .next(null) + .build(); + + given(diaryService.getMyDiaries(eq(member.getId()), any(DiaryFilterRequest.class))) + .willReturn(response); + + // when + ResultActions result = mockMvc.perform(get("/api/diaries/my") + .with(user(memberDetails)) + .param("key", "0") + .param("size", "10")); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")) + .andExpect(jsonPath("$.info.diaries").isArray()); + + then(diaryService).should().getMyDiaries(eq(member.getId()), any(DiaryFilterRequest.class)); + } + + @Test + @DisplayName("내 일기 상세 정보가 성공적으로 조회된다.") + void should_ReturnMyDiaryDetail_When_ValidId() throws Exception { + // given + Long diaryId = 1L; + Member member = Member.builder() + .id(1L) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + MyDiaryResponse response = MyDiaryResponse.builder() + .id(diaryId) + .content("Test content") + .locationName("Seoul") + .build(); + + given(diaryService.getMyDiary(member.getId(), diaryId)).willReturn(response); + + // when + ResultActions result = mockMvc.perform(get("/api/diaries/my/{diaryId}", diaryId) + .with(user(memberDetails))); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")) + .andExpect(jsonPath("$.info.id").value(diaryId)); + + then(diaryService).should().getMyDiary(member.getId(), diaryId); + } + + @Test + @DisplayName("일기의 위치 정보가 성공적으로 조회된다.") + void should_ReturnDiaryLocation_When_ValidId() throws Exception { + // given + Long diaryId = 1L; + Member member = Member.builder() + .id(1L) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + LocationPoint locationPoint = new LocationPoint(37.5665, 126.9780); + given(diaryService.getDiaryLocation(member.getId(), diaryId)).willReturn(locationPoint); + + // when + ResultActions result = mockMvc.perform(get("/api/diaries/{diaryId}/location", diaryId) + .with(user(memberDetails))); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")) + .andExpect(jsonPath("$.info.latitude").value(37.5665)) + .andExpect(jsonPath("$.info.longitude").value(126.9780)); + + then(diaryService).should().getDiaryLocation(member.getId(), diaryId); + } + + @Test + @DisplayName("일기가 성공적으로 수정된다.") + void should_UpdateDiary_When_ValidInput() throws Exception { + // given + Long diaryId = 1L; + Member member = Member.builder() + .id(1L) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + DiaryPatchRequest request = DiaryPatchRequest.builder() + .content("Updated content") + .locationName("Updated location") + .address("Updated address") + .emoji("😊") + .categories(List.of(new CategoryRequest(1L))) + .build(); + + willDoNothing().given(diaryService).updateDiary(member.getId(), diaryId, request); + + // when + ResultActions result = mockMvc.perform(patch("/api/diaries/{diaryId}", diaryId) + .with(user(memberDetails)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")); + + then(diaryService).should().updateDiary( + eq(member.getId()), + eq(diaryId), + argThat(req -> + Objects.equals(req.getContent(), "Updated content") && + Objects.equals(req.getLocationName(), "Updated location") && + Objects.equals(req.getAddress(), "Updated address") && + Objects.equals(req.getEmoji(), "😊") && + req.getCategories().size() == 1 && + Objects.equals(req.getCategories().get(0).getCategoryId(), 1L)) + ); + } + + @Test + @DisplayName("일기가 성공적으로 삭제된다.") + void should_DeleteDiary_When_ValidId() throws Exception { + // given + Long diaryId = 1L; + Member member = Member.builder() + .id(1L) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + willDoNothing().given(diaryService).deleteDiary(member.getId(), diaryId); + + // when + ResultActions result = mockMvc.perform(delete("/api/diaries/{diaryId}", diaryId) + .with(user(memberDetails)) + .with(csrf())); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")); + + then(diaryService).should().deleteDiary(member.getId(), diaryId); + } + + @Test + @DisplayName("일기의 북마크 상태가 성공적으로 토글된다.") + void should_ToggleBookmark_When_ValidId() throws Exception { + // given + Long diaryId = 1L; + Member member = Member.builder() + .id(1L) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + willDoNothing().given(diaryService).toggleBookmark(member.getId(), diaryId); + + // when + ResultActions result = mockMvc.perform(patch("/api/diaries/{diaryId}/bookmark", diaryId) + .with(user(memberDetails)) + .with(csrf())); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")); + + then(diaryService).should().toggleBookmark(member.getId(), diaryId); + } + + @Test + @DisplayName("일기의 공개 상태가 성공적으로 토글된다.") + void should_TogglePrivacy_When_ValidId() throws Exception { + // given + Long diaryId = 1L; + Member member = Member.builder() + .id(1L) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + willDoNothing().given(diaryService).togglePrivacy(member.getId(), diaryId); + + // when + ResultActions result = mockMvc.perform(patch("/api/diaries/{diaryId}/privacy", diaryId) + .with(user(memberDetails)) + .with(csrf())); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")); + + then(diaryService).should().togglePrivacy(member.getId(), diaryId); + } + + @Test + @DisplayName("친구의 일기 목록이 성공적으로 조회된다.") + void should_ReturnFriendDiaries_When_ValidRequest() throws Exception { + // given + Member member = Member.builder() + .id(1L) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + FriendDiariesResponse response = FriendDiariesResponse.builder() + .diaries(List.of(FriendDiarySimpleResponse.builder() + .id(1L) + .content("Friend's content") + .locationName("Friend's location") + .build())) + .next(null) + .build(); + + given(friendDiaryService.getFriendDiaries(eq(member.getId()), any(DiaryFilterRequest.class))) + .willReturn(response); + + // when + ResultActions result = mockMvc.perform(get("/api/diaries/friend") + .with(user(memberDetails)) + .param("key", "0") + .param("size", "10")); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")) + .andExpect(jsonPath("$.info.diaries").isArray()); + + then(friendDiaryService).should().getFriendDiaries(eq(member.getId()), any(DiaryFilterRequest.class)); + } + + @Test + @DisplayName("친구의 일기 상세 정보가 성공적으로 조회된다.") + void should_ReturnFriendDiaryDetail_When_ValidId() throws Exception { + // given + Long diaryId = 1L; + Member member = Member.builder() + .id(1L) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + FriendDiaryResponse response = FriendDiaryResponse.builder() + .id(diaryId) + .content("Friend's content") + .locationName("Friend's location") + .build(); + + given(friendDiaryService.getFriendDiary(member.getId(), diaryId)).willReturn(response); + + // when + ResultActions result = mockMvc.perform(get("/api/diaries/friend/{diaryId}", diaryId) + .with(user(memberDetails))); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")) + .andExpect(jsonPath("$.info.id").value(diaryId)); + + then(friendDiaryService).should().getFriendDiary(member.getId(), diaryId); + } + + @Test + @DisplayName("일기의 댓글 목록이 성공적으로 조회된다.") + void should_ReturnComments_When_ValidDiaryId() throws Exception { + // given + Long diaryId = 1L; + Member member = Member.builder() + .id(1L) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + CommentsResponse response = CommentsResponse.builder() + .comments(List.of(CommentResponse.builder() + .id(1L) + .content("Test comment") + .build())) + .next(null) + .build(); + + given(commentService.getComments(diaryId, 0, 10)).willReturn(response); + + // when + ResultActions result = mockMvc.perform(get("/api/diaries/{diaryId}/comments", diaryId) + .with(user(memberDetails)) + .param("key", "0") + .param("size", "10")); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")) + .andExpect(jsonPath("$.info.comments").isArray()); + + then(commentService).should().getComments(diaryId, 0, 10); + } + + @Test + @DisplayName("댓글이 성공적으로 작성된다.") + void should_CreateComment_When_ValidInput() throws Exception { + // given + Long diaryId = 1L; + Member member = Member.builder() + .id(1L) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + CommentRequest request = new CommentRequest(); + request.setContent("Test comment"); + + willDoNothing().given(commentService).createComment(member.getId(), diaryId, request); + + // when + ResultActions result = mockMvc.perform(post("/api/diaries/{diaryId}/comments", diaryId) + .with(user(memberDetails)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + // then + result.andExpect(status().isOk()) + .andDo(print()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")); + + then(commentService).should().createComment( + eq(member.getId()), + eq(diaryId), + argThat(req -> req.getContent().equals("Test comment")) + ); + } + + @Test + @DisplayName("검증 실패시 수동 일기 작성에 실패한다.") + void should_FailToCreateManualEntry_When_ValidationFails() throws Exception { + // given + Member member = Member.builder() + .id(1L) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + DiaryManualCreateRequest request = DiaryManualCreateRequest.builder() + .content("x".repeat(15001)) // 최대 길이 초과 + .build(); + + // when + ResultActions result = mockMvc.perform(post("/api/diaries/manual") + .with(user(memberDetails)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + // then + result.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(400)); + + then(diaryService).shouldHaveNoInteractions(); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/controller/FcmControllerTest.java b/src/test/java/com/potatocake/everymoment/controller/FcmControllerTest.java new file mode 100644 index 0000000..8a9d8d2 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/controller/FcmControllerTest.java @@ -0,0 +1,126 @@ +package com.potatocake.everymoment.controller; + +import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willDoNothing; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.potatocake.everymoment.dto.request.FcmTokenRequest; +import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.security.MemberDetails; +import com.potatocake.everymoment.service.FcmService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +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.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +@WebMvcTest(FcmController.class) +class FcmControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private FcmService fcmService; + + @Test + @DisplayName("FCM 토큰이 성공적으로 등록된다.") + void should_RegisterToken_When_ValidInput() throws Exception { + // given + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + FcmTokenRequest request = new FcmTokenRequest("fcm-token-123", "device123"); + + willDoNothing().given(fcmService).registerToken(memberId, "device123", "fcm-token-123"); + + // when + ResultActions result = mockMvc.perform(post("/api/fcm/token") + .with(user(memberDetails)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")); + + then(fcmService).should().registerToken(memberId, "device123", "fcm-token-123"); + } + + @Test + @DisplayName("FCM 토큰이 성공적으로 삭제된다.") + void should_RemoveToken_When_ValidInput() throws Exception { + // given + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + String deviceId = "device123"; + + willDoNothing().given(fcmService).removeToken(memberId, deviceId); + + // when + ResultActions result = mockMvc.perform(delete("/api/fcm/token") + .with(user(memberDetails)) + .with(csrf()) + .param("deviceId", deviceId)); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")); + + then(fcmService).should().removeToken(memberId, deviceId); + } + + @Test + @DisplayName("FCM 토큰 요청 데이터가 누락되면 등록에 실패한다.") + void should_FailToRegister_When_InvalidInput() throws Exception { + // given + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + FcmTokenRequest request = new FcmTokenRequest(); // deviceId와 fcmToken 누락 + + // when + ResultActions result = mockMvc.perform(post("/api/fcm/token") + .with(user(memberDetails)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + // then + result.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(400)); + + then(fcmService).shouldHaveNoInteractions(); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/controller/FileControllerTest.java b/src/test/java/com/potatocake/everymoment/controller/FileControllerTest.java new file mode 100644 index 0000000..e0daf22 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/controller/FileControllerTest.java @@ -0,0 +1,156 @@ +package com.potatocake.everymoment.controller; + +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willDoNothing; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.potatocake.everymoment.dto.response.FileResponse; +import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.security.MemberDetails; +import com.potatocake.everymoment.service.FileService; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +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.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +@WithMockUser +@WebMvcTest(FileController.class) +class FileControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private FileService fileService; + + @Test + @DisplayName("파일 목록이 성공적으로 조회된다.") + void should_ReturnFiles_When_ValidDiaryId() throws Exception { + // given + Long diaryId = 1L; + List responses = List.of( + FileResponse.builder() + .id(1L) + .imageUrl("https://example.com/image1.jpg") + .order(1) + .build(), + FileResponse.builder() + .id(2L) + .imageUrl("https://example.com/image2.jpg") + .order(2) + .build() + ); + + given(fileService.getFiles(diaryId)).willReturn(responses); + + // when + ResultActions result = mockMvc.perform(get("/api/diaries/{diaryId}/files", diaryId)); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")) + .andExpect(jsonPath("$.info").isArray()) + .andExpect(jsonPath("$.info[0].imageUrl").value("https://example.com/image1.jpg")) + .andExpect(jsonPath("$.info[1].imageUrl").value("https://example.com/image2.jpg")); + + then(fileService).should().getFiles(diaryId); + } + + @Test + @DisplayName("파일이 성공적으로 업로드된다.") + void should_UploadFiles_When_ValidInput() throws Exception { + // given + Long diaryId = 1L; + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + MockMultipartFile file1 = new MockMultipartFile( + "files", + "test1.jpg", + MediaType.IMAGE_JPEG_VALUE, + "test image 1".getBytes() + ); + MockMultipartFile file2 = new MockMultipartFile( + "files", + "test2.jpg", + MediaType.IMAGE_JPEG_VALUE, + "test image 2".getBytes() + ); + + willDoNothing().given(fileService).uploadFiles(diaryId, memberId, List.of(file1, file2)); + + // when + ResultActions result = mockMvc.perform(multipart("/api/diaries/{diaryId}/files", diaryId) + .file(file1) + .file(file2) + .with(user(memberDetails)) + .with(csrf())); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")); + + then(fileService).should().uploadFiles(diaryId, memberId, List.of(file1, file2)); + } + + @Test + @DisplayName("파일이 성공적으로 수정된다.") + void should_UpdateFiles_When_ValidInput() throws Exception { + // given + Long diaryId = 1L; + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + MockMultipartFile file = new MockMultipartFile( + "files", + "test.jpg", + MediaType.IMAGE_JPEG_VALUE, + "test image".getBytes() + ); + + willDoNothing().given(fileService).updateFiles(diaryId, memberId, List.of(file)); + + // when + ResultActions result = mockMvc.perform(multipart("/api/diaries/{diaryId}/files", diaryId) + .file(file) + .with(request -> { + request.setMethod("PUT"); + return request; + }) + .with(user(memberDetails)) + .with(csrf())); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")); + + then(fileService).should().updateFiles(diaryId, memberId, List.of(file)); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/controller/FriendControllerTest.java b/src/test/java/com/potatocake/everymoment/controller/FriendControllerTest.java new file mode 100644 index 0000000..a28679a --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/controller/FriendControllerTest.java @@ -0,0 +1,149 @@ +package com.potatocake.everymoment.controller; + +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willDoNothing; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.potatocake.everymoment.dto.response.FriendDiarySimpleResponse; +import com.potatocake.everymoment.dto.response.FriendListResponse; +import com.potatocake.everymoment.dto.response.FriendProfileResponse; +import com.potatocake.everymoment.dto.response.OneFriendDiariesResponse; +import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.security.MemberDetails; +import com.potatocake.everymoment.service.FriendService; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +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.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +@WebMvcTest(FriendController.class) +class FriendControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private FriendService friendService; + + @Test + @DisplayName("특정 친구의 일기 목록이 성공적으로 조회된다.") + void should_ReturnFriendDiaries_When_ValidRequest() throws Exception { + // given + Long memberId = 1L; + Long friendId = 2L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + OneFriendDiariesResponse response = OneFriendDiariesResponse.builder() + .diaries(List.of( + FriendDiarySimpleResponse.builder() + .id(1L) + .content("Test diary") + .locationName("Test location") + .build() + )) + .next(null) + .build(); + + given(friendService.OneFriendDiariesResponse(memberId, friendId, null, 0, 10)) + .willReturn(response); + + // when + ResultActions result = mockMvc.perform(get("/api/friends/{friendId}/diaries", friendId) + .with(user(memberDetails)) + .param("key", "0") + .param("size", "10")); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")) + .andExpect(jsonPath("$.info.diaries[0].content").value("Test diary")); + + then(friendService).should().OneFriendDiariesResponse(memberId, friendId, null, 0, 10); + } + + @Test + @DisplayName("내 친구 목록이 성공적으로 조회된다.") + void should_ReturnFriendList_When_ValidRequest() throws Exception { + // given + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + FriendListResponse response = FriendListResponse.builder() + .friends(List.of( + FriendProfileResponse.builder() + .id(2L) + .nickname("friend") + .profileImageUrl("https://example.com/profile.jpg") + .build() + )) + .next(null) + .build(); + + given(friendService.getFriendList(memberId, null, 0, 10)) + .willReturn(response); + + // when + ResultActions result = mockMvc.perform(get("/api/friends/friends") + .with(user(memberDetails)) + .param("key", "0") + .param("size", "10")); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")) + .andExpect(jsonPath("$.info.friends[0].nickname").value("friend")); + + then(friendService).should().getFriendList(memberId, null, 0, 10); + } + + @Test + @DisplayName("친구가 성공적으로 삭제된다.") + void should_DeleteFriend_When_ValidId() throws Exception { + // given + Long memberId = 1L; + Long friendId = 2L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + willDoNothing().given(friendService).deleteFriend(memberId, friendId); + + // when + ResultActions result = mockMvc.perform(delete("/api/friends/{friendId}", friendId) + .with(user(memberDetails)) + .with(csrf())); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")); + + then(friendService).should().deleteFriend(memberId, friendId); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/controller/FriendRequestControllerTest.java b/src/test/java/com/potatocake/everymoment/controller/FriendRequestControllerTest.java new file mode 100644 index 0000000..a800405 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/controller/FriendRequestControllerTest.java @@ -0,0 +1,188 @@ +package com.potatocake.everymoment.controller; + +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willDoNothing; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.potatocake.everymoment.dto.response.FriendRequestPageRequest; +import com.potatocake.everymoment.dto.response.FriendRequestResponse; +import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.exception.ErrorCode; +import com.potatocake.everymoment.security.MemberDetails; +import com.potatocake.everymoment.service.FriendRequestService; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +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.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +@WebMvcTest(FriendRequestController.class) +class FriendRequestControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private FriendRequestService friendRequestService; + + @Test + @DisplayName("친구 요청 목록이 성공적으로 조회된다.") + void should_ReturnFriendRequests_When_ValidRequest() throws Exception { + // given + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + FriendRequestPageRequest response = FriendRequestPageRequest.builder() + .friendRequests(List.of( + FriendRequestResponse.builder() + .id(1L) + .senderId(2L) + .nickname("requester") + .profileImageUrl("https://example.com/profile.jpg") + .build() + )) + .next(null) + .build(); + + given(friendRequestService.getFriendRequests(null, 10, memberId)) + .willReturn(response); + + // when + ResultActions result = mockMvc.perform(get("/api/friend-requests") + .with(user(memberDetails)) + .param("size", "10")); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")) + .andExpect(jsonPath("$.info.friendRequests[0].nickname").value("requester")); + + then(friendRequestService).should().getFriendRequests(null, 10, memberId); + } + + @Test + @DisplayName("친구 요청이 성공적으로 전송된다.") + void should_SendFriendRequest_When_ValidRequest() throws Exception { + // given + Long memberId = 1L; + Long targetMemberId = 2L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + willDoNothing().given(friendRequestService).sendFriendRequest(memberId, targetMemberId); + + // when + ResultActions result = mockMvc.perform(post("/api/members/{memberId}/friend-requests", targetMemberId) + .with(user(memberDetails)) + .with(csrf())); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")); + + then(friendRequestService).should().sendFriendRequest(memberId, targetMemberId); + } + + @Test + @DisplayName("자신에게 친구 요청을 보내면 실패한다.") + void should_FailToSendRequest_When_RequestingSelf() throws Exception { + // given + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + // when + ResultActions result = mockMvc.perform(post("/api/members/{memberId}/friend-requests", memberId) + .with(user(memberDetails)) + .with(csrf())); + + // then + result.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(400)) + .andExpect(jsonPath("$.message").value(ErrorCode.SELF_FRIEND_REQUEST.getMessage())); + + then(friendRequestService).shouldHaveNoInteractions(); + } + + @Test + @DisplayName("친구 요청이 성공적으로 수락된다.") + void should_AcceptFriendRequest_When_ValidRequest() throws Exception { + // given + Long memberId = 1L; + Long requestId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + willDoNothing().given(friendRequestService).acceptFriendRequest(requestId, memberId); + + // when + ResultActions result = mockMvc.perform(post("/api/friend-requests/{requestId}/accept", requestId) + .with(user(memberDetails)) + .with(csrf())); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")); + + then(friendRequestService).should().acceptFriendRequest(requestId, memberId); + } + + @Test + @DisplayName("친구 요청이 성공적으로 거절된다.") + void should_RejectFriendRequest_When_ValidRequest() throws Exception { + // given + Long memberId = 1L; + Long requestId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + willDoNothing().given(friendRequestService).rejectFriendRequest(requestId, memberId); + + // when + ResultActions result = mockMvc.perform(delete("/api/friend-requests/{requestId}/reject", requestId) + .with(user(memberDetails)) + .with(csrf())); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")); + + then(friendRequestService).should().rejectFriendRequest(requestId, memberId); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/controller/LikeControllerTest.java b/src/test/java/com/potatocake/everymoment/controller/LikeControllerTest.java new file mode 100644 index 0000000..8476f1e --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/controller/LikeControllerTest.java @@ -0,0 +1,87 @@ +package com.potatocake.everymoment.controller; + +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willDoNothing; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.potatocake.everymoment.dto.response.LikeCountResponse; +import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.security.MemberDetails; +import com.potatocake.everymoment.service.LikeService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +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.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +@WithMockUser +@WebMvcTest(LikeController.class) +class LikeControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private LikeService likeService; + + @Test + @DisplayName("좋아요 수가 성공적으로 조회된다.") + void should_ReturnLikeCount_When_ValidDiaryId() throws Exception { + // given + Long diaryId = 1L; + LikeCountResponse response = LikeCountResponse.builder() + .likeCount(5L) + .build(); + + given(likeService.getLikeCount(diaryId)).willReturn(response); + + // when + ResultActions result = mockMvc.perform(get("/api/diaries/{diaryId}/likes", diaryId)); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")) + .andExpect(jsonPath("$.info.likeCount").value(5)); + + then(likeService).should().getLikeCount(diaryId); + } + + @Test + @DisplayName("좋아요가 성공적으로 토글된다.") + void should_ToggleLike_When_ValidRequest() throws Exception { + // given + Long diaryId = 1L; + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + willDoNothing().given(likeService).toggleLike(memberId, diaryId); + + // when + ResultActions result = mockMvc.perform(post("/api/diaries/{diaryId}/likes", diaryId) + .with(user(memberDetails)) + .with(csrf())); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")); + + then(likeService).should().toggleLike(memberId, diaryId); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/controller/MemberControllerTest.java b/src/test/java/com/potatocake/everymoment/controller/MemberControllerTest.java index d24b883..4e8f2f7 100644 --- a/src/test/java/com/potatocake/everymoment/controller/MemberControllerTest.java +++ b/src/test/java/com/potatocake/everymoment/controller/MemberControllerTest.java @@ -1,36 +1,38 @@ package com.potatocake.everymoment.controller; -import static com.potatocake.everymoment.exception.ErrorCode.INFO_REQUIRED; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.BDDMockito.willDoNothing; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.potatocake.everymoment.dto.response.AnonymousLoginResponse; +import com.potatocake.everymoment.dto.response.FriendRequestStatus; import com.potatocake.everymoment.dto.response.MemberDetailResponse; import com.potatocake.everymoment.dto.response.MemberMyResponse; import com.potatocake.everymoment.dto.response.MemberSearchResponse; +import com.potatocake.everymoment.dto.response.MemberSearchResultResponse; import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.exception.ErrorCode; import com.potatocake.everymoment.security.MemberDetails; import com.potatocake.everymoment.service.MemberService; +import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; 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.MediaType; import org.springframework.mock.web.MockMultipartFile; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; -import org.springframework.web.multipart.MultipartFile; @WithMockUser @WebMvcTest(MemberController.class) @@ -39,103 +41,169 @@ class MemberControllerTest { @Autowired private MockMvc mockMvc; + @Autowired + private ObjectMapper objectMapper; + @MockBean private MemberService memberService; @Test - @DisplayName("회원 목록 검색이 성공적으로 수행된다.") - void should_SearchMembers_When_ValidInput() throws Exception { + @DisplayName("익명 로그인이 성공적으로 수행된다.") + void should_LoginAnonymously_When_ValidRequest() throws Exception { // given - String nickname = "testUser"; - Long key = 1L; - int size = 10; + Long memberNumber = null; + AnonymousLoginResponse response = AnonymousLoginResponse.builder() + .number(1234L) + .token("jwt-token") + .build(); - Long memberId = 1L; - MemberDetails memberDetails = createMemberDetails(memberId, 1234L, "test"); + given(memberService.anonymousLogin(memberNumber)).willReturn(response); + + // when + ResultActions result = mockMvc.perform(get("/api/members/anonymous-login")); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")) + .andExpect(jsonPath("$.info.number").value(1234)) + .andExpect(jsonPath("$.info.token").value("jwt-token")); + + then(memberService).should().anonymousLogin(memberNumber); + } - MemberSearchResponse response = MemberSearchResponse.builder().build(); + @Test + @DisplayName("회원 검색이 성공적으로 수행된다.") + void should_SearchMembers_When_ValidRequest() throws Exception { + // given + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + MemberSearchResponse response = MemberSearchResponse.builder() + .members(List.of( + MemberSearchResultResponse.builder() + .id(2L) + .nickname("searchResult") + .profileImageUrl("https://example.com/profile.jpg") + .friendRequestStatus(FriendRequestStatus.NONE) + .build() + )) + .next(null) + .build(); - given(memberService.searchMembers(nickname, key, size, memberId)).willReturn(response); + given(memberService.searchMembers(null, null, 10, memberId)) + .willReturn(response); // when ResultActions result = mockMvc.perform(get("/api/members") .with(user(memberDetails)) - .param("nickname", nickname) - .param("key", key.toString()) - .param("size", String.valueOf(size))); + .param("size", "10")); // then - result - .andExpect(status().isOk()) + result.andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) - .andExpect(jsonPath("$.message").value("success")); + .andExpect(jsonPath("$.message").value("success")) + .andExpect(jsonPath("$.info.members[0].nickname").value("searchResult")); - then(memberService).should().searchMembers(nickname, key, size, memberId); + then(memberService).should().searchMembers(null, null, 10, memberId); } @Test - @DisplayName("내 정보 조회가 성공적으로 수행된다.") - void should_ReturnMyInfo_When_ValidMember() throws Exception { + @DisplayName("내 정보가 성공적으로 조회된다.") + void should_ReturnMyInfo_When_ValidRequest() throws Exception { // given Long memberId = 1L; - MemberDetails memberDetails = createMemberDetails(memberId, 1234L, "test"); + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + MemberDetailResponse response = MemberDetailResponse.builder() + .id(memberId) + .nickname("testUser") + .profileImageUrl("https://example.com/profile.jpg") + .build(); - MemberDetailResponse response = MemberDetailResponse.builder().build(); given(memberService.getMyInfo(memberId)).willReturn(response); // when - ResultActions result = performGet("/api/members/me", memberDetails); + ResultActions result = mockMvc.perform(get("/api/members/me") + .with(user(memberDetails))); // then - result - .andExpect(status().isOk()) + result.andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.message").value("success")) - .andExpect(jsonPath("$.info").isNotEmpty()); + .andExpect(jsonPath("$.info.nickname").value("testUser")); then(memberService).should().getMyInfo(memberId); } @Test - @DisplayName("회원 ID로 정보 조회가 성공적으로 수행된다.") - void should_ReturnMemberInfo_When_ValidMemberId() throws Exception { + @DisplayName("회원 정보가 성공적으로 조회된다.") + void should_ReturnMemberInfo_When_ValidId() throws Exception { // given - Long memberId = 1L; - MemberMyResponse response = MemberMyResponse.builder().build(); + Long targetMemberId = 2L; + MemberMyResponse response = MemberMyResponse.builder() + .id(targetMemberId) + .nickname("otherUser") + .profileImageUrl("https://example.com/profile.jpg") + .build(); - given(memberService.getMemberInfo(memberId)).willReturn(response); + given(memberService.getMemberInfo(targetMemberId)).willReturn(response); // when - ResultActions result = mockMvc.perform(get("/api/members/{memberId}", memberId)); + ResultActions result = mockMvc.perform(get("/api/members/{memberId}", targetMemberId)); // then - result - .andExpect(status().isOk()) + result.andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.message").value("success")) - .andExpect(jsonPath("$.info").isNotEmpty()); + .andExpect(jsonPath("$.info.nickname").value("otherUser")); - then(memberService).should().getMemberInfo(memberId); + then(memberService).should().getMemberInfo(targetMemberId); } @Test - @DisplayName("회원 정보 수정이 성공적으로 수행된다.") + @DisplayName("회원 정보가 성공적으로 수정된다.") void should_UpdateMemberInfo_When_ValidInput() throws Exception { // given Long memberId = 1L; - MemberDetails memberDetails = createMemberDetails(memberId, 1234L, "test"); + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + MockMultipartFile profileImage = new MockMultipartFile( + "profileImage", + "profile.jpg", + MediaType.IMAGE_JPEG_VALUE, + "test image".getBytes() + ); - MockMultipartFile profileImage = new MockMultipartFile("profileImage", "image.png", "image/png", new byte[]{}); String nickname = "newNickname"; - willDoNothing().given(memberService).updateMemberInfo(anyLong(), any(MultipartFile.class), anyString()); + willDoNothing().given(memberService) + .updateMemberInfo(memberId, profileImage, nickname); // when - ResultActions result = performMultipart("/api/members", profileImage, nickname, memberDetails); + ResultActions result = mockMvc.perform(multipart("/api/members") + .file(profileImage) + .param("nickname", nickname) + .with(user(memberDetails)) + .with(csrf())); // then - result - .andExpect(status().isOk()) + result.andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.message").value("success")); @@ -143,47 +211,55 @@ void should_UpdateMemberInfo_When_ValidInput() throws Exception { } @Test - @DisplayName("프로필 이미지와 닉네임이 모두 누락되면 예외가 발생한다.") - void should_ThrowException_When_ProfileImageAndNicknameAreMissing() throws Exception { + @DisplayName("회원이 성공적으로 삭제된다.") + void should_DeleteMember_When_ValidRequest() throws Exception { + // given + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + willDoNothing().given(memberService).deleteMember(memberId); + // when - ResultActions result = mockMvc.perform(multipart("/api/members") + ResultActions result = mockMvc.perform(delete("/api/members") + .with(user(memberDetails)) .with(csrf())); // then - result - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value(INFO_REQUIRED.getStatus().value())) - .andExpect(jsonPath("$.message").value(INFO_REQUIRED.getMessage())) - .andDo(print()); + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")); - then(memberService).shouldHaveNoInteractions(); + then(memberService).should().deleteMember(memberId); } - private Member createMember(Long memberId, Long number, String nickname) { - return Member.builder() + @Test + @DisplayName("정보가 누락되면 회원 정보 수정에 실패한다.") + void should_FailToUpdate_When_InfoMissing() throws Exception { + // given + Long memberId = 1L; + Member member = Member.builder() .id(memberId) - .number(number) - .nickname(nickname) + .number(1234L) + .nickname("testUser") .build(); - } - - private MemberDetails createMemberDetails(Long memberId, Long number, String nickname) { - Member member = createMember(memberId, number, nickname); - return new MemberDetails(member); - } - - private ResultActions performGet(String url, MemberDetails memberDetails) throws Exception { - return mockMvc.perform(get(url) - .with(user(memberDetails))); - } + MemberDetails memberDetails = new MemberDetails(member); - private ResultActions performMultipart(String url, MockMultipartFile file, String nickname, - MemberDetails memberDetails) throws Exception { - return mockMvc.perform(multipart(url) - .file(file) - .param("nickname", nickname) + // when + ResultActions result = mockMvc.perform(multipart("/api/members") .with(user(memberDetails)) .with(csrf())); + + // then + result.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(400)) + .andExpect(jsonPath("$.message").value(ErrorCode.INFO_REQUIRED.getMessage())); + + then(memberService).shouldHaveNoInteractions(); } } diff --git a/src/test/java/com/potatocake/everymoment/controller/NotificationControllerTest.java b/src/test/java/com/potatocake/everymoment/controller/NotificationControllerTest.java new file mode 100644 index 0000000..81fda18 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/controller/NotificationControllerTest.java @@ -0,0 +1,115 @@ +package com.potatocake.everymoment.controller; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willDoNothing; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.potatocake.everymoment.dto.response.NotificationListResponse; +import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.security.MemberDetails; +import com.potatocake.everymoment.service.NotificationService; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +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.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +@WebMvcTest(NotificationController.class) +class NotificationControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private NotificationService notificationService; + + @Test + @DisplayName("알림 목록이 성공적으로 조회된다.") + void should_GetNotifications_When_ValidRequest() throws Exception { + // given + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + List responses = List.of( + NotificationListResponse.builder() + .id(1L) + .content("Notification 1") + .type("TEST1") + .targetId(1L) + .isRead(false) + .createdAt(LocalDateTime.now()) + .build(), + NotificationListResponse.builder() + .id(2L) + .content("Notification 2") + .type("TEST2") + .targetId(2L) + .isRead(true) + .createdAt(LocalDateTime.now()) + .build() + ); + + given(notificationService.getNotifications(memberId)).willReturn(responses); + + // when + ResultActions result = mockMvc.perform(get("/api/notifications") + .with(user(memberDetails))); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")) + .andExpect(jsonPath("$.info").isArray()) + .andExpect(jsonPath("$.info[0].content").value("Notification 1")) + .andExpect(jsonPath("$.info[0].read").value(false)) + .andExpect(jsonPath("$.info[1].content").value("Notification 2")) + .andExpect(jsonPath("$.info[1].read").value(true)); + + then(notificationService).should().getNotifications(memberId); + } + + @Test + @DisplayName("알림이 성공적으로 읽음 처리된다.") + void should_UpdateNotification_When_ValidId() throws Exception { + // given + Long memberId = 1L; + Long notificationId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + willDoNothing().given(notificationService).updateNotification(memberId, notificationId); + + // when + ResultActions result = mockMvc.perform(patch("/api/notifications/{notificationId}", notificationId) + .with(user(memberDetails)) + .with(csrf())); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")); + + then(notificationService).should().updateNotification(eq(memberId), eq(notificationId)); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/entity/CategoryTest.java b/src/test/java/com/potatocake/everymoment/entity/CategoryTest.java new file mode 100644 index 0000000..22330b6 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/entity/CategoryTest.java @@ -0,0 +1,51 @@ +package com.potatocake.everymoment.entity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.potatocake.everymoment.exception.ErrorCode; +import com.potatocake.everymoment.exception.GlobalException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CategoryTest { + + @Test + @DisplayName("카테고리명이 성공적으로 업데이트된다.") + void should_UpdateName_When_NewNameProvided() { + // given + Category category = Category.builder() + .categoryName("Original Name") + .build(); + + // when + category.update("New Name"); + + // then + assertThat(category.getCategoryName()).isEqualTo("New Name"); + } + + @Test + @DisplayName("소유자 확인이 성공적으로 수행된다.") + void should_CheckOwner_When_VerifyingOwnership() { + // given + Long ownerId = 1L; + Member owner = Member.builder() + .id(ownerId) + .build(); + + Category category = Category.builder() + .member(owner) + .build(); + + // when & then + assertThatCode(() -> category.checkOwner(ownerId)) + .doesNotThrowAnyException(); + + assertThatThrownBy(() -> category.checkOwner(2L)) + .isInstanceOf(GlobalException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.CATEGORY_NOT_OWNER); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/entity/CommentTest.java b/src/test/java/com/potatocake/everymoment/entity/CommentTest.java new file mode 100644 index 0000000..1a9b07c --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/entity/CommentTest.java @@ -0,0 +1,41 @@ +package com.potatocake.everymoment.entity; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CommentTest { + + @Test + @DisplayName("댓글 내용이 성공적으로 업데이트된다.") + void should_UpdateContent_When_NewContentProvided() { + // given + Comment comment = Comment.builder() + .content("Original content") + .build(); + String newContent = "Updated content"; + + // when + comment.updateContent(newContent); + + // then + assertThat(comment.getContent()).isEqualTo(newContent); + } + + @Test + @DisplayName("null 내용으로 업데이트하면 기존 내용이 유지된다.") + void should_KeepOriginalContent_When_NullContentProvided() { + // given + Comment comment = Comment.builder() + .content("Original content") + .build(); + + // when + comment.updateContent(null); + + // then + assertThat(comment.getContent()).isEqualTo("Original content"); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/entity/DeviceTokenTest.java b/src/test/java/com/potatocake/everymoment/entity/DeviceTokenTest.java new file mode 100644 index 0000000..ff5ab34 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/entity/DeviceTokenTest.java @@ -0,0 +1,27 @@ +package com.potatocake.everymoment.entity; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class DeviceTokenTest { + + @DisplayName("토큰값이 성공적으로 업데이트된다.") + @Test + void should_UpdateToken_When_NewTokenProvided() { + // given + DeviceToken deviceToken = DeviceToken.builder() + .fcmToken("old-token") + .deviceId("device123") + .build(); + String newToken = "new-token"; + + // when + deviceToken.updateToken(newToken); + + // then + assertThat(deviceToken.getFcmToken()).isEqualTo(newToken); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/entity/DiaryTest.java b/src/test/java/com/potatocake/everymoment/entity/DiaryTest.java new file mode 100644 index 0000000..6a2cf8f --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/entity/DiaryTest.java @@ -0,0 +1,160 @@ +package com.potatocake.everymoment.entity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.locationtech.jts.geom.Point; + +class DiaryTest { + + @Test + @DisplayName("북마크 상태가 성공적으로 토글된다.") + void should_ToggleBookmark_When_Called() { + // given + Diary diary = Diary.builder() + .isBookmark(false) + .build(); + + // when + diary.toggleBookmark(); + + // then + assertThat(diary.isBookmark()).isTrue(); + + // when + diary.toggleBookmark(); + + // then + assertThat(diary.isBookmark()).isFalse(); + } + + @Test + @DisplayName("공개 상태가 성공적으로 토글된다.") + void should_TogglePrivacy_When_Called() { + // given + Diary diary = Diary.builder() + .isPublic(false) + .build(); + + // when + diary.togglePublic(); + + // then + assertThat(diary.isPublic()).isTrue(); + + // when + diary.togglePublic(); + + // then + assertThat(diary.isPublic()).isFalse(); + } + + @Test + @DisplayName("일기 내용이 성공적으로 업데이트된다.") + void should_UpdateContent_When_NewContentProvided() { + // given + Diary diary = Diary.builder() + .content("Original content") + .build(); + String newContent = "Updated content"; + + // when + diary.updateContent(newContent); + + // then + assertThat(diary.getContent()).isEqualTo(newContent); + } + + @Test + @DisplayName("location 정보가 성공적으로 업데이트된다.") + void should_UpdateLocation_When_NewLocationProvided() { + // given + Point originalPoint = mock(Point.class); + Diary diary = Diary.builder() + .locationPoint(originalPoint) + .locationName("Original location") + .address("Original address") + .build(); + + Point newPoint = mock(Point.class); + String newLocationName = "New location"; + String newAddress = "New address"; + + // when + diary.updateLocationPoint(newPoint); + diary.updateLocationName(newLocationName); + diary.updateAddress(newAddress); + + // then + assertThat(diary.getLocationPoint()).isEqualTo(newPoint); + assertThat(diary.getLocationName()).isEqualTo(newLocationName); + assertThat(diary.getAddress()).isEqualTo(newAddress); + } + + @Test + @DisplayName("이모지가 성공적으로 업데이트된다.") + void should_UpdateEmoji_When_NewEmojiProvided() { + // given + Diary diary = Diary.builder() + .emoji("😊") + .build(); + String newEmoji = "😍"; + + // when + diary.updateEmoji(newEmoji); + + // then + assertThat(diary.getEmoji()).isEqualTo(newEmoji); + } + + @Test + @DisplayName("작성자 확인이 성공적으로 수행된다.") + void should_CheckOwner_When_VerifyingOwnership() { + // given + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + + Diary diary = Diary.builder() + .member(member) + .build(); + + // when & then + assertThat(diary.checkOwner(memberId)).isTrue(); + assertThat(diary.checkOwner(2L)).isFalse(); + } + + @Test + @DisplayName("내용이 성공적으로 null로 업데이트된다.") + void should_UpdateContentNull_When_Called() { + // given + Diary diary = Diary.builder() + .content("Original content") + .build(); + + // when + diary.updateContentNull(); + + // then + assertThat(diary.getContent()).isNull(); + } + + @Test + @DisplayName("이모지가 성공적으로 null로 업데이트된다.") + void should_UpdateEmojiNull_When_Called() { + // given + Diary diary = Diary.builder() + .emoji("😊") + .build(); + + // when + diary.updateEmojiNull(); + + // then + assertThat(diary.getEmoji()).isNull(); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/entity/FileTest.java b/src/test/java/com/potatocake/everymoment/entity/FileTest.java new file mode 100644 index 0000000..e6bfe21 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/entity/FileTest.java @@ -0,0 +1,35 @@ +package com.potatocake.everymoment.entity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class FileTest { + + @DisplayName("파일이 성공적으로 생성된다.") + @Test + void should_CreateFile_When_ValidInput() { + // given + Long id = 1L; + Diary diary = mock(Diary.class); + String imageUrl = "https://example.com/image.jpg"; + Integer order = 1; + + // when + File file = File.builder() + .id(id) + .diary(diary) + .imageUrl(imageUrl) + .order(order) + .build(); + + // then + assertThat(file.getId()).isEqualTo(id); + assertThat(file.getDiary()).isEqualTo(diary); + assertThat(file.getImageUrl()).isEqualTo(imageUrl); + assertThat(file.getOrder()).isEqualTo(order); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/entity/FriendRequestTest.java b/src/test/java/com/potatocake/everymoment/entity/FriendRequestTest.java new file mode 100644 index 0000000..83531ff --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/entity/FriendRequestTest.java @@ -0,0 +1,34 @@ +package com.potatocake.everymoment.entity; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class FriendRequestTest { + + @Test + @DisplayName("친구 요청이 성공적으로 생성된다.") + void should_CreateFriendRequest_When_ValidInput() { + // given + Member sender = Member.builder() + .id(1L) + .nickname("sender") + .build(); + Member receiver = Member.builder() + .id(2L) + .nickname("receiver") + .build(); + + // when + FriendRequest request = FriendRequest.builder() + .sender(sender) + .receiver(receiver) + .build(); + + // then + assertThat(request.getSender()).isEqualTo(sender); + assertThat(request.getReceiver()).isEqualTo(receiver); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/entity/FriendTest.java b/src/test/java/com/potatocake/everymoment/entity/FriendTest.java new file mode 100644 index 0000000..badb6a6 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/entity/FriendTest.java @@ -0,0 +1,34 @@ +package com.potatocake.everymoment.entity; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class FriendTest { + + @DisplayName("친구 관계가 성공적으로 생성된다.") + @Test + void should_CreateFriendship_When_ValidInput() { + // given + Member member = Member.builder() + .id(1L) + .nickname("user") + .build(); + Member friend = Member.builder() + .id(2L) + .nickname("friend") + .build(); + + // when + Friend friendship = Friend.builder() + .member(member) + .friend(friend) + .build(); + + // then + assertThat(friendship.getMember()).isEqualTo(member); + assertThat(friendship.getFriend()).isEqualTo(friend); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/entity/LikeTest.java b/src/test/java/com/potatocake/everymoment/entity/LikeTest.java new file mode 100644 index 0000000..c3a664c --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/entity/LikeTest.java @@ -0,0 +1,34 @@ +package com.potatocake.everymoment.entity; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class LikeTest { + + @Test + @DisplayName("좋아요가 성공적으로 생성된다.") + void should_CreateLike_When_ValidInput() { + // given + Member member = Member.builder() + .id(1L) + .nickname("user") + .build(); + Diary diary = Diary.builder() + .id(1L) + .content("Test diary") + .build(); + + // when + Like like = Like.builder() + .member(member) + .diary(diary) + .build(); + + // then + assertThat(like.getMember()).isEqualTo(member); + assertThat(like.getDiary()).isEqualTo(diary); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/entity/MemberTest.java b/src/test/java/com/potatocake/everymoment/entity/MemberTest.java new file mode 100644 index 0000000..de6bb1a --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/entity/MemberTest.java @@ -0,0 +1,86 @@ +package com.potatocake.everymoment.entity; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class MemberTest { + + @Test + @DisplayName("회원 정보가 성공적으로 생성된다.") + void should_CreateMember_When_ValidInput() { + // given + Long id = 1L; + Long number = 1234L; + String nickname = "testUser"; + String profileImageUrl = "https://example.com/profile.jpg"; + + // when + Member member = Member.builder() + .id(id) + .number(number) + .nickname(nickname) + .profileImageUrl(profileImageUrl) + .build(); + + // then + assertThat(member.getId()).isEqualTo(id); + assertThat(member.getNumber()).isEqualTo(number); + assertThat(member.getNickname()).isEqualTo(nickname); + assertThat(member.getProfileImageUrl()).isEqualTo(profileImageUrl); + assertThat(member.isDeleted()).isFalse(); + } + + @Test + @DisplayName("회원 정보가 성공적으로 업데이트된다.") + void should_UpdateInfo_When_NewValuesProvided() { + // given + Member member = Member.builder() + .nickname("oldNickname") + .profileImageUrl("https://example.com/old.jpg") + .build(); + + // when + member.update("newNickname", "https://example.com/new.jpg"); + + // then + assertThat(member.getNickname()).isEqualTo("newNickname"); + assertThat(member.getProfileImageUrl()).isEqualTo("https://example.com/new.jpg"); + } + + @Test + @DisplayName("닉네임만 업데이트된다.") + void should_UpdateNickname_When_OnlyNicknameProvided() { + // given + Member member = Member.builder() + .nickname("oldNickname") + .profileImageUrl("https://example.com/profile.jpg") + .build(); + + // when + member.update("newNickname", null); + + // then + assertThat(member.getNickname()).isEqualTo("newNickname"); + assertThat(member.getProfileImageUrl()).isEqualTo("https://example.com/profile.jpg"); + } + + @Test + @DisplayName("프로필 이미지만 업데이트된다.") + void should_UpdateProfileImage_When_OnlyProfileImageProvided() { + // given + Member member = Member.builder() + .nickname("testUser") + .profileImageUrl("https://example.com/old.jpg") + .build(); + + // when + member.update(null, "https://example.com/new.jpg"); + + // then + assertThat(member.getNickname()).isEqualTo("testUser"); + assertThat(member.getProfileImageUrl()).isEqualTo("https://example.com/new.jpg"); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/entity/NotificationTest.java b/src/test/java/com/potatocake/everymoment/entity/NotificationTest.java new file mode 100644 index 0000000..536e8db --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/entity/NotificationTest.java @@ -0,0 +1,51 @@ +package com.potatocake.everymoment.entity; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class NotificationTest { + + @Test + @DisplayName("알림이 성공적으로 생성된다.") + void should_CreateNotification_When_ValidInput() { + // given + Member member = Member.builder() + .id(1L) + .build(); + + // when + Notification notification = Notification.builder() + .member(member) + .content("Test notification") + .type("TEST") + .targetId(1L) + .isRead(false) + .build(); + + // then + assertThat(notification.getMember()).isEqualTo(member); + assertThat(notification.getContent()).isEqualTo("Test notification"); + assertThat(notification.getType()).isEqualTo("TEST"); + assertThat(notification.getTargetId()).isEqualTo(1L); + assertThat(notification.isRead()).isFalse(); + } + + @Test + @DisplayName("알림이 성공적으로 읽음 처리된다.") + void should_MarkAsRead_When_UpdateIsRead() { + // given + Notification notification = Notification.builder() + .content("Test notification") + .isRead(false) + .build(); + + // when + notification.updateIsRead(); + + // then + assertThat(notification.isRead()).isTrue(); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/repository/CategoryRepositoryTest.java b/src/test/java/com/potatocake/everymoment/repository/CategoryRepositoryTest.java new file mode 100644 index 0000000..34bb2ff --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/repository/CategoryRepositoryTest.java @@ -0,0 +1,79 @@ +package com.potatocake.everymoment.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.potatocake.everymoment.entity.Category; +import com.potatocake.everymoment.entity.Member; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.TestPropertySource; + +@TestPropertySource(properties = "spring.jpa.hibernate.ddl-auto=create-drop") +@DataJpaTest +class CategoryRepositoryTest { + + @Autowired + private CategoryRepository categoryRepository; + + @Autowired + private MemberRepository memberRepository; + + private Member createAndSaveMember() { + Member member = Member.builder() + .number(1234L) + .nickname("testUser") + .profileImageUrl("https://example.com/image.jpg") + .build(); + return memberRepository.save(member); + } + + @Test + @DisplayName("카테고리가 성공적으로 저장된다.") + void should_SaveCategory_When_ValidEntity() { + // given + Member member = createAndSaveMember(); + Category category = Category.builder() + .member(member) + .categoryName("Test Category") + .build(); + + // when + Category savedCategory = categoryRepository.save(category); + + // then + assertThat(savedCategory.getId()).isNotNull(); + assertThat(savedCategory.getCategoryName()).isEqualTo("Test Category"); + assertThat(savedCategory.getMember()).isEqualTo(member); + } + + @Test + @DisplayName("회원의 카테고리 목록이 성공적으로 조회된다.") + void should_FindCategories_When_FilteringByMemberId() { + // given + Member member = createAndSaveMember(); + + Category category1 = Category.builder() + .member(member) + .categoryName("Category 1") + .build(); + + Category category2 = Category.builder() + .member(member) + .categoryName("Category 2") + .build(); + + categoryRepository.saveAll(List.of(category1, category2)); + + // when + List categories = categoryRepository.findByMemberId(member.getId()); + + // then + assertThat(categories).hasSize(2); + assertThat(categories).extracting("categoryName") + .containsExactlyInAnyOrder("Category 1", "Category 2"); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/repository/CommentRepositoryTest.java b/src/test/java/com/potatocake/everymoment/repository/CommentRepositoryTest.java new file mode 100644 index 0000000..c2ec107 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/repository/CommentRepositoryTest.java @@ -0,0 +1,240 @@ +package com.potatocake.everymoment.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.potatocake.everymoment.entity.Comment; +import com.potatocake.everymoment.entity.Diary; +import com.potatocake.everymoment.entity.Member; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.test.context.TestPropertySource; + +@TestPropertySource(properties = "spring.jpa.hibernate.ddl-auto=create-drop") +@DataJpaTest +class CommentRepositoryTest { + + @Autowired + private CommentRepository commentRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private DiaryRepository diaryRepository; + + @Test + @DisplayName("댓글이 성공적으로 저장된다.") + void should_SaveComment_When_ValidEntity() { + // given + Member member = createAndSaveMember(); + Diary diary = createAndSaveDiary(member); + + Comment comment = Comment.builder() + .member(member) + .diary(diary) + .content("Test comment") + .build(); + + // when + Comment savedComment = commentRepository.save(comment); + + // then + assertThat(savedComment.getId()).isNotNull(); + assertThat(savedComment.getContent()).isEqualTo("Test comment"); + assertThat(savedComment.getMember()).isEqualTo(member); + assertThat(savedComment.getDiary()).isEqualTo(diary); + } + + @Test + @DisplayName("일기의 댓글 목록이 성공적으로 조회된다.") + void should_FindComments_When_FilteringByDiaryId() { + // given + Member member = createAndSaveMember(); + Diary diary = createAndSaveDiary(member); + + Comment comment1 = Comment.builder() + .member(member) + .diary(diary) + .content("Comment 1") + .build(); + + Comment comment2 = Comment.builder() + .member(member) + .diary(diary) + .content("Comment 2") + .build(); + + commentRepository.saveAll(List.of(comment1, comment2)); + + // when + Page comments = commentRepository.findAllByDiaryId( + diary.getId(), + PageRequest.of(0, 10) + ); + + // then + assertThat(comments.getContent()).hasSize(2); + assertThat(comments.getContent()) + .extracting("content") + .containsExactly("Comment 1", "Comment 2"); + } + + @Test + @DisplayName("페이징이 성공적으로 동작한다.") + void should_ReturnPagedResult_When_UsingPagination() { + // given + Member member = createAndSaveMember(); + Diary diary = createAndSaveDiary(member); + + List comments = List.of( + Comment.builder() + .member(member) + .diary(diary) + .content("Comment 1") + .build(), + Comment.builder() + .member(member) + .diary(diary) + .content("Comment 2") + .build(), + Comment.builder() + .member(member) + .diary(diary) + .content("Comment 3") + .build() + ); + + commentRepository.saveAll(comments); + + // when + Page firstPage = commentRepository.findAllByDiaryId( + diary.getId(), + PageRequest.of(0, 2) + ); + + Page secondPage = commentRepository.findAllByDiaryId( + diary.getId(), + PageRequest.of(1, 2) + ); + + // then + assertThat(firstPage.getContent()).hasSize(2); + assertThat(firstPage.hasNext()).isTrue(); + assertThat(firstPage.getContent()) + .extracting("content") + .containsExactly("Comment 1", "Comment 2"); + + assertThat(secondPage.getContent()).hasSize(1); + assertThat(secondPage.hasNext()).isFalse(); + assertThat(secondPage.getContent()) + .extracting("content") + .containsExactly("Comment 3"); + } + + @Test + @DisplayName("다른 일기의 댓글은 조회되지 않는다.") + void should_NotFindComments_When_DifferentDiary() { + // given + Member member = createAndSaveMember(); + Diary diary1 = createAndSaveDiary(member); + Diary diary2 = createAndSaveDiary(member); + + Comment comment1 = Comment.builder() + .member(member) + .diary(diary1) + .content("Comment for diary 1") + .build(); + + Comment comment2 = Comment.builder() + .member(member) + .diary(diary2) + .content("Comment for diary 2") + .build(); + + commentRepository.saveAll(List.of(comment1, comment2)); + + // when + Page comments = commentRepository.findAllByDiaryId( + diary1.getId(), + PageRequest.of(0, 10) + ); + + // then + assertThat(comments.getContent()).hasSize(1); + assertThat(comments.getContent()) + .extracting("content") + .containsExactly("Comment for diary 1"); + } + + @Test + @DisplayName("빈 페이지가 성공적으로 반환된다.") + void should_ReturnEmptyPage_When_NoComments() { + // given + Member member = createAndSaveMember(); + Diary diary = createAndSaveDiary(member); + + // when + Page comments = commentRepository.findAllByDiaryId( + diary.getId(), + PageRequest.of(0, 10) + ); + + // then + assertThat(comments.getContent()).isEmpty(); + assertThat(comments.getTotalElements()).isZero(); + assertThat(comments.hasNext()).isFalse(); + } + + @Test + @DisplayName("댓글이 성공적으로 삭제된다.") + void should_DeleteComment_When_ValidEntity() { + // given + Member member = createAndSaveMember(); + Diary diary = createAndSaveDiary(member); + Comment comment = Comment.builder() + .member(member) + .diary(diary) + .content("Test comment") + .build(); + Comment savedComment = commentRepository.save(comment); + + // when + commentRepository.delete(savedComment); + + // then + Optional foundComment = commentRepository.findById(savedComment.getId()); + assertThat(foundComment).isEmpty(); + } + + private Member createAndSaveMember() { + Member member = Member.builder() + .number(1234L) + .nickname("testUser") + .profileImageUrl("https://example.com/image.jpg") + .build(); + return memberRepository.save(member); + } + + private Diary createAndSaveDiary(Member member) { + Point point = new GeometryFactory().createPoint(new Coordinate(37.5665, 126.978)); + + Diary diary = Diary.builder() + .member(member) + .content("Test diary") + .locationName("Test location") + .address("Test address") + .locationPoint(point) + .build(); + return diaryRepository.save(diary); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/repository/DeviceTokenRepositoryTest.java b/src/test/java/com/potatocake/everymoment/repository/DeviceTokenRepositoryTest.java new file mode 100644 index 0000000..99ee770 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/repository/DeviceTokenRepositoryTest.java @@ -0,0 +1,210 @@ +package com.potatocake.everymoment.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.potatocake.everymoment.entity.DeviceToken; +import com.potatocake.everymoment.entity.Member; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.TestPropertySource; + +@TestPropertySource(properties = "spring.jpa.hibernate.ddl-auto=create-drop") +@DataJpaTest +class DeviceTokenRepositoryTest { + + @Autowired + private DeviceTokenRepository deviceTokenRepository; + + @Autowired + private MemberRepository memberRepository; + + private Member createAndSaveMember() { + Member member = Member.builder() + .number(1234L) + .nickname("testUser") + .profileImageUrl("https://example.com/image.jpg") + .build(); + return memberRepository.save(member); + } + + @Test + @DisplayName("디바이스 토큰이 성공적으로 저장된다.") + void should_SaveToken_When_ValidEntity() { + // given + Member member = createAndSaveMember(); + DeviceToken token = DeviceToken.builder() + .member(member) + .deviceId("device123") + .fcmToken("fcm-token-123") + .build(); + + // when + DeviceToken savedToken = deviceTokenRepository.save(token); + + // then + assertThat(savedToken.getId()).isNotNull(); + assertThat(savedToken.getDeviceId()).isEqualTo("device123"); + assertThat(savedToken.getFcmToken()).isEqualTo("fcm-token-123"); + assertThat(savedToken.getMember()).isEqualTo(member); + } + + @Test + @DisplayName("회원의 모든 디바이스 토큰이 조회된다.") + void should_FindAllTokens_When_FilteringByMemberId() { + // given + Member member = createAndSaveMember(); + DeviceToken token1 = DeviceToken.builder() + .member(member) + .deviceId("device1") + .fcmToken("token1") + .build(); + DeviceToken token2 = DeviceToken.builder() + .member(member) + .deviceId("device2") + .fcmToken("token2") + .build(); + + deviceTokenRepository.saveAll(List.of(token1, token2)); + + // when + List tokens = deviceTokenRepository.findAllByMemberId(member.getId()); + + // then + assertThat(tokens).hasSize(2); + assertThat(tokens).extracting("deviceId") + .containsExactlyInAnyOrder("device1", "device2"); + } + + @Test + @DisplayName("특정 디바이스의 토큰이 조회된다.") + void should_FindToken_When_FilteringByMemberAndDevice() { + // given + Member member = createAndSaveMember(); + DeviceToken token = DeviceToken.builder() + .member(member) + .deviceId("device123") + .fcmToken("fcm-token-123") + .build(); + + deviceTokenRepository.save(token); + + // when + Optional foundToken = deviceTokenRepository + .findByMemberIdAndDeviceId(member.getId(), "device123"); + + // then + assertThat(foundToken).isPresent(); + assertThat(foundToken.get().getDeviceId()).isEqualTo("device123"); + assertThat(foundToken.get().getFcmToken()).isEqualTo("fcm-token-123"); + } + + @Test + @DisplayName("존재하지 않는 디바이스의 토큰 조회시 빈 결과가 반환된다.") + void should_ReturnEmpty_When_DeviceNotFound() { + // given + Member member = createAndSaveMember(); + + // when + Optional foundToken = deviceTokenRepository + .findByMemberIdAndDeviceId(member.getId(), "non-existent-device"); + + // then + assertThat(foundToken).isEmpty(); + } + + @Test + @DisplayName("디바이스 토큰이 성공적으로 삭제된다.") + void should_DeleteToken_When_ValidMemberAndDevice() { + // given + Member member = createAndSaveMember(); + DeviceToken token = DeviceToken.builder() + .member(member) + .deviceId("device123") + .fcmToken("fcm-token-123") + .build(); + + deviceTokenRepository.save(token); + + // when + deviceTokenRepository.deleteByMemberIdAndDeviceId(member.getId(), "device123"); + + // then + Optional deletedToken = deviceTokenRepository + .findByMemberIdAndDeviceId(member.getId(), "device123"); + assertThat(deletedToken).isEmpty(); + } + + @Test + @DisplayName("여러 디바이스 토큰이 한번에 삭제된다.") + void should_DeleteAllTokens_When_DeletingMultipleTokens() { + // given + Member member = createAndSaveMember(); + List tokens = List.of( + DeviceToken.builder() + .member(member) + .deviceId("device1") + .fcmToken("token1") + .build(), + DeviceToken.builder() + .member(member) + .deviceId("device2") + .fcmToken("token2") + .build() + ); + + deviceTokenRepository.saveAll(tokens); + + // when + deviceTokenRepository.deleteAll(tokens); + + // then + List remainingTokens = deviceTokenRepository.findAllByMemberId(member.getId()); + assertThat(remainingTokens).isEmpty(); + } + + @Test + @DisplayName("사용자별로 디바이스 토큰이 독립적으로 관리된다.") + void should_ManageTokensSeparately_WhenMultipleUsers() { + // given + Member member1 = createAndSaveMember(); + Member member2 = Member.builder() + .number(5678L) + .nickname("testUser2") + .profileImageUrl("https://example.com/image2.jpg") + .build(); + memberRepository.save(member2); + + DeviceToken token1 = DeviceToken.builder() + .member(member1) + .deviceId("device1") + .fcmToken("token1") + .build(); + DeviceToken token2 = DeviceToken.builder() + .member(member2) + .deviceId("device1") + .fcmToken("token2") + .build(); + + deviceTokenRepository.saveAll(List.of(token1, token2)); + + // when + List tokens1 = deviceTokenRepository.findAllByMemberId(member1.getId()); + List tokens2 = deviceTokenRepository.findAllByMemberId(member2.getId()); + + // then + assertThat(tokens1) + .hasSize(1) + .extracting("fcmToken") + .containsExactly("token1"); + + assertThat(tokens2) + .hasSize(1) + .extracting("fcmToken") + .containsExactly("token2"); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/repository/DiaryRepositoryTest.java b/src/test/java/com/potatocake/everymoment/repository/DiaryRepositoryTest.java new file mode 100644 index 0000000..74f276c --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/repository/DiaryRepositoryTest.java @@ -0,0 +1,191 @@ +package com.potatocake.everymoment.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.potatocake.everymoment.entity.Diary; +import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.service.DiarySpecification; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.test.context.TestPropertySource; + +@TestPropertySource(properties = "spring.jpa.hibernate.ddl-auto=create-drop") +@DataJpaTest +class DiaryRepositoryTest { + + @Autowired + private DiaryRepository diaryRepository; + + @Autowired + private MemberRepository memberRepository; + + @Test + @DisplayName("일기가 성공적으로 저장된다.") + void should_SaveDiary_When_ValidEntity() { + // given + Member member = createAndSaveMember(); + Diary diary = createDiary(member, "Test content", "Seoul", "Seoul, South Korea"); + + // when + Diary savedDiary = diaryRepository.save(diary); + + // then + assertThat(savedDiary.getId()).isNotNull(); + assertThat(savedDiary.getContent()).isEqualTo("Test content"); + assertThat(savedDiary.getMember()).isEqualTo(member); + assertThat(savedDiary.getAddress()).isEqualTo("Seoul, South Korea"); + } + + @Test + @DisplayName("회원의 일기 목록이 성공적으로 조회된다.") + void should_FindDiaries_When_FilteringByMember() { + // given + Member member = createAndSaveMember(); + + Diary diary1 = createDiary(member, "Content 1", "Seoul", "Address 1"); + Diary diary2 = createDiary(member, "Content 2", "Busan", "Address 2"); + + diaryRepository.saveAll(List.of(diary1, diary2)); + + // when + Specification spec = (root, query, builder) -> + builder.equal(root.get("member").get("id"), member.getId()); + Page result = diaryRepository.findAll(spec, PageRequest.of(0, 10)); + + // then + assertThat(result.getContent()).hasSize(2); + assertThat(result.getContent()).extracting("content") + .containsExactly("Content 1", "Content 2"); + } + + @Test + @DisplayName("검색 조건으로 일기가 성공적으로 필터링된다.") + void should_FindDiaries_When_FilteringWithSearchCriteria() { + // given + Member member = createAndSaveMember(); + + Diary diary1 = createDiary(member, "Content Seoul", "Seoul", "Seoul Address"); + diary1 = Diary.builder() + .member(member) + .content("Content Seoul") + .locationPoint(diary1.getLocationPoint()) + .locationName("Seoul") + .address("Seoul Address") + .emoji("😊") + .isPublic(true) + .isBookmark(false) + .build(); + + Diary diary2 = createDiary(member, "Content Busan", "Busan", "Busan Address"); + diary2 = Diary.builder() + .member(member) + .content("Content Busan") + .locationPoint(diary2.getLocationPoint()) + .locationName("Busan") + .address("Busan Address") + .emoji("😍") + .isPublic(true) + .isBookmark(false) + .build(); + + diaryRepository.saveAll(List.of(diary1, diary2)); + + // when + Specification spec = DiarySpecification.filterDiaries( + "Seoul", // keyword + List.of("😊"), // emojis + null, // categories + null, // date + null, // from + null, // until + false // isBookmark + ); + + Page result = diaryRepository.findAll(spec, PageRequest.of(0, 10)); + + // then + assertThat(result.getContent()).hasSize(1); + assertThat(result.getContent().get(0).getContent()).isEqualTo("Content Seoul"); + assertThat(result.getContent().get(0).getEmoji()).isEqualTo("😊"); + } + + @Test + @DisplayName("북마크된 일기만 성공적으로 필터링된다.") + void should_FindDiaries_When_FilteringBookmarked() { + // given + Member member = createAndSaveMember(); + + Diary diary1 = createDiary(member, "Content 1", "Seoul", "Seoul Address"); + diary1 = Diary.builder() + .member(member) + .content("Content 1") + .locationPoint(diary1.getLocationPoint()) + .locationName("Seoul") + .address("Seoul Address") + .isBookmark(true) + .isPublic(false) + .build(); + + Diary diary2 = createDiary(member, "Content 2", "Busan", "Busan Address"); + diary2 = Diary.builder() + .member(member) + .content("Content 2") + .locationPoint(diary2.getLocationPoint()) + .locationName("Busan") + .address("Busan Address") + .isBookmark(false) + .isPublic(false) + .build(); + + diaryRepository.saveAll(List.of(diary1, diary2)); + + // when + Specification spec = DiarySpecification.filterDiaries( + null, // keyword + null, // emojis + null, // categories + null, // date + null, // from + null, // until + true // isBookmark + ); + + Page result = diaryRepository.findAll(spec, PageRequest.of(0, 10)); + + // then + assertThat(result.getContent()).hasSize(1); + assertThat(result.getContent().get(0).isBookmark()).isTrue(); + } + + private Member createAndSaveMember() { + Member member = Member.builder() + .number(1234L) + .nickname("testUser") + .profileImageUrl("https://example.com/image.jpg") + .build(); + return memberRepository.save(member); + } + + private Diary createDiary(Member member, String content, String locationName, String address) { + Point point = new GeometryFactory().createPoint(new Coordinate(37.5665, 126.978)); + return Diary.builder() + .member(member) + .content(content) + .locationPoint(point) + .locationName(locationName) + .address(address) + .isBookmark(false) + .isPublic(false) + .build(); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/repository/FileRepositoryTest.java b/src/test/java/com/potatocake/everymoment/repository/FileRepositoryTest.java new file mode 100644 index 0000000..29c5422 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/repository/FileRepositoryTest.java @@ -0,0 +1,206 @@ +package com.potatocake.everymoment.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.potatocake.everymoment.entity.Diary; +import com.potatocake.everymoment.entity.File; +import com.potatocake.everymoment.entity.Member; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.TestPropertySource; + +@TestPropertySource(properties = "spring.jpa.hibernate.ddl-auto=create-drop") +@DataJpaTest +class FileRepositoryTest { + + @Autowired + private FileRepository fileRepository; + + @Autowired + private DiaryRepository diaryRepository; + + @Autowired + private MemberRepository memberRepository; + + @Test + @DisplayName("파일이 성공적으로 저장된다.") + void should_SaveFile_When_ValidEntity() { + // given + Member member = createAndSaveMember(); + Diary diary = createAndSaveDiary(member); + + File file = File.builder() + .diary(diary) + .imageUrl("https://example.com/image.jpg") + .order(1) + .build(); + + // when + File savedFile = fileRepository.save(file); + + // then + assertThat(savedFile.getId()).isNotNull(); + assertThat(savedFile.getImageUrl()).isEqualTo("https://example.com/image.jpg"); + assertThat(savedFile.getOrder()).isEqualTo(1); + assertThat(savedFile.getDiary()).isEqualTo(diary); + } + + @Test + @DisplayName("일기의 파일 목록이 성공적으로 조회된다.") + void should_FindFiles_When_FilteringByDiaryId() { + // given + Member member = createAndSaveMember(); + Diary diary = createAndSaveDiary(member); + + File file1 = File.builder() + .diary(diary) + .imageUrl("https://example.com/image1.jpg") + .order(1) + .build(); + + File file2 = File.builder() + .diary(diary) + .imageUrl("https://example.com/image2.jpg") + .order(2) + .build(); + + fileRepository.saveAll(List.of(file1, file2)); + + // when + List files = fileRepository.findByDiaryId(diary.getId()); + + // then + assertThat(files).hasSize(2); + assertThat(files).extracting("imageUrl") + .containsExactly( + "https://example.com/image1.jpg", + "https://example.com/image2.jpg" + ); + } + + @Test + @DisplayName("다이어리별로 파일이 독립적으로 조회된다.") + void should_FindFilesSeparately_WhenMultipleDiaries() { + // given + Member member = createAndSaveMember(); + Diary diary1 = createAndSaveDiary(member); + Diary diary2 = createAndSaveDiary(member); + + File file1 = File.builder() + .diary(diary1) + .imageUrl("https://example.com/diary1/image.jpg") + .order(1) + .build(); + + File file2 = File.builder() + .diary(diary2) + .imageUrl("https://example.com/diary2/image.jpg") + .order(1) + .build(); + + fileRepository.saveAll(List.of(file1, file2)); + + // when + List filesForDiary1 = fileRepository.findByDiaryId(diary1.getId()); + List filesForDiary2 = fileRepository.findByDiaryId(diary2.getId()); + + // then + assertThat(filesForDiary1) + .hasSize(1) + .extracting("imageUrl") + .containsExactly("https://example.com/diary1/image.jpg"); + + assertThat(filesForDiary2) + .hasSize(1) + .extracting("imageUrl") + .containsExactly("https://example.com/diary2/image.jpg"); + } + + @Test + @DisplayName("특정 순서의 파일이 성공적으로 조회된다.") + void should_FindFile_When_FilteringByDiaryAndOrder() { + // given + Member member = createAndSaveMember(); + Diary diary = createAndSaveDiary(member); + + File file1 = File.builder() + .diary(diary) + .imageUrl("https://example.com/image1.jpg") + .order(1) + .build(); + + File file2 = File.builder() + .diary(diary) + .imageUrl("https://example.com/image2.jpg") + .order(2) + .build(); + + fileRepository.saveAll(List.of(file1, file2)); + + // when + File foundFile = fileRepository.findByDiaryAndOrder(diary, 2); + + // then + assertThat(foundFile).isNotNull(); + assertThat(foundFile.getImageUrl()).isEqualTo("https://example.com/image2.jpg"); + assertThat(foundFile.getOrder()).isEqualTo(2); + } + + @Test + @DisplayName("일기의 모든 파일이 성공적으로 삭제된다.") + void should_DeleteAllFiles_When_DeletingByDiary() { + // given + Member member = createAndSaveMember(); + Diary diary = createAndSaveDiary(member); + + File file1 = File.builder() + .diary(diary) + .imageUrl("https://example.com/image1.jpg") + .order(1) + .build(); + + File file2 = File.builder() + .diary(diary) + .imageUrl("https://example.com/image2.jpg") + .order(2) + .build(); + + fileRepository.saveAll(List.of(file1, file2)); + + // when + fileRepository.deleteByDiary(diary); + + // then + List remainingFiles = fileRepository.findByDiaryId(diary.getId()); + assertThat(remainingFiles).isEmpty(); + } + + private Member createAndSaveMember() { + Member member = Member.builder() + .number(1234L) + .nickname("testUser") + .profileImageUrl("https://example.com/profile.jpg") + .build(); + return memberRepository.save(member); + } + + private Diary createAndSaveDiary(Member member) { + Point point = new GeometryFactory().createPoint(new Coordinate(37.5665, 126.978)); + + Diary diary = Diary.builder() + .member(member) + .content("Test diary") + .locationName("Test location") + .address("Test address") + .locationPoint(point) + .build(); + return diaryRepository.save(diary); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/repository/FriendRepositoryTest.java b/src/test/java/com/potatocake/everymoment/repository/FriendRepositoryTest.java new file mode 100644 index 0000000..4299dbd --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/repository/FriendRepositoryTest.java @@ -0,0 +1,260 @@ +package com.potatocake.everymoment.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.potatocake.everymoment.entity.Friend; +import com.potatocake.everymoment.entity.Member; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.test.context.TestPropertySource; + +@TestPropertySource(properties = "spring.jpa.hibernate.ddl-auto=create-drop") +@DataJpaTest +class FriendRepositoryTest { + + @Autowired + private FriendRepository friendRepository; + + @Autowired + private MemberRepository memberRepository; + + @Test + @DisplayName("친구 관계가 성공적으로 저장된다.") + void should_SaveFriend_When_ValidEntity() { + // given + Member member = createAndSaveMember("user", 123L); + Member friend = createAndSaveMember("friend", 124L); + + Friend friendship = Friend.builder() + .member(member) + .friend(friend) + .build(); + + // when + Friend savedFriendship = friendRepository.save(friendship); + + // then + assertThat(savedFriendship.getId()).isNotNull(); + assertThat(savedFriendship.getMember()).isEqualTo(member); + assertThat(savedFriendship.getFriend()).isEqualTo(friend); + } + + @Test + @DisplayName("회원의 모든 친구가 조회된다.") + void should_FindFriends_When_ValidMember() { + // given + Member member = createAndSaveMember("user", 123L); + Member friend1 = createAndSaveMember("friend1", 124L); + Member friend2 = createAndSaveMember("friend2", 125L); + + friendRepository.save(Friend.builder() + .member(member) + .friend(friend1) + .build()); + friendRepository.save(Friend.builder() + .member(member) + .friend(friend2) + .build()); + + // when + List friends = friendRepository.findFriendsByMember(member); + + // then + assertThat(friends).hasSize(2); + assertThat(friends).extracting(friend -> friend.getFriend().getNickname()) + .containsExactlyInAnyOrder("friend1", "friend2"); + } + + @Test + @DisplayName("특정 친구 관계가 존재하는지 확인된다.") + void should_CheckExistence_When_CheckingFriendship() { + // given + Member member = createAndSaveMember("user", 123L); + Member friend = createAndSaveMember("friend", 124L); + + friendRepository.save(Friend.builder() + .member(member) + .friend(friend) + .build()); + + // when & then + assertThat(friendRepository.existsByMemberIdAndFriendId(member.getId(), friend.getId())) + .isTrue(); + assertThat(friendRepository.existsByMemberIdAndFriendId(member.getId(), 999L)) + .isFalse(); + } + + @Test + @DisplayName("특정 친구 관계가 성공적으로 조회된다.") + void should_FindFriendship_When_ValidMemberAndFriend() { + // given + Member member = createAndSaveMember("user", 123L); + Member friend = createAndSaveMember("friend", 124L); + + Friend friendship = Friend.builder() + .member(member) + .friend(friend) + .build(); + friendRepository.save(friendship); + + // when + Optional foundFriendship = friendRepository.findByMemberAndFriend(member, friend); + + // then + assertThat(foundFriendship).isPresent(); + assertThat(foundFriendship.get().getMember()).isEqualTo(member); + assertThat(foundFriendship.get().getFriend()).isEqualTo(friend); + } + + @Test + @DisplayName("존재하지 않는 친구 관계 조회시 빈 결과가 반환된다.") + void should_ReturnEmpty_When_FriendshipNotFound() { + // given + Member member = createAndSaveMember("user", 123L); + Member notFriend = createAndSaveMember("notFriend", 124L); + + // when + Optional foundFriendship = friendRepository.findByMemberAndFriend(member, notFriend); + + // then + assertThat(foundFriendship).isEmpty(); + } + + @Test + @DisplayName("친구 관계가 성공적으로 삭제된다.") + void should_DeleteFriendship_When_ValidEntity() { + // given + Member member = createAndSaveMember("user", 123L); + Member friend = createAndSaveMember("friend", 124L); + + Friend friendship = Friend.builder() + .member(member) + .friend(friend) + .build(); + Friend savedFriendship = friendRepository.save(friendship); + + // when + friendRepository.delete(savedFriendship); + + // then + Optional deletedFriendship = friendRepository.findByMemberAndFriend(member, friend); + assertThat(deletedFriendship).isEmpty(); + } + + @Test + @DisplayName("친구 관계가 양방향으로 생성된다.") + void should_CreateBidirectionalFriendship_When_AddingFriend() { + // given + Member member1 = createAndSaveMember("user1", 123L); + Member member2 = createAndSaveMember("user2", 124L); + + Friend friendship1 = Friend.builder() + .member(member1) + .friend(member2) + .build(); + Friend friendship2 = Friend.builder() + .member(member2) + .friend(member1) + .build(); + + // when + friendRepository.saveAll(List.of(friendship1, friendship2)); + + // then + assertThat(friendRepository.findByMemberAndFriend(member1, member2)).isPresent(); + assertThat(friendRepository.findByMemberAndFriend(member2, member1)).isPresent(); + } + + @Test + @DisplayName("닉네임으로 친구를 검색할 수 있다.") + void should_FindFriends_When_SearchingByNickname() { + // given + Member member = createAndSaveMember("user", 123L); + Member friend1 = createAndSaveMember("john", 124L); + Member friend2 = createAndSaveMember("johnny", 125L); + Member friend3 = createAndSaveMember("peter", 126L); + + friendRepository.saveAll(List.of( + Friend.builder().member(member).friend(friend1).build(), + Friend.builder().member(member).friend(friend2).build(), + Friend.builder().member(member).friend(friend3).build() + )); + + // when + Specification spec = (root, query, builder) -> { + return builder.and( + builder.equal(root.get("member"), member), + builder.like(root.get("friend").get("nickname"), "%john%") + ); + }; + + Page searchResult = friendRepository.findAll( + spec, + PageRequest.of(0, 10) + ); + + // then + assertThat(searchResult.getContent()) + .hasSize(2) + .extracting(friend -> friend.getFriend().getNickname()) + .containsExactlyInAnyOrder("john", "johnny"); + } + + @Test + @DisplayName("페이징이 성공적으로 동작한다.") + void should_ReturnPagedResult_When_UsingPagination() { + // given + Member member = createAndSaveMember("user", 123L); + List friends = List.of( + createAndSaveMember("friend1", 124L), + createAndSaveMember("friend2", 125L), + createAndSaveMember("friend3", 126L), + createAndSaveMember("friend4", 127L), + createAndSaveMember("friend5", 128L) + ); + + List friendships = friends.stream() + .map(friend -> Friend.builder() + .member(member) + .friend(friend) + .build()) + .toList(); + + friendRepository.saveAll(friendships); + + // when + Page firstPage = friendRepository.findAll( + (root, query, builder) -> builder.equal(root.get("member"), member), + PageRequest.of(0, 2) + ); + + Page secondPage = friendRepository.findAll( + (root, query, builder) -> builder.equal(root.get("member"), member), + PageRequest.of(1, 2) + ); + + // then + assertThat(firstPage.getContent()).hasSize(2); + assertThat(firstPage.hasNext()).isTrue(); + + assertThat(secondPage.getContent()).hasSize(2); + assertThat(secondPage.hasNext()).isTrue(); + } + + private Member createAndSaveMember(String nickname, Long number) { + Member member = Member.builder() + .number(number) + .nickname(nickname) + .profileImageUrl("https://example.com/profile.jpg") + .build(); + return memberRepository.save(member); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/repository/FriendRequestRepositoryTest.java b/src/test/java/com/potatocake/everymoment/repository/FriendRequestRepositoryTest.java new file mode 100644 index 0000000..7e09352 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/repository/FriendRequestRepositoryTest.java @@ -0,0 +1,155 @@ +package com.potatocake.everymoment.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.potatocake.everymoment.entity.FriendRequest; +import com.potatocake.everymoment.entity.Member; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Window; +import org.springframework.test.context.TestPropertySource; + +@TestPropertySource(properties = "spring.jpa.hibernate.ddl-auto=create-drop") +@DataJpaTest +class FriendRequestRepositoryTest { + + @Autowired + private FriendRequestRepository friendRequestRepository; + + @Autowired + private MemberRepository memberRepository; + + @Test + @DisplayName("친구 요청이 성공적으로 저장된다.") + void should_SaveFriendRequest_When_ValidEntity() { + // given + Member sender = createAndSaveMember("sender", 123L); + Member receiver = createAndSaveMember("receiver", 124L); + + FriendRequest request = FriendRequest.builder() + .sender(sender) + .receiver(receiver) + .build(); + + // when + FriendRequest savedRequest = friendRequestRepository.save(request); + + // then + assertThat(savedRequest.getId()).isNotNull(); + assertThat(savedRequest.getSender()).isEqualTo(sender); + assertThat(savedRequest.getReceiver()).isEqualTo(receiver); + } + + @Test + @DisplayName("사용자의 받은 친구 요청 목록이 성공적으로 조회된다.") + void should_FindReceivedRequests_When_ValidReceiver() { + // given + Member receiver = createAndSaveMember("receiver", 123L); + Member sender1 = createAndSaveMember("sender1", 124L); + Member sender2 = createAndSaveMember("sender2", 125L); + + friendRequestRepository.saveAll(List.of( + FriendRequest.builder() + .sender(sender1) + .receiver(receiver) + .build(), + FriendRequest.builder() + .sender(sender2) + .receiver(receiver) + .build() + )); + + // when + Window requests = friendRequestRepository.findByReceiverId( + receiver.getId(), + ScrollPosition.offset(), + PageRequest.of(0, 10) + ); + + // then + assertThat(requests.getContent()).hasSize(2); + assertThat(requests.getContent()) + .extracting(request -> request.getSender().getNickname()) + .containsExactlyInAnyOrder("sender1", "sender2"); + } + + @Test + @DisplayName("이미 존재하는 친구 요청인지 확인된다.") + void should_CheckExistence_When_CheckingRequest() { + // given + Member sender = createAndSaveMember("sender", 123L); + Member receiver = createAndSaveMember("receiver", 124L); + + FriendRequest request = FriendRequest.builder() + .sender(sender) + .receiver(receiver) + .build(); + friendRequestRepository.save(request); + + // when & then + assertThat(friendRequestRepository.existsBySenderIdAndReceiverId(sender.getId(), receiver.getId())) + .isTrue(); + assertThat(friendRequestRepository.existsBySenderIdAndReceiverId(receiver.getId(), sender.getId())) + .isFalse(); + } + + @Test + @DisplayName("특정 친구 요청이 성공적으로 조회된다.") + void should_FindRequest_When_ValidSenderAndReceiver() { + // given + Member sender = createAndSaveMember("sender", 123L); + Member receiver = createAndSaveMember("receiver", 124L); + + FriendRequest request = FriendRequest.builder() + .sender(sender) + .receiver(receiver) + .build(); + friendRequestRepository.save(request); + + // when + Optional foundRequest = friendRequestRepository + .findBySenderIdAndReceiverId(sender.getId(), receiver.getId()); + + // then + assertThat(foundRequest).isPresent(); + assertThat(foundRequest.get().getSender()).isEqualTo(sender); + assertThat(foundRequest.get().getReceiver()).isEqualTo(receiver); + } + + @Test + @DisplayName("친구 요청이 성공적으로 삭제된다.") + void should_DeleteRequest_When_ValidEntity() { + // given + Member sender = createAndSaveMember("sender", 123L); + Member receiver = createAndSaveMember("receiver", 124L); + + FriendRequest request = FriendRequest.builder() + .sender(sender) + .receiver(receiver) + .build(); + FriendRequest savedRequest = friendRequestRepository.save(request); + + // when + friendRequestRepository.delete(savedRequest); + + // then + Optional deletedRequest = friendRequestRepository.findById(savedRequest.getId()); + assertThat(deletedRequest).isEmpty(); + } + + private Member createAndSaveMember(String nickname, Long number) { + Member member = Member.builder() + .number(number) + .nickname(nickname) + .profileImageUrl("https://example.com/profile.jpg") + .build(); + return memberRepository.save(member); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/repository/LikeRepositoryTest.java b/src/test/java/com/potatocake/everymoment/repository/LikeRepositoryTest.java new file mode 100644 index 0000000..87027c4 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/repository/LikeRepositoryTest.java @@ -0,0 +1,170 @@ +package com.potatocake.everymoment.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.potatocake.everymoment.entity.Diary; +import com.potatocake.everymoment.entity.Like; +import com.potatocake.everymoment.entity.Member; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.TestPropertySource; + +@TestPropertySource(properties = "spring.jpa.hibernate.ddl-auto=create-drop") +@DataJpaTest +class LikeRepositoryTest { + + @Autowired + private LikeRepository likeRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private DiaryRepository diaryRepository; + + @Test + @DisplayName("좋아요가 성공적으로 저장된다.") + void should_SaveLike_When_ValidEntity() { + // given + Member member = createAndSaveMember(); + Diary diary = createAndSaveDiary(member); + + Like like = Like.builder() + .member(member) + .diary(diary) + .build(); + + // when + Like savedLike = likeRepository.save(like); + + // then + assertThat(savedLike.getId()).isNotNull(); + assertThat(savedLike.getMember()).isEqualTo(member); + assertThat(savedLike.getDiary()).isEqualTo(diary); + } + + @Test + @DisplayName("일기의 좋아요 수가 성공적으로 조회된다.") + void should_CountLikes_When_CountingByDiary() { + // given + Member member1 = createAndSaveMember(); + Member member2 = Member.builder() + .number(5678L) + .nickname("testUser2") + .profileImageUrl("https://example.com/profile2.jpg") + .build(); + memberRepository.save(member2); + + Diary diary = createAndSaveDiary(member1); + + Like like1 = Like.builder() + .member(member1) + .diary(diary) + .build(); + Like like2 = Like.builder() + .member(member2) + .diary(diary) + .build(); + + likeRepository.saveAll(List.of(like1, like2)); + + // when + Long likeCount = likeRepository.countByDiary(diary); + + // then + assertThat(likeCount).isEqualTo(2L); + } + + @Test + @DisplayName("특정 사용자의 좋아요가 성공적으로 조회된다.") + void should_FindLike_When_FilteringByMemberAndDiary() { + // given + Member member = createAndSaveMember(); + Diary diary = createAndSaveDiary(member); + + Like like = Like.builder() + .member(member) + .diary(diary) + .build(); + likeRepository.save(like); + + // when + Optional foundLike = likeRepository.findByMemberIdAndDiaryId(member.getId(), diary.getId()); + + // then + assertThat(foundLike).isPresent(); + assertThat(foundLike.get().getMember()).isEqualTo(member); + assertThat(foundLike.get().getDiary()).isEqualTo(diary); + } + + @Test + @DisplayName("좋아요 여부가 성공적으로 확인된다.") + void should_CheckExistence_When_CheckingLike() { + // given + Member member = createAndSaveMember(); + Diary diary = createAndSaveDiary(member); + + Like like = Like.builder() + .member(member) + .diary(diary) + .build(); + likeRepository.save(like); + + // when & then + assertThat(likeRepository.existsByMemberIdAndDiaryId(member.getId(), diary.getId())) + .isTrue(); + assertThat(likeRepository.existsByMemberIdAndDiaryId(member.getId(), 999L)) + .isFalse(); + } + + @Test + @DisplayName("좋아요가 성공적으로 삭제된다.") + void should_DeleteLike_When_ValidEntity() { + // given + Member member = createAndSaveMember(); + Diary diary = createAndSaveDiary(member); + + Like like = Like.builder() + .member(member) + .diary(diary) + .build(); + Like savedLike = likeRepository.save(like); + + // when + likeRepository.delete(savedLike); + + // then + Optional deletedLike = likeRepository.findById(savedLike.getId()); + assertThat(deletedLike).isEmpty(); + } + + private Member createAndSaveMember() { + Member member = Member.builder() + .number(1234L) + .nickname("testUser") + .profileImageUrl("https://example.com/profile.jpg") + .build(); + return memberRepository.save(member); + } + + private Diary createAndSaveDiary(Member member) { + Point point = new GeometryFactory().createPoint(new Coordinate(37.5665, 126.978)); + + Diary diary = Diary.builder() + .member(member) + .content("Test content") + .locationName("Test location") + .address("Test address") + .locationPoint(point) + .build(); + return diaryRepository.save(diary); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/repository/MemberRepositoryTest.java b/src/test/java/com/potatocake/everymoment/repository/MemberRepositoryTest.java new file mode 100644 index 0000000..f28ec1a --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/repository/MemberRepositoryTest.java @@ -0,0 +1,177 @@ +package com.potatocake.everymoment.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.potatocake.everymoment.entity.Member; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Window; +import org.springframework.test.context.TestPropertySource; + +@TestPropertySource(properties = "spring.jpa.hibernate.ddl-auto=create-drop") +@DataJpaTest +class MemberRepositoryTest { + + @Autowired + private MemberRepository memberRepository; + + @Test + @DisplayName("회원이 성공적으로 저장된다.") + void should_SaveMember_When_ValidEntity() { + // given + Member member = Member.builder() + .number(1234L) + .nickname("testUser") + .profileImageUrl("https://example.com/profile.jpg") + .build(); + + // when + Member savedMember = memberRepository.save(member); + + // then + assertThat(savedMember.getId()).isNotNull(); + assertThat(savedMember.getNumber()).isEqualTo(1234L); + assertThat(savedMember.getNickname()).isEqualTo("testUser"); + assertThat(savedMember.getProfileImageUrl()).isEqualTo("https://example.com/profile.jpg"); + } + + @Test + @DisplayName("회원 번호로 회원이 성공적으로 조회된다.") + void should_FindMember_When_SearchingByNumber() { + // given + Member member = Member.builder() + .number(1234L) + .nickname("testUser") + .profileImageUrl("https://example.com/profile.jpg") + .build(); + memberRepository.save(member); + + // when + Optional foundMember = memberRepository.findByNumber(1234L); + + // then + assertThat(foundMember).isPresent(); + assertThat(foundMember.get().getNickname()).isEqualTo("testUser"); + } + + @Test + @DisplayName("닉네임으로 회원이 성공적으로 검색된다.") + void should_FindMembers_When_SearchingByNickname() { + // given + Member member1 = Member.builder() + .number(1234L) + .nickname("john") + .profileImageUrl("https://example.com/profile1.jpg") + .build(); + Member member2 = Member.builder() + .number(5678L) + .nickname("johnny") + .profileImageUrl("https://example.com/profile2.jpg") + .build(); + Member member3 = Member.builder() + .number(9012L) + .nickname("peter") + .profileImageUrl("https://example.com/profile3.jpg") + .build(); + + memberRepository.saveAll(List.of(member1, member2, member3)); + + // when + Window result = memberRepository.findByNicknameContaining( + "john", + ScrollPosition.offset(), + PageRequest.of(0, 10) + ); + + // then + assertThat(result.getContent()).hasSize(2) + .extracting("nickname") + .containsExactlyInAnyOrder("john", "johnny"); + } + + @Test + @DisplayName("회원 번호 존재 여부가 성공적으로 확인된다.") + void should_CheckExistence_When_CheckingNumber() { + // given + Member member = Member.builder() + .number(1234L) + .nickname("testUser") + .profileImageUrl("https://example.com/profile.jpg") + .build(); + memberRepository.save(member); + + // when & then + assertThat(memberRepository.existsByNumber(1234L)).isTrue(); + assertThat(memberRepository.existsByNumber(5678L)).isFalse(); + } + + @Test + @DisplayName("다음 익명 회원 번호가 성공적으로 생성된다.") + void should_GenerateNextNumber_When_CreatingAnonymous() { + // given + Member member1 = Member.builder() + .number(-1L) + .nickname("anonymous1") + .profileImageUrl("https://example.com/profile1.jpg") + .build(); + Member member2 = Member.builder() + .number(-2L) + .nickname("anonymous2") + .profileImageUrl("https://example.com/profile2.jpg") + .build(); + + memberRepository.saveAll(List.of(member1, member2)); + + // when + Long nextNumber = memberRepository.findNextAnonymousNumber(); + + // then + assertThat(nextNumber).isEqualTo(-3L); + } + + @Test + @DisplayName("회원이 성공적으로 삭제된다.") + void should_DeleteMember_When_ValidEntity() { + // given + Member member = Member.builder() + .number(1234L) + .nickname("testUser") + .profileImageUrl("https://example.com/profile.jpg") + .build(); + Member savedMember = memberRepository.save(member); + + // when + memberRepository.delete(savedMember); + + // then + Optional deletedMember = memberRepository.findById(savedMember.getId()); + assertThat(deletedMember).isEmpty(); + } + + @Test + @DisplayName("회원 정보가 성공적으로 업데이트된다.") + void should_UpdateMember_When_ValidInput() { + // given + Member member = Member.builder() + .number(1234L) + .nickname("oldNickname") + .profileImageUrl("https://example.com/old.jpg") + .build(); + memberRepository.save(member); + + // when + member.update("newNickname", "https://example.com/new.jpg"); + + // then + Member updatedMember = memberRepository.findByNumber(1234L).orElseThrow(); + assertThat(updatedMember.getNickname()).isEqualTo("newNickname"); + assertThat(updatedMember.getProfileImageUrl()).isEqualTo("https://example.com/new.jpg"); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/repository/NotificationRepositoryTest.java b/src/test/java/com/potatocake/everymoment/repository/NotificationRepositoryTest.java new file mode 100644 index 0000000..b5924c8 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/repository/NotificationRepositoryTest.java @@ -0,0 +1,120 @@ +package com.potatocake.everymoment.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.entity.Notification; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.TestPropertySource; + +@TestPropertySource(properties = "spring.jpa.hibernate.ddl-auto=create-drop") +@DataJpaTest +class NotificationRepositoryTest { + + @Autowired + private NotificationRepository notificationRepository; + + @Autowired + private MemberRepository memberRepository; + + @Test + @DisplayName("알림이 성공적으로 저장된다.") + void should_SaveNotification_When_ValidEntity() { + // given + Member member = Member.builder() + .number(1234L) + .nickname("testUser") + .profileImageUrl("https://example.com/profile.jpg") + .build(); + Member savedMember = memberRepository.save(member); + + Notification notification = Notification.builder() + .member(savedMember) + .content("Test notification") + .type("TEST") + .targetId(1L) + .isRead(false) + .build(); + + // when + Notification savedNotification = notificationRepository.save(notification); + + // then + assertThat(savedNotification.getId()).isNotNull(); + assertThat(savedNotification.getContent()).isEqualTo("Test notification"); + assertThat(savedNotification.getMember()).isEqualTo(savedMember); + } + + @Test + @DisplayName("회원의 알림 목록이 성공적으로 조회된다.") + void should_FindNotifications_When_FilteringByMemberId() { + // given + Member member = Member.builder() + .number(1234L) + .nickname("testUser") + .profileImageUrl("https://example.com/profile.jpg") + .build(); + Member savedMember = memberRepository.save(member); + + List notifications = List.of( + Notification.builder() + .member(savedMember) + .content("Notification 1") + .type("TEST1") + .targetId(1L) + .build(), + Notification.builder() + .member(savedMember) + .content("Notification 2") + .type("TEST2") + .targetId(2L) + .build() + ); + + notificationRepository.saveAll(notifications); + + // when + List foundNotifications = notificationRepository + .findAllByMemberId(savedMember.getId()); + + // then + assertThat(foundNotifications).hasSize(2); + assertThat(foundNotifications) + .extracting("content") + .containsExactlyInAnyOrder("Notification 1", "Notification 2"); + } + + @Test + @DisplayName("알림이 성공적으로 삭제된다.") + void should_DeleteNotification_When_ValidEntity() { + // given + Member member = Member.builder() + .number(1234L) + .nickname("testUser") + .profileImageUrl("https://example.com/profile.jpg") + .build(); + Member savedMember = memberRepository.save(member); + + Notification notification = Notification.builder() + .member(savedMember) + .content("Test notification") + .type("TEST") + .targetId(1L) + .build(); + + Notification savedNotification = notificationRepository.save(notification); + + // when + notificationRepository.delete(savedNotification); + + // then + List remainingNotifications = notificationRepository + .findAllByMemberId(savedMember.getId()); + assertThat(remainingNotifications).isEmpty(); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/service/CategoryServiceTest.java b/src/test/java/com/potatocake/everymoment/service/CategoryServiceTest.java new file mode 100644 index 0000000..c2b931d --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/service/CategoryServiceTest.java @@ -0,0 +1,168 @@ +package com.potatocake.everymoment.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; + +import com.potatocake.everymoment.dto.request.CategoryCreateRequest; +import com.potatocake.everymoment.dto.response.CategoryResponse; +import com.potatocake.everymoment.entity.Category; +import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.exception.ErrorCode; +import com.potatocake.everymoment.exception.GlobalException; +import com.potatocake.everymoment.repository.CategoryRepository; +import com.potatocake.everymoment.repository.MemberRepository; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class CategoryServiceTest { + + @InjectMocks + private CategoryService categoryService; + + @Mock + private CategoryRepository categoryRepository; + + @Mock + private MemberRepository memberRepository; + + @Test + @DisplayName("카테고리가 성공적으로 추가된다.") + void should_AddCategory_When_ValidInput() { + // given + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + + CategoryCreateRequest request = new CategoryCreateRequest("New Category"); + + Category category = Category.builder() + .id(1L) + .member(member) + .categoryName("New Category") + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(categoryRepository.save(any(Category.class))).willReturn(category); + + // when + categoryService.addCategory(memberId, request); + + // then + then(memberRepository).should().findById(memberId); + then(categoryRepository).should().save(any(Category.class)); + } + + @Test + @DisplayName("카테고리 목록이 성공적으로 조회된다.") + void should_GetCategories_When_ValidMemberId() { + // given + Long memberId = 1L; + List categories = List.of( + Category.builder().id(1L).categoryName("Category 1").build(), + Category.builder().id(2L).categoryName("Category 2").build() + ); + + given(categoryRepository.findByMemberId(memberId)).willReturn(categories); + + // when + List responses = categoryService.getCategories(memberId); + + // then + assertThat(responses).hasSize(2); + assertThat(responses).extracting("categoryName") + .containsExactly("Category 1", "Category 2"); + then(categoryRepository).should().findByMemberId(memberId); + } + + @Test + @DisplayName("카테고리가 성공적으로 수정된다.") + void should_UpdateCategory_When_ValidInput() { + // given + Long categoryId = 1L; + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + + Category category = Category.builder() + .id(categoryId) + .member(member) + .categoryName("Original Name") + .build(); + + CategoryCreateRequest request = new CategoryCreateRequest("Updated Name"); + + given(categoryRepository.findById(categoryId)).willReturn(Optional.of(category)); + + // when + categoryService.updateCategory(categoryId, memberId, request); + + // then + assertThat(category.getCategoryName()).isEqualTo("Updated Name"); + then(categoryRepository).should().findById(categoryId); + } + + @Test + @DisplayName("다른 사용자의 카테고리를 수정하려고 하면 예외가 발생한다.") + void should_ThrowException_When_UpdateOtherUserCategory() { + // given + Long categoryId = 1L; + Long memberId = 1L; + Member owner = Member.builder() + .id(2L) + .build(); + + Category category = Category.builder() + .id(categoryId) + .member(owner) + .categoryName("Original Name") + .build(); + + CategoryCreateRequest request = new CategoryCreateRequest("Updated Name"); + + given(categoryRepository.findById(categoryId)).willReturn(Optional.of(category)); + + // when & then + assertThatThrownBy(() -> categoryService.updateCategory(categoryId, memberId, request)) + .isInstanceOf(GlobalException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.CATEGORY_NOT_OWNER); + } + + @Test + @DisplayName("카테고리가 성공적으로 삭제된다.") + void should_DeleteCategory_When_ValidRequest() { + // given + Long categoryId = 1L; + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + + Category category = Category.builder() + .id(categoryId) + .member(member) + .categoryName("Category") + .build(); + + given(categoryRepository.findById(categoryId)).willReturn(Optional.of(category)); + + // when + categoryService.deleteCategory(categoryId, memberId); + + // then + then(categoryRepository).should().findById(categoryId); + then(categoryRepository).should().delete(category); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/service/CommentServiceTest.java b/src/test/java/com/potatocake/everymoment/service/CommentServiceTest.java new file mode 100644 index 0000000..da7f913 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/service/CommentServiceTest.java @@ -0,0 +1,219 @@ +package com.potatocake.everymoment.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; + +import com.potatocake.everymoment.constant.NotificationType; +import com.potatocake.everymoment.dto.request.CommentRequest; +import com.potatocake.everymoment.dto.response.CommentsResponse; +import com.potatocake.everymoment.entity.Comment; +import com.potatocake.everymoment.entity.Diary; +import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.exception.ErrorCode; +import com.potatocake.everymoment.exception.GlobalException; +import com.potatocake.everymoment.repository.CommentRepository; +import com.potatocake.everymoment.repository.DiaryRepository; +import com.potatocake.everymoment.repository.MemberRepository; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; + +@ExtendWith(MockitoExtension.class) +class CommentServiceTest { + + @InjectMocks + private CommentService commentService; + + @Mock + private CommentRepository commentRepository; + + @Mock + private DiaryRepository diaryRepository; + + @Mock + private MemberRepository memberRepository; + + @Mock + private NotificationService notificationService; + + @Test + @DisplayName("댓글 목록이 성공적으로 조회된다.") + void should_GetComments_When_ValidDiaryId() { + // given + Long diaryId = 1L; + Member member = Member.builder() + .id(1L) + .nickname("testUser") + .build(); + Comment comment = Comment.builder() + .id(1L) + .member(member) + .content("Test comment") + .build(); + + Page commentPage = new PageImpl<>(List.of(comment)); + + given(commentRepository.findAllByDiaryId(eq(diaryId), any(PageRequest.class))) + .willReturn(commentPage); + + // when + CommentsResponse response = commentService.getComments(diaryId, 0, 10); + + // then + assertThat(response.getComments()).hasSize(1); + assertThat(response.getComments().get(0).getContent()).isEqualTo("Test comment"); + then(commentRepository).should().findAllByDiaryId(eq(diaryId), any(PageRequest.class)); + } + + @Test + @DisplayName("댓글이 성공적으로 생성된다.") + void should_CreateComment_When_ValidInput() { + // given + Long memberId = 1L; + Long diaryId = 1L; + Member member = Member.builder() + .id(memberId) + .nickname("testUser") + .build(); + + Member diaryOwner = Member.builder() + .id(2L) + .build(); + + Diary diary = Diary.builder() + .id(diaryId) + .member(diaryOwner) + .isPublic(true) + .build(); + + CommentRequest request = new CommentRequest(); + request.setContent("New comment"); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(diaryRepository.findById(diaryId)).willReturn(Optional.of(diary)); + given(commentRepository.save(any(Comment.class))).willReturn( + Comment.builder() + .id(1L) + .member(member) + .diary(diary) + .content(request.getContent()) + .build() + ); + + // when + commentService.createComment(memberId, diaryId, request); + + // then + then(commentRepository).should().save(any(Comment.class)); + then(notificationService).should().createAndSendNotification( + eq(diary.getMember().getId()), + eq(NotificationType.COMMENT), + eq(diaryId), + eq(member.getNickname()) + ); + } + + @Test + @DisplayName("자신의 일기에 댓글을 작성할 때는 알림이 발송되지 않는다.") + void should_NotSendNotification_When_CommentingOwnDiary() { + // given + Long memberId = 1L; + Long diaryId = 1L; + Member member = Member.builder() + .id(memberId) + .nickname("testUser") + .build(); + + Diary diary = Diary.builder() + .id(diaryId) + .member(member) // 일기 작성자와 댓글 작성자가 동일 + .isPublic(true) + .build(); + + CommentRequest request = new CommentRequest(); + request.setContent("New comment"); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(diaryRepository.findById(diaryId)).willReturn(Optional.of(diary)); + given(commentRepository.save(any(Comment.class))).willReturn( + Comment.builder() + .id(1L) + .member(member) + .diary(diary) + .content(request.getContent()) + .build() + ); + + // when + commentService.createComment(memberId, diaryId, request); + + // then + then(commentRepository).should().save(any(Comment.class)); + then(notificationService).shouldHaveNoInteractions(); + } + + @Test + @DisplayName("비공개 일기에 댓글을 작성하려고 하면 예외가 발생한다.") + void should_ThrowException_When_DiaryNotPublic() { + // given + Long memberId = 1L; + Long diaryId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + Diary diary = Diary.builder() + .id(diaryId) + .isPublic(false) + .build(); + CommentRequest request = new CommentRequest(); + request.setContent("New comment"); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(diaryRepository.findById(diaryId)).willReturn(Optional.of(diary)); + + // when & then + assertThatThrownBy(() -> commentService.createComment(memberId, diaryId, request)) + .isInstanceOf(GlobalException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.DIARY_NOT_PUBLIC); + } + + @Test + @DisplayName("댓글이 성공적으로 수정된다.") + void should_UpdateComment_When_ValidInput() { + // given + Long memberId = 1L; + Long commentId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + Comment comment = Comment.builder() + .id(commentId) + .member(member) + .content("Original content") + .build(); + CommentRequest request = new CommentRequest(); + request.setContent("Updated content"); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(commentRepository.findById(commentId)).willReturn(Optional.of(comment)); + + // when + commentService.updateComment(memberId, commentId, request); + + // then + assertThat(comment.getContent()).isEqualTo("Updated content"); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/service/DiaryServiceTest.java b/src/test/java/com/potatocake/everymoment/service/DiaryServiceTest.java new file mode 100644 index 0000000..78379fe --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/service/DiaryServiceTest.java @@ -0,0 +1,524 @@ +package com.potatocake.everymoment.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +import com.potatocake.everymoment.constant.NotificationType; +import com.potatocake.everymoment.dto.LocationPoint; +import com.potatocake.everymoment.dto.request.CategoryRequest; +import com.potatocake.everymoment.dto.request.DiaryAutoCreateRequest; +import com.potatocake.everymoment.dto.request.DiaryFilterRequest; +import com.potatocake.everymoment.dto.request.DiaryManualCreateRequest; +import com.potatocake.everymoment.dto.request.DiaryPatchRequest; +import com.potatocake.everymoment.dto.response.MyDiariesResponse; +import com.potatocake.everymoment.dto.response.MyDiaryResponse; +import com.potatocake.everymoment.entity.Category; +import com.potatocake.everymoment.entity.Diary; +import com.potatocake.everymoment.entity.DiaryCategory; +import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.exception.ErrorCode; +import com.potatocake.everymoment.exception.GlobalException; +import com.potatocake.everymoment.repository.CategoryRepository; +import com.potatocake.everymoment.repository.DiaryCategoryRepository; +import com.potatocake.everymoment.repository.DiaryRepository; +import com.potatocake.everymoment.repository.FileRepository; +import com.potatocake.everymoment.repository.LikeRepository; +import com.potatocake.everymoment.repository.MemberRepository; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; + +@ExtendWith(MockitoExtension.class) +class DiaryServiceTest { + + @InjectMocks + private DiaryService diaryService; + + @Mock + private DiaryRepository diaryRepository; + + @Mock + private MemberRepository memberRepository; + + @Mock + private CategoryRepository categoryRepository; + + @Mock + private DiaryCategoryRepository diaryCategoryRepository; + + @Mock + private FileRepository fileRepository; + + @Mock + private LikeRepository likeRepository; + + @Mock + private GeometryFactory geometryFactory; + + @Mock + private NotificationService notificationService; + + @Test + @DisplayName("자동 일기가 성공적으로 저장된다.") + void should_SaveAutoDiary_When_ValidInput() { + // given + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + DiaryAutoCreateRequest request = DiaryAutoCreateRequest.builder() + .locationPoint(new LocationPoint(37.5665, 126.978)) + .locationName("Seoul") + .address("Seoul, South Korea") + .build(); + + Point point = mock(Point.class); + Diary savedDiary = Diary.builder() + .id(1L) + .member(member) + .locationPoint(point) + .locationName("Seoul") + .address("Seoul, South Korea") + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(geometryFactory.createPoint(any(Coordinate.class))).willReturn(point); + given(diaryRepository.save(any(Diary.class))).willReturn(savedDiary); + + // when + diaryService.createDiaryAuto(memberId, request); + + // then + then(memberRepository).should().findById(memberId); + then(geometryFactory).should().createPoint(any(Coordinate.class)); + then(diaryRepository).should().save(any(Diary.class)); + then(notificationService).should().createAndSendNotification( + eq(memberId), + eq(NotificationType.MOOD_CHECK), + eq(savedDiary.getId()), + eq(savedDiary.getLocationName()) + ); + } + + @Test + @DisplayName("존재하지 않는 회원이 자동 일기를 작성하려 하면 예외가 발생한다.") + void should_ThrowException_When_MemberNotFoundInAutoCreate() { + // given + Long memberId = 1L; + DiaryAutoCreateRequest request = DiaryAutoCreateRequest.builder() + .locationPoint(new LocationPoint(37.5665, 126.978)) + .locationName("Seoul") + .address("Seoul, South Korea") + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> diaryService.createDiaryAuto(memberId, request)) + .isInstanceOf(GlobalException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.MEMBER_NOT_FOUND); + } + + @Test + @DisplayName("수동 일기가 성공적으로 저장된다.") + void should_SaveManualDiary_When_ValidInput() { + // given + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + + Long categoryId = 1L; + Category category = Category.builder() + .id(categoryId) + .member(member) + .build(); + + DiaryManualCreateRequest request = DiaryManualCreateRequest.builder() + .locationPoint(new LocationPoint(37.5665, 126.978)) + .locationName("Seoul") + .address("Seoul, South Korea") + .content("Test content") + .emoji("😊") + .categories(List.of(new CategoryRequest(categoryId))) + .isBookmark(false) + .isPublic(true) + .build(); + + Point point = mock(Point.class); + Diary savedDiary = Diary.builder() + .id(1L) + .member(member) + .locationPoint(point) + .locationName("Seoul") + .address("Seoul, South Korea") + .content("Test content") + .emoji("😊") + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(geometryFactory.createPoint(any(Coordinate.class))).willReturn(point); + given(diaryRepository.save(any(Diary.class))).willReturn(savedDiary); + given(categoryRepository.findById(categoryId)).willReturn(Optional.of(category)); + + // when + diaryService.createDiaryManual(memberId, request); + + // then + then(memberRepository).should().findById(memberId); + then(geometryFactory).should().createPoint(any(Coordinate.class)); + then(diaryRepository).should().save(any(Diary.class)); + then(categoryRepository).should().findById(categoryId); + then(diaryCategoryRepository).should().save(any(DiaryCategory.class)); + } + + @Test + @DisplayName("존재하지 않는 회원이 수동 일기를 작성하려 하면 예외가 발생한다.") + void should_ThrowException_When_MemberNotFoundInManualCreate() { + // given + Long memberId = 1L; + DiaryManualCreateRequest request = DiaryManualCreateRequest.builder() + .locationPoint(new LocationPoint(37.5665, 126.978)) + .locationName("Seoul") + .address("Seoul, South Korea") + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> diaryService.createDiaryManual(memberId, request)) + .isInstanceOf(GlobalException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.MEMBER_NOT_FOUND); + } + + @Test + @DisplayName("존재하지 않는 카테고리로 수동 일기를 작성하려 하면 예외가 발생한다.") + void should_ThrowException_When_CategoryNotFoundInManualCreate() { + // given + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + + Long categoryId = 1L; + DiaryManualCreateRequest request = DiaryManualCreateRequest.builder() + .locationPoint(new LocationPoint(37.5665, 126.978)) + .locationName("Seoul") + .address("Seoul, South Korea") + .categories(List.of(new CategoryRequest(categoryId))) + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(categoryRepository.findById(categoryId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> diaryService.createDiaryManual(memberId, request)) + .isInstanceOf(GlobalException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.CATEGORY_NOT_FOUND); + } + + @Test + @DisplayName("내 일기 목록이 성공적으로 조회된다.") + void should_ReturnMyDiaries_When_ValidRequest() { + // given + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + + DiaryFilterRequest filterRequest = DiaryFilterRequest.builder() + .key(0) + .size(10) + .build(); + + Diary diary = Diary.builder() + .id(1L) + .member(member) + .content("Test content") + .locationName("Seoul") + .build(); + + Page diaryPage = new PageImpl<>(List.of(diary)); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(diaryRepository.findAll(any(Specification.class), any(Pageable.class))) + .willReturn(diaryPage); + + // when + MyDiariesResponse response = diaryService.getMyDiaries(memberId, filterRequest); + + // then + assertThat(response.getDiaries()).hasSize(1); + assertThat(response.getNext()).isNull(); + then(memberRepository).should().findById(memberId); + then(diaryRepository).should().findAll(any(Specification.class), any(Pageable.class)); + } + + @Test + @DisplayName("내 일기가 성공적으로 조회된다.") + void should_ReturnMyDiary_When_ValidId() { + // given + Long memberId = 1L; + Long diaryId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + Diary diary = Diary.builder() + .id(diaryId) + .member(member) + .content("Test content") + .locationName("Seoul") + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(diaryRepository.findById(diaryId)).willReturn(Optional.of(diary)); + given(diaryCategoryRepository.findByDiary(diary)).willReturn(List.of()); + given(likeRepository.existsByMemberIdAndDiaryId(memberId, diaryId)).willReturn(false); + + // when + MyDiaryResponse response = diaryService.getMyDiary(memberId, diaryId); + + // then + assertThat(response.getId()).isEqualTo(diaryId); + assertThat(response.getContent()).isEqualTo("Test content"); + assertThat(response.getLocationName()).isEqualTo("Seoul"); + then(memberRepository).should().findById(memberId); + then(diaryRepository).should().findById(diaryId); + then(diaryCategoryRepository).should().findByDiary(diary); + then(likeRepository).should().existsByMemberIdAndDiaryId(memberId, diaryId); + } + + @Test + @DisplayName("다른 사용자의 일기를 조회하려고 하면 예외가 발생한다.") + void should_ThrowException_When_NotMyDiary() { + // given + Long memberId = 1L; + Long diaryId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + Member otherMember = Member.builder() + .id(2L) + .build(); + Diary diary = Diary.builder() + .id(diaryId) + .member(otherMember) + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(diaryRepository.findById(diaryId)).willReturn(Optional.of(diary)); + + // when & then + assertThatThrownBy(() -> diaryService.getMyDiary(memberId, diaryId)) + .isInstanceOf(GlobalException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.DIARY_NOT_FOUND); + } + + @Test + @DisplayName("일기의 위치 정보가 성공적으로 조회된다.") + void should_ReturnLocation_When_ValidId() { + // given + Long memberId = 1L; + Long diaryId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + + Point point = mock(Point.class); + given(point.getX()).willReturn(37.5665); + given(point.getY()).willReturn(126.978); + + Diary diary = Diary.builder() + .id(diaryId) + .member(member) + .locationPoint(point) + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(diaryRepository.findById(diaryId)).willReturn(Optional.of(diary)); + + // when + LocationPoint location = diaryService.getDiaryLocation(memberId, diaryId); + + // then + assertThat(location.getLatitude()).isEqualTo(37.5665); + assertThat(location.getLongitude()).isEqualTo(126.978); + then(memberRepository).should().findById(memberId); + then(diaryRepository).should().findById(diaryId); + } + + @Test + @DisplayName("일기가 성공적으로 수정된다.") + void should_UpdateDiary_When_ValidRequest() { + // given + Long memberId = 1L; + Long diaryId = 1L; + Long categoryId = 1L; + + Member member = Member.builder() + .id(memberId) + .build(); + + Category category = Category.builder() + .id(categoryId) + .member(member) + .build(); + + Diary diary = Diary.builder() + .id(diaryId) + .member(member) + .content("Original content") + .build(); + + DiaryPatchRequest request = DiaryPatchRequest.builder() + .content("Updated content") + .locationName("Updated location") + .categories(List.of(new CategoryRequest(categoryId))) + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(diaryRepository.findById(diaryId)).willReturn(Optional.of(diary)); + given(categoryRepository.findById(categoryId)).willReturn(Optional.of(category)); + + // when + diaryService.updateDiary(memberId, diaryId, request); + + // then + assertThat(diary.getContent()).isEqualTo("Updated content"); + assertThat(diary.getLocationName()).isEqualTo("Updated location"); + then(memberRepository).should().findById(memberId); + then(diaryRepository).should().findById(diaryId); + then(diaryCategoryRepository).should().deleteByDiary(diary); + then(categoryRepository).should().findById(categoryId); + then(diaryCategoryRepository).should().save(any(DiaryCategory.class)); + } + + @Test + @DisplayName("일기가 성공적으로 삭제된다.") + void should_DeleteDiary_When_ValidId() { + // given + Long memberId = 1L; + Long diaryId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + Diary diary = Diary.builder() + .id(diaryId) + .member(member) + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(diaryRepository.findById(diaryId)).willReturn(Optional.of(diary)); + + // when + diaryService.deleteDiary(memberId, diaryId); + + // then + then(memberRepository).should().findById(memberId); + then(diaryRepository).should().findById(diaryId); + then(diaryRepository).should().delete(diary); + } + + @Test + @DisplayName("북마크가 성공적으로 토글된다.") + void should_ToggleBookmark_When_ValidId() { + // given + Long memberId = 1L; + Long diaryId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + Diary diary = Diary.builder() + .id(diaryId) + .member(member) + .isBookmark(false) + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(diaryRepository.findById(diaryId)).willReturn(Optional.of(diary)); + + // when + diaryService.toggleBookmark(memberId, diaryId); + + // then + assertThat(diary.isBookmark()).isTrue(); + then(memberRepository).should().findById(memberId); + then(diaryRepository).should().findById(diaryId); + } + + @Test + @DisplayName("공개 상태가 성공적으로 토글된다.") + void should_TogglePrivacy_When_ValidId() { + // given + Long memberId = 1L; + Long diaryId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + Diary diary = Diary.builder() + .id(diaryId) + .member(member) + .isPublic(false) + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(diaryRepository.findById(diaryId)).willReturn(Optional.of(diary)); + + // when + diaryService.togglePrivacy(memberId, diaryId); + + // then + assertThat(diary.isPublic()).isTrue(); + then(memberRepository).should().findById(memberId); + then(diaryRepository).should().findById(diaryId); + } + + @Test + @DisplayName("일기 내용을 삭제하면 null로 설정된다.") + void should_SetContentNull_When_ContentDeleteIsTrue() { + // given + Long memberId = 1L; + Long diaryId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + Diary diary = Diary.builder() + .id(diaryId) + .member(member) + .content("Original content") + .build(); + + DiaryPatchRequest request = DiaryPatchRequest.builder() + .contentDelete(true) + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(diaryRepository.findById(diaryId)).willReturn(Optional.of(diary)); + + // when + diaryService.updateDiary(memberId, diaryId, request); + + // then + assertThat(diary.getContent()).isNull(); + then(memberRepository).should().findById(memberId); + then(diaryRepository).should().findById(diaryId); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/service/FcmServiceTest.java b/src/test/java/com/potatocake/everymoment/service/FcmServiceTest.java new file mode 100644 index 0000000..3713901 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/service/FcmServiceTest.java @@ -0,0 +1,181 @@ +package com.potatocake.everymoment.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.Mockito.mock; + +import com.google.firebase.messaging.BatchResponse; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.MessagingErrorCode; +import com.google.firebase.messaging.SendResponse; +import com.potatocake.everymoment.dto.request.FcmNotificationRequest; +import com.potatocake.everymoment.entity.DeviceToken; +import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.repository.DeviceTokenRepository; +import com.potatocake.everymoment.repository.MemberRepository; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class FcmServiceTest { + + @InjectMocks + private FcmService fcmService; + + @Mock + private FirebaseMessaging firebaseMessaging; + + @Mock + private DeviceTokenRepository deviceTokenRepository; + + @Mock + private MemberRepository memberRepository; + + @Test + @DisplayName("알림이 성공적으로 전송된다.") + void should_SendNotification_When_ValidInput() throws Exception { + // given + Long targetMemberId = 1L; + DeviceToken deviceToken = DeviceToken.builder() + .fcmToken("fcm-token-123") + .build(); + + FcmNotificationRequest request = FcmNotificationRequest.builder() + .title("Test Title") + .body("Test Body") + .type("TEST") + .targetId(1L) + .build(); + + BatchResponse batchResponse = mock(BatchResponse.class); + SendResponse sendResponse = mock(SendResponse.class); + given(sendResponse.isSuccessful()).willReturn(true); + given(batchResponse.getResponses()).willReturn(List.of(sendResponse)); + + given(deviceTokenRepository.findAllByMemberId(targetMemberId)) + .willReturn(List.of(deviceToken)); + given(firebaseMessaging.sendEach(any())).willReturn(batchResponse); + + // when + fcmService.sendNotification(targetMemberId, request); + + // then + then(firebaseMessaging).should().sendEach(any()); + } + + @Test + @DisplayName("토큰이 성공적으로 등록된다.") + void should_RegisterToken_When_ValidInput() { + // given + Long memberId = 1L; + String deviceId = "device123"; + String fcmToken = "fcm-token-123"; + + Member member = Member.builder() + .id(memberId) + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(deviceTokenRepository.findByMemberIdAndDeviceId(memberId, deviceId)) + .willReturn(Optional.empty()); + + // when + fcmService.registerToken(memberId, deviceId, fcmToken); + + // then + then(deviceTokenRepository).should().save(any(DeviceToken.class)); + } + + @Test + @DisplayName("기존 토큰이 성공적으로 업데이트된다.") + void should_UpdateToken_When_TokenExists() { + // given + Long memberId = 1L; + String deviceId = "device123"; + String fcmToken = "fcm-token-123"; + + Member member = Member.builder() + .id(memberId) + .build(); + + DeviceToken existingToken = DeviceToken.builder() + .member(member) + .deviceId(deviceId) + .fcmToken("old-token") + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(deviceTokenRepository.findByMemberIdAndDeviceId(memberId, deviceId)) + .willReturn(Optional.of(existingToken)); + + // when + fcmService.registerToken(memberId, deviceId, fcmToken); + + // then + assertThat(existingToken.getFcmToken()).isEqualTo(fcmToken); + } + + @Test + @DisplayName("토큰이 성공적으로 삭제된다.") + void should_RemoveToken_When_ValidInput() { + // given + Long memberId = 1L; + String deviceId = "device123"; + + willDoNothing().given(deviceTokenRepository) + .deleteByMemberIdAndDeviceId(memberId, deviceId); + + // when + fcmService.removeToken(memberId, deviceId); + + // then + then(deviceTokenRepository).should().deleteByMemberIdAndDeviceId(memberId, deviceId); + } + + @Test + @DisplayName("잘못된 토큰은 자동으로 삭제된다.") + void should_DeleteToken_When_TokenInvalid() throws Exception { + // given + Long targetMemberId = 1L; + DeviceToken deviceToken = DeviceToken.builder() + .fcmToken("invalid-token") + .build(); + + FcmNotificationRequest request = FcmNotificationRequest.builder() + .title("Test Title") + .body("Test Body") + .type("TEST") + .targetId(1L) + .build(); + + BatchResponse batchResponse = mock(BatchResponse.class); + SendResponse sendResponse = mock(SendResponse.class); + FirebaseMessagingException exception = mock(FirebaseMessagingException.class); + + given(sendResponse.isSuccessful()).willReturn(false); + given(sendResponse.getException()).willReturn(exception); + given(exception.getMessagingErrorCode()).willReturn(MessagingErrorCode.UNREGISTERED); + given(batchResponse.getResponses()).willReturn(List.of(sendResponse)); + + given(deviceTokenRepository.findAllByMemberId(targetMemberId)) + .willReturn(List.of(deviceToken)); + given(firebaseMessaging.sendEach(any())).willReturn(batchResponse); + + // when + fcmService.sendNotification(targetMemberId, request); + + // then + then(deviceTokenRepository).should().deleteAll(any()); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/service/FileServiceTest.java b/src/test/java/com/potatocake/everymoment/service/FileServiceTest.java new file mode 100644 index 0000000..d2f07a7 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/service/FileServiceTest.java @@ -0,0 +1,134 @@ +package com.potatocake.everymoment.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +import com.potatocake.everymoment.dto.response.FileResponse; +import com.potatocake.everymoment.entity.Diary; +import com.potatocake.everymoment.entity.File; +import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.repository.DiaryRepository; +import com.potatocake.everymoment.repository.FileRepository; +import com.potatocake.everymoment.util.S3FileUploader; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.web.multipart.MultipartFile; + +@ExtendWith(MockitoExtension.class) +class FileServiceTest { + + @InjectMocks + private FileService fileService; + + @Mock + private FileRepository fileRepository; + + @Mock + private DiaryRepository diaryRepository; + + @Mock + private S3FileUploader uploader; + + @Test + @DisplayName("파일 목록이 성공적으로 조회된다.") + void should_GetFiles_When_ValidDiaryId() { + // given + Long diaryId = 1L; + List files = List.of( + File.builder() + .id(1L) + .imageUrl("https://example.com/image1.jpg") + .order(1) + .build(), + File.builder() + .id(2L) + .imageUrl("https://example.com/image2.jpg") + .order(2) + .build() + ); + + given(fileRepository.findByDiaryId(diaryId)).willReturn(files); + + // when + List responses = fileService.getFiles(diaryId); + + // then + assertThat(responses).hasSize(2); + assertThat(responses).extracting("imageUrl") + .containsExactly( + "https://example.com/image1.jpg", + "https://example.com/image2.jpg" + ); + } + + @Test + @DisplayName("파일이 성공적으로 업로드된다.") + void should_UploadFiles_When_ValidInput() { + // given + Long diaryId = 1L; + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + Diary diary = Diary.builder() + .id(diaryId) + .member(member) + .build(); + + MultipartFile file1 = mock(MultipartFile.class); + MultipartFile file2 = mock(MultipartFile.class); + List files = List.of(file1, file2); + + given(diaryRepository.findById(diaryId)).willReturn(Optional.of(diary)); + given(uploader.uploadFile(any(MultipartFile.class))) + .willReturn("https://example.com/image1.jpg") + .willReturn("https://example.com/image2.jpg"); + + // when + fileService.uploadFiles(diaryId, memberId, files); + + // then + then(fileRepository).should().saveAll(anyList()); + then(uploader).should().uploadFile(file1); + then(uploader).should().uploadFile(file2); + } + + @Test + @DisplayName("파일이 성공적으로 수정된다.") + void should_UpdateFiles_When_ValidInput() { + // given + Long diaryId = 1L; + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + Diary diary = Diary.builder() + .id(diaryId) + .member(member) + .build(); + + MultipartFile file = mock(MultipartFile.class); + List files = List.of(file); + + given(diaryRepository.findById(diaryId)).willReturn(Optional.of(diary)); + given(uploader.uploadFile(file)).willReturn("https://example.com/new-image.jpg"); + + // when + fileService.updateFiles(diaryId, memberId, files); + + // then + then(fileRepository).should().deleteByDiary(diary); + then(fileRepository).should().saveAll(anyList()); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/service/FriendRequestServiceTest.java b/src/test/java/com/potatocake/everymoment/service/FriendRequestServiceTest.java new file mode 100644 index 0000000..35c6548 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/service/FriendRequestServiceTest.java @@ -0,0 +1,297 @@ +package com.potatocake.everymoment.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.internal.verification.VerificationModeFactory.times; + +import com.potatocake.everymoment.constant.NotificationType; +import com.potatocake.everymoment.dto.response.FriendRequestPageRequest; +import com.potatocake.everymoment.entity.Friend; +import com.potatocake.everymoment.entity.FriendRequest; +import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.exception.ErrorCode; +import com.potatocake.everymoment.exception.GlobalException; +import com.potatocake.everymoment.repository.FriendRepository; +import com.potatocake.everymoment.repository.FriendRequestRepository; +import com.potatocake.everymoment.repository.MemberRepository; +import com.potatocake.everymoment.util.PagingUtil; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Window; + +@ExtendWith(MockitoExtension.class) +class FriendRequestServiceTest { + + @InjectMocks + private FriendRequestService friendRequestService; + + @Mock + private FriendRequestRepository friendRequestRepository; + + @Mock + private FriendRepository friendRepository; + + @Mock + private MemberRepository memberRepository; + + @Mock + private NotificationService notificationService; + + @Mock + private PagingUtil pagingUtil; + + @Test + @DisplayName("친구 요청 목록이 성공적으로 조회된다.") + void should_GetFriendRequests_When_ValidRequest() { + // given + Long memberId = 1L; + Long key = null; + int size = 10; + + Member requester = Member.builder() + .id(2L) + .nickname("requester") + .profileImageUrl("https://example.com/profile.jpg") + .build(); + + FriendRequest request = FriendRequest.builder() + .id(1L) + .sender(requester) + .build(); + + ScrollPosition scrollPosition = ScrollPosition.offset(); + Pageable pageable = PageRequest.of(0, size); + Window window = Window.from(List.of(request), i -> scrollPosition, false); + + given(pagingUtil.createScrollPosition(key)).willReturn(scrollPosition); + given(pagingUtil.createPageable(size, Sort.Direction.DESC)).willReturn(pageable); + given(friendRequestRepository.findByReceiverId(memberId, scrollPosition, pageable)) + .willReturn(window); + given(memberRepository.findAllById(any())).willReturn(List.of(requester)); + + given(pagingUtil.getNextKey(any(), any())).willReturn(null); + + // when + FriendRequestPageRequest response = friendRequestService.getFriendRequests(key, size, memberId); + + // then + assertThat(response.getFriendRequests()).hasSize(1); + assertThat(response.getFriendRequests().get(0).getNickname()).isEqualTo("requester"); + assertThat(response.getNext()).isNull(); + } + + @Test + @DisplayName("친구 요청이 성공적으로 전송된다.") + void should_SendFriendRequest_When_ValidRequest() { + // given + Long senderId = 1L; + Long receiverId = 2L; + Member sender = Member.builder() + .id(senderId) + .nickname("sender") + .build(); + Member receiver = Member.builder() + .id(receiverId) + .build(); + + FriendRequest savedRequest = FriendRequest.builder() + .id(1L) // ID 설정 + .sender(sender) + .receiver(receiver) + .build(); + + given(memberRepository.findById(senderId)).willReturn(Optional.of(sender)); + given(memberRepository.findById(receiverId)).willReturn(Optional.of(receiver)); + given(friendRepository.existsByMemberIdAndFriendId(senderId, receiverId)).willReturn(false); + given(friendRequestRepository.existsBySenderIdAndReceiverId(senderId, receiverId)) + .willReturn(false); + given(friendRequestRepository.save(any(FriendRequest.class))) + .willReturn(savedRequest); + + // when + friendRequestService.sendFriendRequest(senderId, receiverId); + + // then + then(friendRequestRepository).should().save(any(FriendRequest.class)); + then(notificationService).should().createAndSendNotification( + eq(receiverId), + eq(NotificationType.FRIEND_REQUEST), + eq(savedRequest.getId()), + eq(sender.getNickname()) + ); + } + + @Test + @DisplayName("이미 친구인 사용자에게 요청을 보내면 예외가 발생한다.") + void should_ThrowException_When_AlreadyFriends() { + // given + Long senderId = 1L; + Long receiverId = 2L; + + given(friendRepository.existsByMemberIdAndFriendId(senderId, receiverId)).willReturn(true); + + // when & then + assertThatThrownBy(() -> friendRequestService.sendFriendRequest(senderId, receiverId)) + .isInstanceOf(GlobalException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.ALREADY_FRIEND); + } + + @Test + @DisplayName("이미 요청을 보낸 사용자에게 다시 요청을 보내면 예외가 발생한다.") + void should_ThrowException_When_RequestAlreadyExists() { + // given + Long senderId = 1L; + Long receiverId = 2L; + + given(friendRequestRepository.existsBySenderIdAndReceiverId(senderId, receiverId)) + .willReturn(true); + + // when & then + assertThatThrownBy(() -> friendRequestService.sendFriendRequest(senderId, receiverId)) + .isInstanceOf(GlobalException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.FRIEND_REQUEST_ALREADY_EXISTS); + } + + @Test + @DisplayName("친구 요청이 성공적으로 수락된다.") + void should_AcceptFriendRequest_When_ValidRequest() { + // given + Long requestId = 1L; + Long receiverId = 1L; + + Member sender = Member.builder() + .id(2L) + .nickname("sender") + .build(); + Member receiver = Member.builder() + .id(receiverId) + .build(); + + FriendRequest request = FriendRequest.builder() + .id(requestId) + .sender(sender) + .receiver(receiver) + .build(); + + given(friendRequestRepository.findById(requestId)) + .willReturn(Optional.of(request)); + + // when + friendRequestService.acceptFriendRequest(requestId, receiverId); + + // then + then(friendRepository).should(times(2)).save(any(Friend.class)); + then(friendRequestRepository).should().delete(request); + then(notificationService).should().createAndSendNotification( + eq(sender.getId()), + eq(NotificationType.FRIEND_ACCEPT), + eq(receiver.getId()), + eq(receiver.getNickname()) + ); + } + + @Test + @DisplayName("다른 사용자의 친구 요청을 수락하려고 하면 예외가 발생한다.") + void should_ThrowException_When_AcceptingOtherUserRequest() { + // given + Long requestId = 1L; + Long receiverId = 1L; + Long otherUserId = 3L; + + Member sender = Member.builder() + .id(2L) + .build(); + Member receiver = Member.builder() + .id(otherUserId) // 다른 사용자 + .build(); + + FriendRequest request = FriendRequest.builder() + .id(requestId) + .sender(sender) + .receiver(receiver) + .build(); + + given(friendRequestRepository.findById(requestId)) + .willReturn(Optional.of(request)); + + // when & then + assertThatThrownBy(() -> friendRequestService.acceptFriendRequest(requestId, receiverId)) + .isInstanceOf(GlobalException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.FRIEND_REQUEST_NOT_FOUND); + } + + @Test + @DisplayName("친구 요청이 성공적으로 거절된다.") + void should_RejectFriendRequest_When_ValidRequest() { + // given + Long requestId = 1L; + Long receiverId = 1L; + + Member sender = Member.builder() + .id(2L) + .build(); + Member receiver = Member.builder() + .id(receiverId) + .build(); + + FriendRequest request = FriendRequest.builder() + .id(requestId) + .sender(sender) + .receiver(receiver) + .build(); + + given(friendRequestRepository.findById(requestId)) + .willReturn(Optional.of(request)); + + // when + friendRequestService.rejectFriendRequest(requestId, receiverId); + + // then + then(friendRequestRepository).should().delete(request); + } + + @Test + @DisplayName("다른 사용자의 친구 요청을 거절하려고 하면 예외가 발생한다.") + void should_ThrowException_When_RejectingOtherUserRequest() { + // given + Long requestId = 1L; + Long receiverId = 1L; + Long otherUserId = 3L; + + Member sender = Member.builder() + .id(2L) + .build(); + Member receiver = Member.builder() + .id(otherUserId) // 다른 사용자 + .build(); + + FriendRequest request = FriendRequest.builder() + .id(requestId) + .sender(sender) + .receiver(receiver) + .build(); + + given(friendRequestRepository.findById(requestId)) + .willReturn(Optional.of(request)); + + // when & then + assertThatThrownBy(() -> friendRequestService.rejectFriendRequest(requestId, receiverId)) + .isInstanceOf(GlobalException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.FRIEND_REQUEST_NOT_FOUND); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/service/FriendServiceTest.java b/src/test/java/com/potatocake/everymoment/service/FriendServiceTest.java new file mode 100644 index 0000000..70209c6 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/service/FriendServiceTest.java @@ -0,0 +1,188 @@ +package com.potatocake.everymoment.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; + +import com.potatocake.everymoment.dto.response.FriendListResponse; +import com.potatocake.everymoment.dto.response.OneFriendDiariesResponse; +import com.potatocake.everymoment.entity.Diary; +import com.potatocake.everymoment.entity.Friend; +import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.exception.ErrorCode; +import com.potatocake.everymoment.exception.GlobalException; +import com.potatocake.everymoment.repository.DiaryRepository; +import com.potatocake.everymoment.repository.FileRepository; +import com.potatocake.everymoment.repository.FriendRepository; +import com.potatocake.everymoment.repository.MemberRepository; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; + +@ExtendWith(MockitoExtension.class) +class FriendServiceTest { + + @InjectMocks + private FriendService friendService; + + @Mock + private FriendRepository friendRepository; + + @Mock + private MemberRepository memberRepository; + + @Mock + private DiaryRepository diaryRepository; + + @Mock + private FileRepository fileRepository; + + @Test + @DisplayName("특정 친구의 일기 목록이 성공적으로 조회된다.") + void should_ReturnFriendDiaries_When_ValidRequest() { + // given + Long memberId = 1L; + Long friendId = 2L; + Member member = Member.builder() + .id(memberId) + .build(); + Member friend = Member.builder() + .id(friendId) + .build(); + Friend friendship = Friend.builder() + .member(member) + .friend(friend) + .build(); + + Diary diary = Diary.builder() + .id(1L) + .member(friend) + .content("Test diary") + .isPublic(true) + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(memberRepository.findById(friendId)).willReturn(Optional.of(friend)); + given(friendRepository.findByMemberAndFriend(member, friend)) + .willReturn(Optional.of(friendship)); + given(diaryRepository.findAll(any(Specification.class), any(PageRequest.class))) + .willReturn(new PageImpl<>(List.of(diary))); + + // when + OneFriendDiariesResponse response = friendService + .OneFriendDiariesResponse(memberId, friendId, null, 0, 10); + + // then + assertThat(response.getDiaries()).hasSize(1); + assertThat(response.getDiaries().get(0).getContent()).isEqualTo("Test diary"); + + then(memberRepository).should().findById(memberId); + then(memberRepository).should().findById(friendId); + then(friendRepository).should().findByMemberAndFriend(member, friend); + then(diaryRepository).should().findAll(any(Specification.class), any(PageRequest.class)); + } + + @Test + @DisplayName("친구 목록이 성공적으로 조회된다.") + void should_ReturnFriendList_When_ValidRequest() { + // given + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + Member friend = Member.builder() + .id(2L) + .nickname("friend") + .profileImageUrl("https://example.com/profile.jpg") + .build(); + Friend friendship = Friend.builder() + .member(member) + .friend(friend) + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(friendRepository.findAll(any(Specification.class), any(PageRequest.class))) + .willReturn(new PageImpl<>(List.of(friendship))); + + // when + FriendListResponse response = friendService.getFriendList(memberId, null, 0, 10); + + // then + assertThat(response.getFriends()).hasSize(1); + assertThat(response.getFriends().get(0).getNickname()).isEqualTo("friend"); + + then(memberRepository).should().findById(memberId); + then(friendRepository).should().findAll(any(Specification.class), any(PageRequest.class)); + } + + @Test + @DisplayName("친구가 성공적으로 삭제된다.") + void should_DeleteFriend_When_ValidRequest() { + // given + Long memberId = 1L; + Long friendId = 2L; + Member member = Member.builder() + .id(memberId) + .build(); + Member friend = Member.builder() + .id(friendId) + .build(); + Friend friendship1 = Friend.builder() + .member(member) + .friend(friend) + .build(); + Friend friendship2 = Friend.builder() + .member(friend) + .friend(member) + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(memberRepository.findById(friendId)).willReturn(Optional.of(friend)); + given(friendRepository.findByMemberAndFriend(member, friend)) + .willReturn(Optional.of(friendship1)); + given(friendRepository.findByMemberAndFriend(friend, member)) + .willReturn(Optional.of(friendship2)); + + // when + friendService.deleteFriend(memberId, friendId); + + // then + then(friendRepository).should().delete(friendship1); + then(friendRepository).should().delete(friendship2); + } + + @Test + @DisplayName("존재하지 않는 친구를 삭제하려고 하면 예외가 발생한다.") + void should_ThrowException_When_FriendNotFound() { + // given + Long memberId = 1L; + Long friendId = 2L; + Member member = Member.builder() + .id(memberId) + .build(); + Member friend = Member.builder() + .id(friendId) + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(memberRepository.findById(friendId)).willReturn(Optional.of(friend)); + given(friendRepository.findByMemberAndFriend(member, friend)) + .willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> friendService.deleteFriend(memberId, friendId)) + .isInstanceOf(GlobalException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.FRIEND_NOT_FOUND); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/service/LikeServiceTest.java b/src/test/java/com/potatocake/everymoment/service/LikeServiceTest.java new file mode 100644 index 0000000..f645c71 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/service/LikeServiceTest.java @@ -0,0 +1,158 @@ +package com.potatocake.everymoment.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; + +import com.potatocake.everymoment.constant.NotificationType; +import com.potatocake.everymoment.dto.response.LikeCountResponse; +import com.potatocake.everymoment.entity.Diary; +import com.potatocake.everymoment.entity.Like; +import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.exception.ErrorCode; +import com.potatocake.everymoment.exception.GlobalException; +import com.potatocake.everymoment.repository.DiaryRepository; +import com.potatocake.everymoment.repository.LikeRepository; +import com.potatocake.everymoment.repository.MemberRepository; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class LikeServiceTest { + + @InjectMocks + private LikeService likeService; + + @Mock + private LikeRepository likeRepository; + + @Mock + private DiaryRepository diaryRepository; + + @Mock + private MemberRepository memberRepository; + + @Mock + private NotificationService notificationService; + + @Test + @DisplayName("좋아요 수가 성공적으로 조회된다.") + void should_ReturnLikeCount_When_ValidDiaryId() { + // given + Long diaryId = 1L; + Diary diary = Diary.builder() + .id(diaryId) + .build(); + + given(diaryRepository.findById(diaryId)).willReturn(Optional.of(diary)); + given(likeRepository.countByDiary(diary)).willReturn(5L); + + // when + LikeCountResponse response = likeService.getLikeCount(diaryId); + + // then + assertThat(response.getLikeCount()).isEqualTo(5L); + then(diaryRepository).should().findById(diaryId); + then(likeRepository).should().countByDiary(diary); + } + + @Test + @DisplayName("좋아요가 성공적으로 추가된다.") + void should_AddLike_When_NotLiked() { + // given + Long memberId = 1L; + Long diaryId = 1L; + Member member = Member.builder() + .id(memberId) + .nickname("testUser") + .build(); + Member diaryOwner = Member.builder() + .id(2L) + .build(); + Diary diary = Diary.builder() + .id(diaryId) + .member(diaryOwner) + .isPublic(true) + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(diaryRepository.findById(diaryId)).willReturn(Optional.of(diary)); + given(likeRepository.findByMemberIdAndDiaryId(memberId, diaryId)) + .willReturn(Optional.empty()); + + // when + likeService.toggleLike(memberId, diaryId); + + // then + then(likeRepository).should().save(any(Like.class)); + then(notificationService).should().createAndSendNotification( + eq(diaryOwner.getId()), + eq(NotificationType.LIKE), + eq(diaryId), + eq(member.getNickname()) + ); + } + + @Test + @DisplayName("좋아요가 성공적으로 취소된다.") + void should_RemoveLike_When_AlreadyLiked() { + // given + Long memberId = 1L; + Long diaryId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + Diary diary = Diary.builder() + .id(diaryId) + .isPublic(true) + .build(); + Like like = Like.builder() + .id(1L) + .member(member) + .diary(diary) + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(diaryRepository.findById(diaryId)).willReturn(Optional.of(diary)); + given(likeRepository.findByMemberIdAndDiaryId(memberId, diaryId)) + .willReturn(Optional.of(like)); + + // when + likeService.toggleLike(memberId, diaryId); + + // then + then(likeRepository).should().delete(like); + } + + @Test + @DisplayName("다른 사용자의 비공개 일기에 좋아요를 누르면 예외가 발생한다.") + void should_ThrowException_When_DiaryNotPublic() { + // given + Long memberId = 1L; + Long diaryId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + Diary diary = Diary.builder() + .id(diaryId) + .isPublic(false) + .member(Member.builder().id(2L).build()) // 다른 사용자의 일기 + .build(); + + given(diaryRepository.findById(diaryId)).willReturn(Optional.of(diary)); + + // when & then + assertThatThrownBy(() -> likeService.toggleLike(memberId, diaryId)) + .isInstanceOf(GlobalException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.DIARY_NOT_PUBLIC); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/service/MemberServiceTest.java b/src/test/java/com/potatocake/everymoment/service/MemberServiceTest.java index df574a6..f4283fb 100644 --- a/src/test/java/com/potatocake/everymoment/service/MemberServiceTest.java +++ b/src/test/java/com/potatocake/everymoment/service/MemberServiceTest.java @@ -1,23 +1,25 @@ package com.potatocake.everymoment.service; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; -import static org.mockito.Mockito.mock; -import static org.mockito.internal.verification.VerificationModeFactory.times; +import static org.mockito.BDDMockito.willDoNothing; +import com.potatocake.everymoment.dto.response.AnonymousLoginResponse; import com.potatocake.everymoment.dto.response.FriendRequestStatus; import com.potatocake.everymoment.dto.response.MemberDetailResponse; import com.potatocake.everymoment.dto.response.MemberSearchResponse; import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.exception.ErrorCode; import com.potatocake.everymoment.exception.GlobalException; import com.potatocake.everymoment.repository.FriendRepository; import com.potatocake.everymoment.repository.FriendRequestRepository; import com.potatocake.everymoment.repository.MemberRepository; +import com.potatocake.everymoment.util.JwtUtil; import com.potatocake.everymoment.util.PagingUtil; import com.potatocake.everymoment.util.S3FileUploader; import java.util.List; @@ -28,9 +30,12 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Sort; import org.springframework.data.domain.Window; -import org.springframework.web.multipart.MultipartFile; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; @ExtendWith(MockitoExtension.class) class MemberServiceTest { @@ -42,10 +47,10 @@ class MemberServiceTest { private MemberRepository memberRepository; @Mock - private FriendRequestRepository friendRequestRepository; + private FriendRepository friendRepository; @Mock - private FriendRepository friendRepository; + private FriendRequestRepository friendRequestRepository; @Mock private PagingUtil pagingUtil; @@ -53,92 +58,180 @@ class MemberServiceTest { @Mock private S3FileUploader s3FileUploader; + @Mock + private JwtUtil jwtUtil; + + @Test + @DisplayName("익명 로그인이 성공적으로 수행된다.") + void should_LoginAnonymously_When_ValidRequest() { + // given + Long nextNumber = 1234L; + given(memberRepository.findNextAnonymousNumber()).willReturn(nextNumber); + given(memberRepository.save(any(Member.class))).willAnswer(invocation -> { + Member member = invocation.getArgument(0); + return Member.builder() + .id(1L) + .number(member.getNumber()) + .nickname(member.getNickname()) + .build(); + }); + given(jwtUtil.create(anyLong())).willReturn("jwt-token"); + + // when + AnonymousLoginResponse response = memberService.anonymousLogin(null); + + // then + assertThat(response.getNumber()).isEqualTo(nextNumber); + assertThat(response.getToken()).isEqualTo("jwt-token"); + then(memberRepository).should().findNextAnonymousNumber(); + then(memberRepository).should().save(any(Member.class)); + then(jwtUtil).should().create(anyLong()); + } + + @Test + @DisplayName("기존 번호로 익명 로그인하면 토큰이 발급된다.") + void should_ReturnToken_When_ExistingNumber() { + // given + Long memberNumber = 1234L; + Member member = Member.builder() + .id(1L) + .number(memberNumber) + .build(); + + given(memberRepository.findByNumber(memberNumber)).willReturn(Optional.of(member)); + given(jwtUtil.create(member.getId())).willReturn("jwt-token"); + + // when + AnonymousLoginResponse response = memberService.anonymousLogin(memberNumber); + + // then + assertThat(response.getToken()).isEqualTo("jwt-token"); + assertThat(response.getNumber()).isNull(); // 기존 회원이므로 번호를 반환하지 않음 + then(memberRepository).should().findByNumber(memberNumber); + then(jwtUtil).should().create(member.getId()); + } + @Test - @DisplayName("회원 목록 검색이 성공적으로 수행된다.") - void should_ReturnMemberList_When_ValidSearchConditions() { + @DisplayName("회원 검색이 성공적으로 수행된다.") + void should_SearchMembers_When_ValidRequest() { // given - String nickname = "testUser"; - Long key = 1L; + String nickname = "test"; + Long key = null; int size = 10; Long currentMemberId = 1L; - Member member1 = Member.builder().id(2L).nickname("testUser1").build(); - Member member2 = Member.builder().id(3L).nickname("testUser2").build(); - List members = List.of(member1, member2); - Window window = Window.from(members, ScrollPosition::offset, false); - - given(memberRepository.findByNicknameContaining(anyString(), any(), any())) - .willReturn(window); - given(friendRepository.existsByMemberIdAndFriendId(anyLong(), anyLong())) - .willReturn(false); + Member member1 = Member.builder() + .id(2L) + .nickname("testUser1") + .build(); + Member member2 = Member.builder() + .id(3L) + .nickname("testUser2") + .build(); + + ScrollPosition scrollPosition = ScrollPosition.offset(); + Window window = Window.from(List.of(member1, member2), i -> scrollPosition, false); + + given(pagingUtil.createScrollPosition(key)).willReturn(scrollPosition); + given(pagingUtil.createPageable(size, Sort.Direction.ASC)).willReturn(PageRequest.of(0, size)); + given(memberRepository.findByNicknameContaining(anyString(), any(), any())).willReturn(window); + given(friendRepository.existsByMemberIdAndFriendId(anyLong(), anyLong())).willReturn(false); given(friendRequestRepository.findBySenderIdAndReceiverId(anyLong(), anyLong())) .willReturn(Optional.empty()); - given(pagingUtil.getNextKey(any(), any())).willReturn(null); // when - MemberSearchResponse result = memberService.searchMembers(nickname, key, size, currentMemberId); + MemberSearchResponse response = memberService.searchMembers(nickname, key, size, currentMemberId); // then - assertThat(result).isNotNull(); - assertThat(result.getMembers()).hasSize(2); - assertThat(result.getMembers().get(0).getFriendRequestStatus()).isEqualTo(FriendRequestStatus.NONE); - assertThat(result.getMembers().get(1).getFriendRequestStatus()).isEqualTo(FriendRequestStatus.NONE); - assertThat(result.getNext()).isNull(); - - then(memberRepository).should().findByNicknameContaining(anyString(), any(), any()); - then(friendRepository).should(times(2)).existsByMemberIdAndFriendId(anyLong(), anyLong()); - then(friendRequestRepository).should(times(4)).findBySenderIdAndReceiverId(anyLong(), anyLong()); - then(pagingUtil).should().getNextKey(any(), any()); + assertThat(response.getMembers()).hasSize(2); + assertThat(response.getMembers()).extracting("nickname") + .containsExactly("testUser1", "testUser2"); + assertThat(response.getMembers()).extracting("friendRequestStatus") + .containsOnly(FriendRequestStatus.NONE); } @Test - @DisplayName("내 정보 조회가 성공적으로 수행된다.") - void should_ReturnMyInfo_When_ValidMemberId() { + @DisplayName("내 정보가 성공적으로 조회된다.") + void should_ReturnMyInfo_When_ValidId() { // given Long memberId = 1L; - Member member = Member.builder().build(); + Member member = Member.builder() + .id(memberId) + .nickname("testUser") + .profileImageUrl("https://example.com/profile.jpg") + .build(); + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); // when - MemberDetailResponse result = memberService.getMyInfo(memberId); + MemberDetailResponse response = memberService.getMyInfo(memberId); // then - assertThat(result).isNotNull(); - - then(memberRepository).should().findById(memberId); + assertThat(response.getId()).isEqualTo(memberId); + assertThat(response.getNickname()).isEqualTo("testUser"); + assertThat(response.getProfileImageUrl()).isEqualTo("https://example.com/profile.jpg"); } @Test - @DisplayName("회원 정보 수정이 성공적으로 수행된다.") + @DisplayName("회원 정보가 성공적으로 수정된다.") void should_UpdateMemberInfo_When_ValidInput() { // given Long memberId = 1L; - MultipartFile profileImage = mock(MultipartFile.class); - String nickname = "newNickname"; + Member member = Member.builder() + .id(memberId) + .nickname("oldNickname") + .profileImageUrl("https://example.com/old.jpg") + .build(); + + MockMultipartFile profileImage = new MockMultipartFile( + "profileImage", + "profile.jpg", + MediaType.IMAGE_JPEG_VALUE, + "test image".getBytes() + ); + String newNickname = "newNickname"; - Member member = Member.builder().build(); given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); - given(s3FileUploader.uploadFile(profileImage)).willReturn("profileUrl"); + given(s3FileUploader.uploadFile(profileImage)).willReturn("https://example.com/new.jpg"); // when - memberService.updateMemberInfo(memberId, profileImage, nickname); + memberService.updateMemberInfo(memberId, profileImage, newNickname); // then - assertThat(member.getNickname()).isEqualTo("newNickname"); + assertThat(member.getNickname()).isEqualTo(newNickname); + assertThat(member.getProfileImageUrl()).isEqualTo("https://example.com/new.jpg"); + } - then(memberRepository).should().findById(memberId); + @Test + @DisplayName("회원이 성공적으로 삭제된다.") + void should_DeleteMember_When_ValidId() { + // given + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + willDoNothing().given(memberRepository).delete(member); + + // when + memberService.deleteMember(memberId); + + // then + then(memberRepository).should().delete(member); } @Test - @DisplayName("존재하지 않는 회원의 정보를 수정하려고 하면 예외가 발생한다.") + @DisplayName("존재하지 않는 회원 조회시 예외가 발생한다.") void should_ThrowException_When_MemberNotFound() { // given Long memberId = 1L; given(memberRepository.findById(memberId)).willReturn(Optional.empty()); // when & then - assertThatExceptionOfType(GlobalException.class) - .isThrownBy(() -> memberService.updateMemberInfo(memberId, null, "newNickname")); + assertThatThrownBy(() -> memberService.getMyInfo(memberId)) + .isInstanceOf(GlobalException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.MEMBER_NOT_FOUND); } } diff --git a/src/test/java/com/potatocake/everymoment/service/NotificationServiceTest.java b/src/test/java/com/potatocake/everymoment/service/NotificationServiceTest.java new file mode 100644 index 0000000..4ad4aa0 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/service/NotificationServiceTest.java @@ -0,0 +1,176 @@ +package com.potatocake.everymoment.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; + +import com.potatocake.everymoment.constant.NotificationType; +import com.potatocake.everymoment.dto.request.FcmNotificationRequest; +import com.potatocake.everymoment.dto.response.NotificationListResponse; +import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.entity.Notification; +import com.potatocake.everymoment.exception.ErrorCode; +import com.potatocake.everymoment.exception.GlobalException; +import com.potatocake.everymoment.repository.MemberRepository; +import com.potatocake.everymoment.repository.NotificationRepository; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class NotificationServiceTest { + + @InjectMocks + private NotificationService notificationService; + + @Mock + private NotificationRepository notificationRepository; + + @Mock + private MemberRepository memberRepository; + + @Mock + private FcmService fcmService; + + @Test + @DisplayName("알림이 성공적으로 생성되고 전송된다.") + void should_CreateAndSendNotification_When_ValidInput() { + // given + Long receiverId = 1L; + Member receiver = Member.builder() + .id(receiverId) + .nickname("receiver") + .build(); + + given(memberRepository.findById(receiverId)).willReturn(Optional.of(receiver)); + given(notificationRepository.save(any(Notification.class))).willAnswer(invocation -> { + Notification notification = invocation.getArgument(0); + return Notification.builder() + .id(1L) + .member(notification.getMember()) + .content(notification.getContent()) + .type(notification.getType()) + .targetId(notification.getTargetId()) + .build(); + }); + + // when + notificationService.createAndSendNotification( + receiverId, + NotificationType.COMMENT, + 1L, + "testUser" + ); + + // then + then(notificationRepository).should().save(any(Notification.class)); + then(fcmService).should().sendNotification(eq(receiverId), any(FcmNotificationRequest.class)); + } + + @Test + @DisplayName("알림 목록이 성공적으로 조회된다.") + void should_GetNotifications_When_ValidMemberId() { + // given + Long memberId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + + List notifications = List.of( + Notification.builder() + .id(1L) + .member(member) + .content("Notification 1") + .type("TEST1") + .targetId(1L) + .isRead(false) + .build(), + Notification.builder() + .id(2L) + .member(member) + .content("Notification 2") + .type("TEST2") + .targetId(2L) + .isRead(true) + .build() + ); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(notificationRepository.findAllByMemberId(memberId)).willReturn(notifications); + + // when + List responses = notificationService.getNotifications(memberId); + + // then + assertThat(responses).hasSize(2); + assertThat(responses).extracting("content") + .containsExactly("Notification 1", "Notification 2"); + assertThat(responses).extracting("isRead") + .containsExactly(false, true); + } + + @Test + @DisplayName("알림이 성공적으로 읽음 처리된다.") + void should_UpdateNotification_When_ValidInput() { + // given + Long memberId = 1L; + Long notificationId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + Notification notification = Notification.builder() + .id(notificationId) + .member(member) + .content("Test notification") + .isRead(false) + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(notificationRepository.findById(notificationId)) + .willReturn(Optional.of(notification)); + + // when + notificationService.updateNotification(memberId, notificationId); + + // then + assertThat(notification.isRead()).isTrue(); + } + + @Test + @DisplayName("다른 사용자의 알림을 읽음 처리하려고 하면 예외가 발생한다.") + void should_ThrowException_When_UpdateOtherUserNotification() { + // given + Long memberId = 1L; + Long notificationId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + Member otherMember = Member.builder() + .id(2L) + .build(); + Notification notification = Notification.builder() + .id(notificationId) + .member(otherMember) // 다른 사용자의 알림 + .content("Test notification") + .isRead(false) + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(notificationRepository.findById(notificationId)) + .willReturn(Optional.of(notification)); + + // when & then + assertThatThrownBy(() -> notificationService.updateNotification(memberId, notificationId)) + .isInstanceOf(GlobalException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.NOTIFICATION_NOT_FOUND); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/util/IdExtractorTest.java b/src/test/java/com/potatocake/everymoment/util/IdExtractorTest.java new file mode 100644 index 0000000..01b98c2 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/util/IdExtractorTest.java @@ -0,0 +1,28 @@ +package com.potatocake.everymoment.util; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.potatocake.everymoment.entity.Member; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class IdExtractorTest { + + @Test + @DisplayName("IdExtractor 가 성공적으로 ID를 추출한다.") + void should_ExtractId_When_ValidInput() { + // given + Member member = Member.builder() + .id(1L) + .build(); + + IdExtractor extractor = Member::getId; + + // when + Long extractedId = extractor.extractId(member); + + // then + assertThat(extractedId).isEqualTo(1L); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/util/JwtUtilTest.java b/src/test/java/com/potatocake/everymoment/util/JwtUtilTest.java new file mode 100644 index 0000000..29fbe19 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/util/JwtUtilTest.java @@ -0,0 +1,127 @@ +package com.potatocake.everymoment.util; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.util.ReflectionTestUtils; + +class JwtUtilTest { + + private JwtUtil jwtUtil; + private static final String TEST_SECRET = "dGVzdF9zZWNyZXRfdGVzdF9zZWNyZXRfdGVzdF9zZWNyZXRfdGVzdF9zZWNyZXRfdGVzdF9zZWNyZXQ="; + + @BeforeEach + void setUp() { + jwtUtil = new JwtUtil(); + ReflectionTestUtils.setField(jwtUtil, "secret", TEST_SECRET); + jwtUtil.init(); + } + + @Test + @DisplayName("토큰이 성공적으로 생성된다.") + void should_CreateToken_When_ValidInput() { + // given + Long id = 1L; + + // when + String token = jwtUtil.create(id); + + // then + assertThat(token).isNotEmpty(); + assertThatCode(() -> jwtUtil.getId(token)) + .doesNotThrowAnyException(); + assertThat(jwtUtil.getId(token)).isEqualTo(id); + } + + @Test + @DisplayName("유효한 토큰에서 ID가 성공적으로 추출된다.") + void should_ExtractId_When_ValidToken() { + // given + Long expectedId = 1L; + String token = jwtUtil.create(expectedId); + + // when + Long extractedId = jwtUtil.getId(token); + + // then + assertThat(extractedId).isEqualTo(expectedId); + } + + @Test + @DisplayName("만료되지 않은 토큰은 유효하다고 판단된다.") + void should_ReturnFalse_When_TokenNotExpired() { + // given + String token = jwtUtil.create(1L); + + // when + boolean isExpired = jwtUtil.isExpired(token); + + // then + assertThat(isExpired).isFalse(); + } + + @Test + @DisplayName("잘못된 형식의 토큰은 만료되었다고 판단된다.") + void should_ReturnTrue_When_InvalidToken() { + // given + String invalidToken = "invalid.token.format"; + + // when + boolean isExpired = jwtUtil.isExpired(invalidToken); + + // then + assertThat(isExpired).isTrue(); + } + + @Test + @DisplayName("Authorization 헤더에서 토큰이 성공적으로 추출된다.") + void should_ResolveToken_When_ValidAuthorizationHeader() { + // given + String token = "valid-token"; + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader(HttpHeaders.AUTHORIZATION, jwtUtil.PREFIX + token); + + // when + Optional resolvedToken = jwtUtil.resolveToken(request); + + // then + assertThat(resolvedToken) + .isPresent() + .contains(token); + } + + @Test + @DisplayName("Authorization 헤더가 없으면 빈 Optional 이 반환된다.") + void should_ReturnEmpty_When_NoAuthorizationHeader() { + // given + HttpServletRequest request = new MockHttpServletRequest(); + + // when + Optional resolvedToken = jwtUtil.resolveToken(request); + + // then + assertThat(resolvedToken).isEmpty(); + } + + @Test + @DisplayName("Bearer 접두사가 없는 Authorization 헤더는 빈 Optional 을 반환한다.") + void should_ReturnEmpty_When_NoBearerPrefix() { + // given + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader(HttpHeaders.AUTHORIZATION, "invalid-format-token"); + + // when + Optional resolvedToken = jwtUtil.resolveToken(request); + + // then + assertThat(resolvedToken).isEmpty(); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/util/PagingUtilTest.java b/src/test/java/com/potatocake/everymoment/util/PagingUtilTest.java index 4808d11..f22effa 100644 --- a/src/test/java/com/potatocake/everymoment/util/PagingUtilTest.java +++ b/src/test/java/com/potatocake/everymoment/util/PagingUtilTest.java @@ -1,16 +1,15 @@ package com.potatocake.everymoment.util; import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.data.domain.Sort.Direction.ASC; -import com.potatocake.everymoment.entity.Member; import java.util.List; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Window; class PagingUtilTest { @@ -24,52 +23,121 @@ void setUp() { @Test @DisplayName("스크롤 위치가 성공적으로 생성된다.") - void should_CreateScrollPosition_When_KeyIsNull() { + void should_CreateScrollPosition_When_GivenKey() { + // given + Long key = 1L; + + // when + ScrollPosition position = pagingUtil.createScrollPosition(key); + + // then + assertThat(position).isNotNull(); + } + + @Test + @DisplayName("key 가 null 일 때 offset 스크롤 위치가 생성된다.") + void should_CreateOffsetPosition_When_KeyIsNull() { + // given + Long key = null; + // when - ScrollPosition position = pagingUtil.createScrollPosition(null); + ScrollPosition position = pagingUtil.createScrollPosition(key); // then - Assertions.assertThat(position).isNotNull(); + assertThat(position).isEqualTo(ScrollPosition.offset()); } @Test @DisplayName("페이지 정보가 성공적으로 생성된다.") - void should_CreatePageable_When_ValidSizeProvided() { + void should_CreatePageable_When_ValidInput() { + // given + int size = 10; + Direction direction = Direction.DESC; + // when - Pageable pageable = pagingUtil.createPageable(10, ASC); + Pageable pageable = pagingUtil.createPageable(size, direction); // then - assertThat(pageable).isNotNull(); - assertThat(pageable.getPageSize()).isEqualTo(10); + assertThat(pageable.getPageSize()).isEqualTo(size); + assertThat(pageable.getSort().getOrderFor("id").getDirection()).isEqualTo(direction); } @Test - @DisplayName("다음 페이지 키가 성공적으로 반환된다.") - void should_ReturnNextKey_When_WindowHasNext() { + @DisplayName("페이지 정보가 올바른 정렬 순서를 가진다.") + void should_CreatePageableWithCorrectSort_When_DirectionGiven() { // given - List members = List.of(Member.builder().id(1L).build()); - Window window = Window.from(members, ScrollPosition::offset, true); + int size = 10; + Direction direction = Direction.ASC; // when - Long nextKey = pagingUtil.getNextKey(window, Member::getId); + Pageable pageable = pagingUtil.createPageable(size, direction); // then - assertThat(nextKey).isNotNull(); - assertThat(nextKey).isEqualTo(1L); + Sort sort = pageable.getSort(); + assertThat(sort.getOrderFor("id")).isNotNull(); + assertThat(sort.getOrderFor("id").getDirection()).isEqualTo(direction); } @Test - @DisplayName("다음 페이지가 존재하지 않을 때, 키 값으로 null 을 반환한다.") - void should_ReturnNull_When_WindowHasNoNext() { + @DisplayName("다음 키가 성공적으로 생성된다.") + void should_GetNextKey_When_ValidWindow() { // given - List members = List.of(Member.builder().id(1L).build()); - Window window = Window.from(members, ScrollPosition::offset, false); + TestEntity entity1 = new TestEntity(1L); + TestEntity entity2 = new TestEntity(2L); + + List content = List.of(entity1, entity2); + ScrollPosition scrollPosition = ScrollPosition.offset(); + + Window window = Window.from(content, i -> scrollPosition, true); // when - Long nextKey = pagingUtil.getNextKey(window, Member::getId); + Long nextKey = pagingUtil.getNextKey(window, TestEntity::getId); + + // then + assertThat(nextKey).isEqualTo(2L); + } + + @Test + @DisplayName("다음 페이지가 없으면 null 을 반환한다.") + void should_ReturnNull_When_NoNextPage() { + // given + TestEntity entity = new TestEntity(1L); + ScrollPosition scrollPosition = ScrollPosition.offset(); + + Window window = Window.from(List.of(entity), i -> scrollPosition, false); + + // when + Long nextKey = pagingUtil.getNextKey(window, TestEntity::getId); + + // then + assertThat(nextKey).isNull(); + } + + @Test + @DisplayName("빈 윈도우에 대해 null 을 반환한다.") + void should_ReturnNull_When_EmptyWindow() { + // given + ScrollPosition scrollPosition = ScrollPosition.offset(); + + Window window = Window.from(List.of(), i -> scrollPosition, false); + + // when + Long nextKey = pagingUtil.getNextKey(window, TestEntity::getId); // then assertThat(nextKey).isNull(); } + private static class TestEntity { + private final Long id; + + TestEntity(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + } + } diff --git a/src/test/java/com/potatocake/everymoment/util/S3FileUploaderTest.java b/src/test/java/com/potatocake/everymoment/util/S3FileUploaderTest.java new file mode 100644 index 0000000..b748018 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/util/S3FileUploaderTest.java @@ -0,0 +1,104 @@ +package com.potatocake.everymoment.util; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.potatocake.everymoment.config.AwsS3Properties; +import com.potatocake.everymoment.exception.ErrorCode; +import com.potatocake.everymoment.exception.GlobalException; +import java.io.IOException; +import java.net.URL; +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.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.util.ReflectionTestUtils; + +@ExtendWith(MockitoExtension.class) +class S3FileUploaderTest { + + private S3FileUploader uploader; + + @Mock + private AmazonS3 amazonS3; + + @Mock + private AwsS3Properties properties; + + @BeforeEach + void setUp() { + uploader = new S3FileUploader(properties); + ReflectionTestUtils.setField(uploader, "amazonS3", amazonS3); + } + + @Test + @DisplayName("파일이 성공적으로 업로드된다.") + void should_UploadFile_When_ValidInput() throws IOException { + // given + MockMultipartFile file = new MockMultipartFile( + "file", + "test.jpg", + MediaType.IMAGE_JPEG_VALUE, + "test image".getBytes() + ); + + given(properties.bucket()).willReturn("test-bucket"); + given(amazonS3.getUrl(any(), any())).willReturn(new URL("https://example.com/test.jpg")); + + // when + String url = uploader.uploadFile(file); + + // then + assertThat(url).isEqualTo("https://example.com/test.jpg"); + then(amazonS3).should().putObject(any(PutObjectRequest.class)); + } + + @Test + @DisplayName("지원하지 않는 파일 형식이면 예외가 발생한다.") + void should_ThrowException_When_UnsupportedFileType() { + // given + MockMultipartFile file = new MockMultipartFile( + "file", + "test.txt", + MediaType.TEXT_PLAIN_VALUE, + "test content".getBytes() + ); + + // when & then + assertThatThrownBy(() -> uploader.uploadFile(file)) + .isInstanceOf(GlobalException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.INVALID_FILE_TYPE); + + then(amazonS3).shouldHaveNoInteractions(); + } + + @Test + @DisplayName("파일 업로드 실패시 예외가 발생한다.") + void should_ThrowException_When_UploadFails() { + // given + MockMultipartFile file = new MockMultipartFile( + "file", + "test.jpg", + MediaType.IMAGE_JPEG_VALUE, + "test image".getBytes() + ); + + given(properties.bucket()).willReturn("test-bucket"); + given(amazonS3.putObject(any(PutObjectRequest.class))) + .willThrow(new RuntimeException("Upload failed")); + + // when & then + assertThatThrownBy(() -> uploader.uploadFile(file)) + .isInstanceOf(RuntimeException.class); + } + +}