From 06f02036f777bd469a43176beceac6521e12443e Mon Sep 17 00:00:00 2001 From: Jay Park Date: Wed, 29 Oct 2025 17:05:29 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[feat]=20Comment,=20User=20service=20test?= =?UTF-8?q?=20=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 --- build.gradle | 10 +- .../U2E/domain/user/service/JwtUtil.java | 30 +++-- .../auth/interceptor/AuthInterceptor.java | 4 +- .../comment/service/CommentServiceTest.java | 103 ++++++++++++++++++ .../U2E/domain/user/service/JwtUtilTest.java | 48 ++++++++ src/test/resources/application-test.yml | 6 +- 6 files changed, 183 insertions(+), 18 deletions(-) create mode 100644 src/test/java/Konkuk/U2E/domain/comment/service/CommentServiceTest.java create mode 100644 src/test/java/Konkuk/U2E/domain/user/service/JwtUtilTest.java diff --git a/build.gradle b/build.gradle index 6255c92..e83a088 100644 --- a/build.gradle +++ b/build.gradle @@ -52,9 +52,9 @@ dependencies { testRuntimeOnly 'org.junit.platform:junit-platform-launcher' // JWT - implementation 'io.jsonwebtoken:jjwt-api:0.11.5' - implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' - implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + implementation 'io.jsonwebtoken:jjwt-api:0.12.3' + implementation 'io.jsonwebtoken:jjwt-impl:0.12.3' + implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3' testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' testImplementation 'io.rest-assured:spring-mock-mvc:5.4.0' @@ -71,6 +71,10 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' } +test { + useJUnitPlatform() +} + jacoco { toolVersion = "0.8.12" } diff --git a/src/main/java/Konkuk/U2E/domain/user/service/JwtUtil.java b/src/main/java/Konkuk/U2E/domain/user/service/JwtUtil.java index 8bd83c4..9ffb7cb 100644 --- a/src/main/java/Konkuk/U2E/domain/user/service/JwtUtil.java +++ b/src/main/java/Konkuk/U2E/domain/user/service/JwtUtil.java @@ -1,12 +1,13 @@ package Konkuk.U2E.domain.user.service; import Konkuk.U2E.domain.user.exception.InvalidAccessTokenException; -import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; import java.util.Date; import static Konkuk.U2E.global.response.status.BaseExceptionResponseStatus.INVALID_ACCESS_TOKEN; @@ -14,27 +15,32 @@ @Component public class JwtUtil { - @Value("${jwt.secret}") - private String secretKey; + private final SecretKey secretKey; @Value("${jwt.expiration}") private long expirationMs; + public JwtUtil(@Value("${jwt.secret}") String secret) { + secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm()); + } + public String generateAccessToken(String username) { return Jwts.builder() - .setSubject(username) - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() + expirationMs)) - .signWith(SignatureAlgorithm.HS256, secretKey) + .claim("username", username) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + expirationMs)) + .signWith(secretKey) .compact(); } - public Claims validateAccessToken(String token) { + public String validateAndGetName(String token) { try { return Jwts.parser() - .setSigningKey(secretKey) - .parseClaimsJws(token) - .getBody(); + .verifyWith(secretKey) + .build() + .parseSignedClaims(token) + .getPayload() + .get("username", String.class); } catch (Exception e) { throw new InvalidAccessTokenException(INVALID_ACCESS_TOKEN); } diff --git a/src/main/java/Konkuk/U2E/global/auth/interceptor/AuthInterceptor.java b/src/main/java/Konkuk/U2E/global/auth/interceptor/AuthInterceptor.java index e859bb0..5478ac4 100644 --- a/src/main/java/Konkuk/U2E/global/auth/interceptor/AuthInterceptor.java +++ b/src/main/java/Konkuk/U2E/global/auth/interceptor/AuthInterceptor.java @@ -40,8 +40,8 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons } if (token != null) { - Claims claims = jwtUtil.validateAccessToken(token); - request.setAttribute("username", claims.getSubject()); + String userName = jwtUtil.validateAndGetName(token); + request.setAttribute("username", userName); log.info("사용자 {} 인증", request.getAttribute("username")); return true; } diff --git a/src/test/java/Konkuk/U2E/domain/comment/service/CommentServiceTest.java b/src/test/java/Konkuk/U2E/domain/comment/service/CommentServiceTest.java new file mode 100644 index 0000000..1e65e66 --- /dev/null +++ b/src/test/java/Konkuk/U2E/domain/comment/service/CommentServiceTest.java @@ -0,0 +1,103 @@ +package Konkuk.U2E.domain.comment.service; + +import Konkuk.U2E.domain.comment.domain.Comment; +import Konkuk.U2E.domain.comment.dto.request.PostCommentCreateRequest; +import Konkuk.U2E.domain.comment.dto.response.GetCommentsResponse; +import Konkuk.U2E.domain.comment.exception.CommentOfNewsNotFoundException; +import Konkuk.U2E.domain.comment.repository.CommentRepository; +import Konkuk.U2E.domain.news.domain.News; +import Konkuk.U2E.domain.news.repository.NewsRepository; +import Konkuk.U2E.domain.user.domain.User; +import Konkuk.U2E.domain.user.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +@DisplayName("CommentService 단위 테스트") +class CommentServiceTest { + + CommentRepository commentRepository = mock(CommentRepository.class); + NewsRepository newsRepository = mock(NewsRepository.class); + UserRepository userRepository = mock(UserRepository.class); + + CommentService sut; + + @BeforeEach + void setUp() { + sut = new CommentService(commentRepository, newsRepository, userRepository); + } + + private News sampleNews() { + return News.builder() + .newsUrl("https://x") + .imageUrl(null) + .newsTitle("title") + .newsBody("body") + .newsDate(LocalDate.now()) + .climateList(List.of()) + .build(); + } + + @Test + @DisplayName("getCommentInfo - 뉴스가 없으면 예외") + void getCommentInfo_whenNewsMissing_thenThrow() { + when(newsRepository.findById(1L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> sut.getCommentInfo(1L)) + .isInstanceOf(CommentOfNewsNotFoundException.class); + } + + @Test + @DisplayName("getCommentInfo - 정상 조회") + void getCommentInfo_whenExists_thenOk() { + News news = sampleNews(); + when(newsRepository.findById(1L)).thenReturn(Optional.of(news)); + + Comment c = Comment.builder() + .contents("hello") + .news(news) + .user(User.builder().name("kim").password("p").build()) + .build(); + + when(commentRepository.findCommentsByNewsId(1L)).thenReturn(List.of(c)); + + GetCommentsResponse res = sut.getCommentInfo(1L); + + assertThat(res.commentList()).hasSize(1); + assertThat(res.commentList().get(0).contents()).isEqualTo("hello"); + } + + @Test + @DisplayName("createComment - 뉴스가 없으면 예외") + void createComment_whenNewsMissing_thenThrow() { + when(newsRepository.findById(9L)).thenReturn(Optional.empty()); + when(userRepository.findUserByName("kim")) + .thenReturn(User.builder().name("kim").password("p").build()); + + PostCommentCreateRequest req = new PostCommentCreateRequest(9L, "hi"); + + assertThatThrownBy(() -> sut.createComment("kim", req)) + .isInstanceOf(CommentOfNewsNotFoundException.class); + } + + @Test + @DisplayName("createComment - 정상 저장") + void createComment_whenValid_thenSave() { + User user = User.builder().name("kim").password("p").build(); + News news = sampleNews(); + when(userRepository.findUserByName("kim")).thenReturn(user); + when(newsRepository.findById(7L)).thenReturn(Optional.of(news)); + + PostCommentCreateRequest req = new PostCommentCreateRequest(7L, "hi"); + sut.createComment("kim", req); + + verify(commentRepository, times(1)).save(any(Comment.class)); + } +} diff --git a/src/test/java/Konkuk/U2E/domain/user/service/JwtUtilTest.java b/src/test/java/Konkuk/U2E/domain/user/service/JwtUtilTest.java new file mode 100644 index 0000000..c25fb89 --- /dev/null +++ b/src/test/java/Konkuk/U2E/domain/user/service/JwtUtilTest.java @@ -0,0 +1,48 @@ +package Konkuk.U2E.domain.user.service; + +import Konkuk.U2E.domain.user.exception.InvalidAccessTokenException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.util.ReflectionTestUtils; + +import static Konkuk.U2E.global.response.status.BaseExceptionResponseStatus.INVALID_ACCESS_TOKEN; +import static org.assertj.core.api.Assertions.*; + +@ActiveProfiles("test") +@DisplayName("JwtUtil 단위 테스트") +class JwtUtilTest { + + private JwtUtil jwtUtil = new JwtUtil("abcdefghakjfnjklafljkaklfjaslkdfjlkjkljklafjkkljflkaflksjklijklmnopqrstuvwxyz"); + + @Test + @DisplayName("토큰 생성/검증 - OK (username 클레임 반환)") + void generate_and_validate_ok() { + // when + String token = jwtUtil.generateAccessToken("tester"); + + // then + String name = jwtUtil.validateAndGetName(token); + assertThat(name).isEqualTo("tester"); + } + + @Test + @DisplayName("잘못된 토큰 - INVALID_ACCESS_TOKEN 예외") + void validate_invalidToken_thenThrow() { + assertThatThrownBy(() -> jwtUtil.validateAndGetName("not.jwt")) + .isInstanceOf(InvalidAccessTokenException.class) + .hasMessageContaining(INVALID_ACCESS_TOKEN.getMessage()); + } + + @Test + @DisplayName("만료된 토큰 - INVALID_ACCESS_TOKEN 예외") + void validate_expiredToken_thenThrow() { + // 만료 시간을 과거로 세팅해서 즉시 만료 토큰 생성 + ReflectionTestUtils.setField(jwtUtil, "expirationMs", -1L); + String token = jwtUtil.generateAccessToken("tester"); + + assertThatThrownBy(() -> jwtUtil.validateAndGetName(token)) + .isInstanceOf(InvalidAccessTokenException.class) + .hasMessageContaining(INVALID_ACCESS_TOKEN.getMessage()); + } +} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index dfb0959..3f5f35b 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -3,7 +3,7 @@ spring: name: U2E jwt: - secret: abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz + secret: abcdefghakjfnjklafljkaklfjaslkdfjlkjkljklafjkkljflkaflksjklijklmnopqrstuvwxyz expiration: 3600000 --- spring: @@ -28,3 +28,7 @@ spring: dialect: org.hibernate.dialect.H2Dialect +gemini: + api-key: test-secret-key-1234567890-abcdefghijklmnopqrstuvwxyz-ABCDEFGH + model: gemini-2.5-flash + endpoint: https://generativelanguage.googleapis.com/v1beta \ No newline at end of file From cbce4de3b052f4a8925ed7abffe580f5734e3f09 Mon Sep 17 00:00:00 2001 From: Jay Park Date: Wed, 29 Oct 2025 17:35:15 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[feat]=20Comment=20Controller=20=EB=B0=8F?= =?UTF-8?q?=20JwtUtil=20Test=20=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 --- .../controller/CommentControllerTest.java | 95 +++++++++++++++++++ .../U2E/domain/user/service/JwtUtilTest.java | 4 +- 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 src/test/java/Konkuk/U2E/domain/comment/controller/CommentControllerTest.java diff --git a/src/test/java/Konkuk/U2E/domain/comment/controller/CommentControllerTest.java b/src/test/java/Konkuk/U2E/domain/comment/controller/CommentControllerTest.java new file mode 100644 index 0000000..a0f50b9 --- /dev/null +++ b/src/test/java/Konkuk/U2E/domain/comment/controller/CommentControllerTest.java @@ -0,0 +1,95 @@ +package Konkuk.U2E.domain.comment.controller; + +import Konkuk.U2E.domain.comment.dto.request.PostCommentCreateRequest; +import Konkuk.U2E.domain.comment.dto.response.GetCommentsResponse; +import Konkuk.U2E.domain.comment.service.CommentService; +import Konkuk.U2E.domain.user.service.JwtUtil; +import Konkuk.U2E.global.auth.resolver.LoginUserArgumentResolver; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.List; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@ActiveProfiles("test") +@SpringBootTest +@AutoConfigureMockMvc +class CommentControllerTest { + + @Autowired MockMvc mockMvc; + @Autowired ObjectMapper om; + + @MockitoBean CommentService commentService; + + @MockitoBean LoginUserArgumentResolver loginUserArgumentResolver; + + @BeforeEach + void setUpResolver() throws Exception { + when(loginUserArgumentResolver.supportsParameter(any())).thenReturn(true); + when(loginUserArgumentResolver.resolveArgument(any(), any(), any(), any())) + .thenReturn("test-user"); + } + + @MockitoBean + JwtUtil jwtUtil; + + @BeforeEach + void setUp() { + when(jwtUtil.validateAndGetName("dummy-token")).thenReturn("test-user"); + } + + @Nested + @DisplayName("GET /comments/{newsId}") + class GetCommentsApi { + + @Test + @DisplayName("댓글 목록 조회 – OK (BaseResponse.data.commentList)") + void getComments_ok() throws Exception { + // given + when(commentService.getCommentInfo(1L)) + .thenReturn(new GetCommentsResponse(List.of())); + + // when & then + mockMvc.perform(get("/comments/{newsId}", 1L) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.data.commentList").isArray()); + } + } + + @Nested + @DisplayName("POST /comments") + class PostCommentsApi { + + @Test + @DisplayName("댓글 작성 – OK (LoginUser가 'test-user'로 주입)") + void postComments_ok() throws Exception { + // given + PostCommentCreateRequest req = new PostCommentCreateRequest(5L, "hi"); + + // when & then + mockMvc.perform(post("/comments") + .header("Authorization", "Bearer dummy-token") + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsString(req))) + .andExpect(status().isOk()); + + verify(commentService).createComment(eq("test-user"), any(PostCommentCreateRequest.class)); + } + } +} \ No newline at end of file diff --git a/src/test/java/Konkuk/U2E/domain/user/service/JwtUtilTest.java b/src/test/java/Konkuk/U2E/domain/user/service/JwtUtilTest.java index c25fb89..351d869 100644 --- a/src/test/java/Konkuk/U2E/domain/user/service/JwtUtilTest.java +++ b/src/test/java/Konkuk/U2E/domain/user/service/JwtUtilTest.java @@ -13,11 +13,13 @@ @DisplayName("JwtUtil 단위 테스트") class JwtUtilTest { - private JwtUtil jwtUtil = new JwtUtil("abcdefghakjfnjklafljkaklfjaslkdfjlkjkljklafjkkljflkaflksjklijklmnopqrstuvwxyz"); + private final JwtUtil jwtUtil = new JwtUtil("abcdefghakjfnjklafljkaklfjaslkdfjlkjkljklafjkkljflkaflksjklijklmnopqrstuvwxyz"); @Test @DisplayName("토큰 생성/검증 - OK (username 클레임 반환)") void generate_and_validate_ok() { + ReflectionTestUtils.setField(jwtUtil, "expirationMs", 3_600_000L); // 1시간 + // when String token = jwtUtil.generateAccessToken("tester");