From 352ceb7b298a244a32babc2c3bd1d091b07889e0 Mon Sep 17 00:00:00 2001 From: JunHyeongChoi Date: Thu, 7 Nov 2024 14:41:10 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20JWT=20=EC=9D=B8=EC=A6=9D=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/master_weekly_cicd.yml | 1 + .github/workflows/pr_weekly_ci.yml | 1 + .../com/potatocake/everymoment/util/JwtUtil.java | 15 ++++++++++++++- src/main/resources/application-prod.yml | 3 +++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/master_weekly_cicd.yml b/.github/workflows/master_weekly_cicd.yml index e80fa57..c0e3672 100644 --- a/.github/workflows/master_weekly_cicd.yml +++ b/.github/workflows/master_weekly_cicd.yml @@ -45,6 +45,7 @@ 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: | diff --git a/.github/workflows/pr_weekly_ci.yml b/.github/workflows/pr_weekly_ci.yml index 78b2a68..f54701f 100644 --- a/.github/workflows/pr_weekly_ci.yml +++ b/.github/workflows/pr_weekly_ci.yml @@ -48,6 +48,7 @@ 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: | 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 9baf0ef..0c6cad9 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -29,3 +29,6 @@ aws: bucket: ${AWS_S3_BUCKET} accessKey: ${AWS_S3_ACCESS_KEY} secretKey: ${AWS_S3_SECRET_KEY} + +jwt: + secret: ${JWT_SECRET} From 8191df6c54e873c4d863af5ed9094d26498c3ea6 Mon Sep 17 00:00:00 2001 From: JunHyeongChoi Date: Thu, 7 Nov 2024 14:58:24 +0900 Subject: [PATCH 2/3] =?UTF-8?q?test:=20Notification=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NotificationControllerTest.java | 115 ++++++++++++ .../everymoment/entity/NotificationTest.java | 51 +++++ .../NotificationRepositoryTest.java | 120 ++++++++++++ .../service/NotificationServiceTest.java | 176 ++++++++++++++++++ 4 files changed, 462 insertions(+) create mode 100644 src/test/java/com/potatocake/everymoment/controller/NotificationControllerTest.java create mode 100644 src/test/java/com/potatocake/everymoment/entity/NotificationTest.java create mode 100644 src/test/java/com/potatocake/everymoment/repository/NotificationRepositoryTest.java create mode 100644 src/test/java/com/potatocake/everymoment/service/NotificationServiceTest.java 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/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/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/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); + } + +} From 69f9291380cd0322afa788991ef19699e98bc278 Mon Sep 17 00:00:00 2001 From: JunHyeongChoi Date: Thu, 7 Nov 2024 15:21:01 +0900 Subject: [PATCH 3/3] =?UTF-8?q?test:=20=EB=AA=A8=EB=93=A0=20util=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../everymoment/util/IdExtractorTest.java | 28 ++++ .../everymoment/util/JwtUtilTest.java | 127 ++++++++++++++++++ .../everymoment/util/PagingUtilTest.java | 112 ++++++++++++--- .../everymoment/util/S3FileUploaderTest.java | 104 ++++++++++++++ 4 files changed, 349 insertions(+), 22 deletions(-) create mode 100644 src/test/java/com/potatocake/everymoment/util/IdExtractorTest.java create mode 100644 src/test/java/com/potatocake/everymoment/util/JwtUtilTest.java create mode 100644 src/test/java/com/potatocake/everymoment/util/S3FileUploaderTest.java 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); + } + +}