From 241c3278272228374a47ed0d9b573a62a9b31a2c Mon Sep 17 00:00:00 2001 From: ChoiDongKuen Date: Mon, 15 Jan 2024 10:04:48 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=95=84=EC=9B=83=EC=9D=84=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.=20(#80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Test Container 의존성 추가 (#73) * feat: RedisTestContainerConfig 구현 (#73) * feat: IntegrationTest 적용 (#73) * test: 회원 탈퇴 API 통합 테스트 진행 (with redis) (#73) * fix: SonarCloud 에러 수정 (#73) * fix: SonarCloud 에러 수정 (#73) * feat: 통합 테스트 RedisService 의존성 추가 및 Repository Redis 관련 메소드 추가 (#73) * refactor: SecurityService clearSecurityContext 메소드 추가 (#73) * feat: 회원 로그아웃 API 구현 (#52) * test: 회원 로그아웃 통합 테스트 구현 (#52) * refactor: UserService 코드 리팩토링 (코드 리뷰 반영) (#52) --- gradle/test.gradle | 5 ++ .../security/service/SecurityService.java | 9 ++- .../user/controller/UserController.java | 5 ++ .../teumteum/user/service/UserService.java | 8 +++ .../core/config/RedisTestContainerConfig.java | 25 +++++++ .../java/net/teumteum/integration/Api.java | 8 +++ .../teumteum/integration/IntegrationTest.java | 3 + .../net/teumteum/integration/Repository.java | 66 +++++++++++-------- .../integration/UserIntegrationTest.java | 58 +++++++++++++++- 9 files changed, 155 insertions(+), 32 deletions(-) create mode 100644 src/test/java/net/teumteum/core/config/RedisTestContainerConfig.java diff --git a/gradle/test.gradle b/gradle/test.gradle index ab0d1cf9..f7dbd28a 100644 --- a/gradle/test.gradle +++ b/gradle/test.gradle @@ -6,6 +6,11 @@ allprojects { } dependencies { + // test container for redis + testImplementation "org.junit.jupiter:junit-jupiter:5.8.1" + testImplementation "org.testcontainers:testcontainers:1.17.6" + testImplementation "org.testcontainers:junit-jupiter:1.17.6" + testImplementation "org.junit.jupiter:junit-jupiter-api:${junitVersion}" testImplementation "org.junit.jupiter:junit-jupiter-params:${junitVersion}" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junitVersion}" diff --git a/src/main/java/net/teumteum/core/security/service/SecurityService.java b/src/main/java/net/teumteum/core/security/service/SecurityService.java index a8375c6d..793ad966 100644 --- a/src/main/java/net/teumteum/core/security/service/SecurityService.java +++ b/src/main/java/net/teumteum/core/security/service/SecurityService.java @@ -16,11 +16,6 @@ public static void clearSecurityContext() { SecurityContextHolder.clearContext(); } - private UserAuthentication getUserAuthentication() { - return (UserAuthentication) SecurityContextHolder.getContext().getAuthentication(); - } - - public Long getCurrentUserId() { return getUserAuthentication() == null ? userConnector.findAllUser().get(0).getId() : getUserAuthentication().getId(); @@ -36,4 +31,8 @@ public void setUserId(Long userId) { UserAuthentication userAuthentication = getUserAuthentication(); userAuthentication.setUserId(userId); } + + private UserAuthentication getUserAuthentication() { + return (UserAuthentication) SecurityContextHolder.getContext().getAuthentication(); + } } diff --git a/src/main/java/net/teumteum/user/controller/UserController.java b/src/main/java/net/teumteum/user/controller/UserController.java index 909b9b45..87474518 100644 --- a/src/main/java/net/teumteum/user/controller/UserController.java +++ b/src/main/java/net/teumteum/user/controller/UserController.java @@ -90,6 +90,11 @@ public UserRegisterResponse register(@RequestBody UserRegisterRequest request) { return userService.register(request); } + @PostMapping("/logouts") + @ResponseStatus(HttpStatus.OK) + public void logout() { + userService.logout(getCurrentUserId()); + } @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(IllegalArgumentException.class) diff --git a/src/main/java/net/teumteum/user/service/UserService.java b/src/main/java/net/teumteum/user/service/UserService.java index 7a9ecf99..55ccc3ed 100644 --- a/src/main/java/net/teumteum/user/service/UserService.java +++ b/src/main/java/net/teumteum/user/service/UserService.java @@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor; import net.teumteum.core.security.Authenticated; import net.teumteum.core.security.service.RedisService; +import net.teumteum.core.security.service.SecurityService; import net.teumteum.user.domain.BalanceGameType; import net.teumteum.user.domain.InterestQuestion; import net.teumteum.user.domain.User; @@ -77,6 +78,13 @@ public UserRegisterResponse register(UserRegisterRequest request) { return new UserRegisterResponse(userRepository.save(request.toUser()).getId()); } + @Transactional + public void logout(Long userId) { + getUser(userId); + redisService.deleteData(String.valueOf(userId)); + SecurityService.clearSecurityContext(); + } + public FriendsResponse findFriendsByUserId(Long userId) { var user = getUser(userId); diff --git a/src/test/java/net/teumteum/core/config/RedisTestContainerConfig.java b/src/test/java/net/teumteum/core/config/RedisTestContainerConfig.java new file mode 100644 index 00000000..e3af98ab --- /dev/null +++ b/src/test/java/net/teumteum/core/config/RedisTestContainerConfig.java @@ -0,0 +1,25 @@ +package net.teumteum.core.config; + +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +@Testcontainers +public class RedisTestContainerConfig implements BeforeAllCallback { + + private static final String REDIS_IMAGE = "redis:7.0.8-alpine"; + private static final int REDIS_PORT = 6379; + + private GenericContainer redis; + + @Override + public void beforeAll(ExtensionContext context) throws Exception { + redis = new GenericContainer(DockerImageName.parse(REDIS_IMAGE)).withExposedPorts(REDIS_PORT); + + redis.start(); + System.setProperty("spring.data.redis.host", redis.getHost()); + System.setProperty("spring.data.redis.port", String.valueOf(redis.getMappedPort(REDIS_PORT))); + } +} diff --git a/src/test/java/net/teumteum/integration/Api.java b/src/test/java/net/teumteum/integration/Api.java index 1d4ea32d..d00c4da6 100644 --- a/src/test/java/net/teumteum/integration/Api.java +++ b/src/test/java/net/teumteum/integration/Api.java @@ -143,4 +143,12 @@ ResponseSpec registerUserCard(String accessToken, UserRegisterRequest userRegist .bodyValue(userRegisterRequest) .exchange(); } + + ResponseSpec logoutUser(String accessToken) { + return webTestClient + .post() + .uri("/users/logouts") + .header(HttpHeaders.AUTHORIZATION, accessToken) + .exchange(); + } } diff --git a/src/test/java/net/teumteum/integration/IntegrationTest.java b/src/test/java/net/teumteum/integration/IntegrationTest.java index 39bf0967..8ea33d79 100644 --- a/src/test/java/net/teumteum/integration/IntegrationTest.java +++ b/src/test/java/net/teumteum/integration/IntegrationTest.java @@ -1,9 +1,11 @@ package net.teumteum.integration; import net.teumteum.Application; +import net.teumteum.core.config.RedisTestContainerConfig; import net.teumteum.user.infra.GptTestServer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; import org.springframework.boot.test.context.SpringBootTest; @@ -12,6 +14,7 @@ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @AutoConfigureWebTestClient(timeout = "10000") +@ExtendWith(RedisTestContainerConfig.class) @ContextConfiguration(classes = { Api.class, Repository.class, diff --git a/src/test/java/net/teumteum/integration/Repository.java b/src/test/java/net/teumteum/integration/Repository.java index 8a5ce133..4ba47c7e 100644 --- a/src/test/java/net/teumteum/integration/Repository.java +++ b/src/test/java/net/teumteum/integration/Repository.java @@ -1,9 +1,11 @@ package net.teumteum.integration; -import jakarta.persistence.EntityManager; +import java.util.List; +import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import net.teumteum.core.config.AppConfig; +import net.teumteum.core.security.service.RedisService; import net.teumteum.meeting.domain.Meeting; import net.teumteum.meeting.domain.MeetingFixture; import net.teumteum.meeting.domain.MeetingRepository; @@ -14,20 +16,16 @@ import org.springframework.boot.test.context.TestComponent; import org.springframework.context.annotation.Import; -import java.util.List; -import java.util.stream.Stream; - -import java.util.List; -import java.util.stream.Stream; - @TestComponent @Import(AppConfig.class) @RequiredArgsConstructor class Repository { + private final UserRepository userRepository; private final MeetingRepository meetingRepository; - private final EntityManager entityManager; + + private final RedisService redisService; User saveAndGetUser() { var user = UserFixture.getNullIdUser(); @@ -38,6 +36,10 @@ List getAllUser() { return userRepository.findAll(); } + void clearUserRepository() { + userRepository.deleteAll(); + } + Meeting saveAndGetOpenMeeting() { var meeting = MeetingFixture.getOpenMeeting(); @@ -56,68 +58,80 @@ Meeting saveAndGetOpenFullMeeting() { List saveAndGetOpenMeetingsByTopic(int size, Topic topic) { var meetings = Stream.generate(() -> MeetingFixture.getOpenMeetingWithTopic(topic)) - .limit(size) - .toList(); + .limit(size) + .toList(); return meetingRepository.saveAllAndFlush(meetings); } List saveAndGetCloseMeetingsTopic(int size, Topic topic) { var meetings = Stream.generate(() -> MeetingFixture.getCloseMeetingWithTopic(topic)) - .limit(size) - .toList(); + .limit(size) + .toList(); return meetingRepository.saveAllAndFlush(meetings); } List saveAndGetOpenMeetingsByTitle(int size, String title) { var meetings = Stream.generate(() -> MeetingFixture.getOpenMeetingWithTitle(title)) - .limit(size) - .toList(); + .limit(size) + .toList(); return meetingRepository.saveAllAndFlush(meetings); } List saveAndGetCloseMeetingsByTitle(int size, String title) { var meetings = Stream.generate(() -> MeetingFixture.getCloseMeetingWithTitle(title)) - .limit(size) - .toList(); + .limit(size) + .toList(); return meetingRepository.saveAllAndFlush(meetings); } List saveAndGetOpenMeetingsByIntroduction(int size, String introduction) { var meetings = Stream.generate(() -> MeetingFixture.getOpenMeetingWithIntroduction(introduction)) - .limit(size) - .toList(); + .limit(size) + .toList(); return meetingRepository.saveAllAndFlush(meetings); } List saveAndGetCloseMeetingsByIntroduction(int size, String introduction) { var meetings = Stream.generate(() -> MeetingFixture.getCloseMeetingWithIntroduction(introduction)) - .limit(size) - .toList(); + .limit(size) + .toList(); return meetingRepository.saveAllAndFlush(meetings); } List saveAndGetOpenMeetingsByParticipantUserId(int size, Long participantUserId) { var meetings = Stream.generate(() -> MeetingFixture.getOpenMeetingWithParticipantUserId(participantUserId)) - .limit(size) - .toList(); + .limit(size) + .toList(); return meetingRepository.saveAllAndFlush(meetings); } List saveAndGetCloseMeetingsByParticipantUserId(int size, Long participantUserId) { var meetings = Stream.generate(() -> MeetingFixture.getCloseMeetingWithParticipantUserId(participantUserId)) - .limit(size) - .toList(); + .limit(size) + .toList(); return meetingRepository.saveAllAndFlush(meetings); } List saveAndGetOpenMeetings(int size) { var meetings = Stream.generate(MeetingFixture::getOpenMeeting) - .limit(size) - .toList(); + .limit(size) + .toList(); return meetingRepository.saveAllAndFlush(meetings); } + void saveRedisDataWithExpiration(String key, String value, Long duration) { + redisService.setDataWithExpiration(key, value, duration); + } + + void deleteRedisData(String key) { + redisService.deleteData(key); + } + + void getRedisData(String key) { + redisService.getData(key); + } + void clear() { userRepository.deleteAll(); meetingRepository.deleteAll(); diff --git a/src/test/java/net/teumteum/integration/UserIntegrationTest.java b/src/test/java/net/teumteum/integration/UserIntegrationTest.java index a73c483b..a75f7648 100644 --- a/src/test/java/net/teumteum/integration/UserIntegrationTest.java +++ b/src/test/java/net/teumteum/integration/UserIntegrationTest.java @@ -1,5 +1,7 @@ package net.teumteum.integration; +import static org.assertj.core.api.Assertions.assertThatCode; + import java.util.List; import net.teumteum.core.error.ErrorResponse; import net.teumteum.user.domain.User; @@ -17,6 +19,7 @@ class UserIntegrationTest extends IntegrationTest { private static final String VALID_TOKEN = "VALID_TOKEN"; private static final String INVALID_TOKEN = "IN_VALID_TOKEN"; + private static final Long DURATION = 3600000L; @Nested @DisplayName("유저 조회 API는") @@ -196,9 +199,45 @@ void Return_empty_friends_when_received_empty_friends_user_id() { } } + @Nested + @DisplayName("회원 탈퇴 API는") + class Withdraw_user { + + @Test + @DisplayName("현재 로그인한 회원을 탈퇴 처리한다.") + void Withdraw_user_info_api() { + // given + var me = repository.saveAndGetUser(); + repository.saveRedisDataWithExpiration(String.valueOf(me.getId()), VALID_TOKEN, DURATION); + + loginContext.setUserId(me.getId()); + + // when & then + assertThatCode(() -> api.withdrawUser(VALID_TOKEN)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("해당 회원이 존재하지 않으면, 500 에러를 반환한다.") + void Return_500_error_if_user_not_exist() { + // given + repository.clearUserRepository(); + + // when + var result = api.withdrawUser(VALID_TOKEN); + + // then + Assertions.assertThat(result.expectStatus().is5xxServerError() + .expectBody(ErrorResponse.class) + .returnResult() + .getResponseBody()) + .usingRecursiveComparison().isNull(); + } + } + @Nested @DisplayName("회원 카드 등록 API는") - class Register_user_card { + class Register_user_card_api { @Test @DisplayName("등록할 회원의 정보가 주어지면, 회원 정보를 저장한다.") @@ -237,4 +276,21 @@ void Return_400_badRequest_register_user_card() { .isNotNull(); } } + + @Nested + @DisplayName("회원 로그아웃 API 는") + class Logout_user_api { + + @Test + @DisplayName("현재 로그인된 유저를 로그아웃 시킨다.") + void Logout_user() { + // given + var existUser = repository.saveAndGetUser(); + repository.saveRedisDataWithExpiration(String.valueOf(existUser.getId()), VALID_TOKEN, DURATION); + + // when & then + assertThatCode(() -> api.logoutUser(VALID_TOKEN)) + .doesNotThrowAnyException(); + } + } } From 89a14f32af3afb98030fd4db0c8e3f438a0bd4d3 Mon Sep 17 00:00:00 2001 From: ChoiDongKuen Date: Mon, 15 Jan 2024 10:10:31 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20Security=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B0=8F=20=EA=B8=B0=ED=83=80=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20(#84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: static 경로 설정 (#83) * chore: jwt 관련 binding 의존성 추가 (#83) * chore: 라이브러리 버전 추가 (#83) * refactor: 인증 관련 코드 리팩토링 (#83) * feat: ErrorResponse static 메서드 추가 (#83) * feat: SecurityConfig 추가 구현 (#83) --- gradle.properties | 4 ++ gradle/devtool.gradle | 9 +++- .../auth/controller/OAuthLoginController.java | 5 +- .../teumteum/core/error/ErrorResponse.java | 5 +- .../core/security/SecurityConfig.java | 52 +++++++++++-------- .../filter/JwtAuthenticationEntryPoint.java | 20 ++++--- src/main/resources/application-dev.yml | 1 + src/main/resources/application-prod.yml | 4 +- 8 files changed, 63 insertions(+), 37 deletions(-) diff --git a/gradle.properties b/gradle.properties index a31bbd59..f478ef71 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,3 +19,7 @@ mockWebServerVersion=4.12.0 sentryVersion=4.1.1 ### AWS-CLOUD ### springCloudAwsVersion=3.1.0 +### JWT ### +jwtVersion=0.9.1 +jwtBindingVersion=4.0.1 +jwtJaxbApiVersion=2.3.1 diff --git a/gradle/devtool.gradle b/gradle/devtool.gradle index 06937c24..5b4ad617 100644 --- a/gradle/devtool.gradle +++ b/gradle/devtool.gradle @@ -2,7 +2,14 @@ allprojects { dependencies { compileOnly "org.projectlombok:lombok:${lombokVersion}" annotationProcessor "org.projectlombok:lombok" - implementation 'io.jsonwebtoken:jjwt:0.9.1' + + implementation "io.jsonwebtoken:jjwt:${jwtVersion}" + + // com.sun.xml.bind + implementation "com.sun.xml.bind:jaxb-impl:${jwtBindingVersion}" + implementation "com.sun.xml.bind:jaxb-core:${jwtBindingVersion}" + // javax.xml.bind + implementation "javax.xml.bind:jaxb-api:${jwtJaxbApiVersion}" testCompileOnly "org.projectlombok:lombok:${lombokVersion}" testAnnotationProcessor "org.projectlombok:lombok" diff --git a/src/main/java/net/teumteum/auth/controller/OAuthLoginController.java b/src/main/java/net/teumteum/auth/controller/OAuthLoginController.java index 8a605eda..782080be 100644 --- a/src/main/java/net/teumteum/auth/controller/OAuthLoginController.java +++ b/src/main/java/net/teumteum/auth/controller/OAuthLoginController.java @@ -3,19 +3,18 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import net.teumteum.auth.domain.response.TokenResponse; +import net.teumteum.auth.service.OAuthService; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; @Slf4j -@RequestMapping @RequiredArgsConstructor public class OAuthLoginController { - private final net.teumteum.auth.service.OAuthService oAuthService; + private final OAuthService oAuthService; @GetMapping("/logins/callbacks/{provider}") @ResponseStatus(HttpStatus.OK) diff --git a/src/main/java/net/teumteum/core/error/ErrorResponse.java b/src/main/java/net/teumteum/core/error/ErrorResponse.java index b9b1323e..b91d26b8 100644 --- a/src/main/java/net/teumteum/core/error/ErrorResponse.java +++ b/src/main/java/net/teumteum/core/error/ErrorResponse.java @@ -4,7 +4,6 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.RequiredArgsConstructor; @Getter @NoArgsConstructor @@ -16,4 +15,8 @@ public class ErrorResponse { public static ErrorResponse of(Throwable exception) { return new ErrorResponse(exception.getMessage()); } + + public static ErrorResponse of(String message) { + return new ErrorResponse(message); + } } diff --git a/src/main/java/net/teumteum/core/security/SecurityConfig.java b/src/main/java/net/teumteum/core/security/SecurityConfig.java index a2a74af0..fc437297 100644 --- a/src/main/java/net/teumteum/core/security/SecurityConfig.java +++ b/src/main/java/net/teumteum/core/security/SecurityConfig.java @@ -3,18 +3,19 @@ import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS; - -import java.util.Collections; +import java.util.List; import lombok.RequiredArgsConstructor; import net.teumteum.core.security.filter.JwtAccessDeniedHandler; import net.teumteum.core.security.filter.JwtAuthenticationEntryPoint; import net.teumteum.core.security.filter.JwtAuthenticationFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; @@ -27,43 +28,48 @@ @RequiredArgsConstructor public class SecurityConfig { - private static final String[] PATTERNS = {"/", "/css/**", "/images/**", "/js/**", "/favicon.ico", "/h2-console/**", - "/docs/index.html", "/common/*.html", "/jwt-test", "/auth/**"}; + private static final String[] PATTERNS = {"/css/**", "/images/**", "/js/**", "/favicon.ico", "/h2-console/**", + "/logins/**"}; private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; private final JwtAccessDeniedHandler accessDeniedHandler; private final JwtAuthenticationFilter jwtAuthenticationFilter; - @Bean - CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration config = new CorsConfiguration(); - config.setAllowedHeaders(Collections.singletonList("*")); - config.setAllowedMethods(Collections.singletonList("*")); - config.setAllowedOriginPatterns(Collections.singletonList("/**")); // 허용할 origin - config.setAllowCredentials(true); - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", config); - return source; - } - @Bean public WebSecurityCustomizer webSecurityCustomizer() { - return web -> web.ignoring().requestMatchers("/h2-console/**"); + return web -> web.ignoring() + .requestMatchers("/css/**", "/js/**", "/img/**", "/favicon.ico", "/error"); } @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http.csrf(AbstractHttpConfigurer::disable) - .cors(AbstractHttpConfigurer::disable) - .authorizeHttpRequests( - request -> request.requestMatchers("/**").permitAll() - .requestMatchers(PATTERNS).permitAll().anyRequest() - .authenticated()).httpBasic(AbstractHttpConfigurer::disable).formLogin(AbstractHttpConfigurer::disable) + http.csrf(AbstractHttpConfigurer::disable).cors(cors -> cors.configurationSource(corsConfigurationSource())) + .authorizeHttpRequests(request -> request.requestMatchers("/auth/**", "/logins/**").permitAll() + .requestMatchers(HttpMethod.POST, "/users/registers").permitAll().requestMatchers(PATTERNS).permitAll() + .anyRequest().authenticated()).httpBasic(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(STATELESS)) + .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)) .exceptionHandling( exceptionHandling -> exceptionHandling.authenticationEntryPoint(jwtAuthenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler)) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } + + @Bean + CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + config.addAllowedOrigin("http://localhost:3000"); + config.addAllowedOrigin("https://api.teum.org"); + config.addAllowedHeader("*"); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); + config.addExposedHeader("Authorization"); + config.addExposedHeader("Authorization-refresh"); + config.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } } diff --git a/src/main/java/net/teumteum/core/security/filter/JwtAuthenticationEntryPoint.java b/src/main/java/net/teumteum/core/security/filter/JwtAuthenticationEntryPoint.java index 20519ae8..9bf74010 100644 --- a/src/main/java/net/teumteum/core/security/filter/JwtAuthenticationEntryPoint.java +++ b/src/main/java/net/teumteum/core/security/filter/JwtAuthenticationEntryPoint.java @@ -1,30 +1,34 @@ package net.teumteum.core.security.filter; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.OutputStream; import lombok.extern.slf4j.Slf4j; +import net.teumteum.core.error.ErrorResponse; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; -import java.io.IOException; - -import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; - @Slf4j @Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + @Override public void commence(HttpServletRequest request, - HttpServletResponse response, - AuthenticationException authenticationException + HttpServletResponse response, + AuthenticationException authenticationException ) throws IOException { this.sendUnAuthenticatedError(response, authenticationException); } private void sendUnAuthenticatedError(HttpServletResponse response, - Exception exception) throws IOException { + Exception exception) throws IOException { + OutputStream os = response.getOutputStream(); + ObjectMapper objectMapper = new ObjectMapper(); log.error("Responding with unauthenticated error. Message - {}", exception.getMessage()); - response.sendError(SC_UNAUTHORIZED, exception.getMessage()); + objectMapper.writeValue(os, ErrorResponse.of("인증 과정에서 오류가 발생했습니다.")); + os.flush(); } } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index bf798954..ccfd58c0 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -7,6 +7,7 @@ spring: mvc: pathmatch: matching-strategy: ant_path_matcher + static-path-pattern: /static/** servlet: multipart: diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 7ac4f5dd..5687b991 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -7,13 +7,15 @@ spring: mvc: pathmatch: matching-strategy: ant_path_matcher + static-path-pattern: /static/** + servlet: multipart: max-file-size: 10MB max-request-size: 50MB -### JPA ### + ### JPA ### jpa: hibernate: ddl-auto: validate From e2695ba434333f019f4d954e578d6c440b050566 Mon Sep 17 00:00:00 2001 From: ChoiDongKuen Date: Mon, 15 Jan 2024 10:11:34 +0900 Subject: [PATCH 3/7] =?UTF-8?q?refactor:=20=EA=B8=B0=EC=A1=B4=20API=20&=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20?= =?UTF-8?q?=EB=B0=8F=20=EB=88=84=EB=9D=BD=EB=90=9C=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EA=B5=AC=ED=98=84=20(#82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 유저 관련 API url 리팩토링 (#81) * refactor: 기존 테스트 관련 클래스 리팩토링 (#81) * refactor: 기존 코드 리팩토링 (#81) --- .../teumteum/auth/service/OAuthService.java | 9 +-- .../user/controller/UserController.java | 4 +- .../core/security/SecurityServiceTest.java | 73 +++++++++++++++++++ .../java/net/teumteum/integration/Api.java | 4 +- .../teumteum/integration/IntegrationTest.java | 12 +-- .../net/teumteum/integration/Repository.java | 4 +- 6 files changed, 86 insertions(+), 20 deletions(-) create mode 100644 src/test/java/net/teumteum/core/security/SecurityServiceTest.java diff --git a/src/main/java/net/teumteum/auth/service/OAuthService.java b/src/main/java/net/teumteum/auth/service/OAuthService.java index 43dd8308..2adf5f44 100644 --- a/src/main/java/net/teumteum/auth/service/OAuthService.java +++ b/src/main/java/net/teumteum/auth/service/OAuthService.java @@ -67,11 +67,10 @@ private OAuthUserInfo getOAuthUserInfo(ClientRegistration clientRegistration, Au private TokenResponse checkUserAndMakeResponse(OAuthUserInfo oAuthUserInfo, Authenticated authenticated) { String oauthId = oAuthUserInfo.getOAuthId(); - Optional user = getUser(oauthId, authenticated); - if (user.isEmpty()) { - return new TokenResponse(oAuthUserInfo.getOAuthId()); - } - return jwtService.createServiceToken(user.get()); + + return getUser(oauthId, authenticated) + .map(jwtService::createServiceToken) + .orElseGet(() -> new TokenResponse(oauthId)); } private Map getOAuthAttribute(ClientRegistration clientRegistration, OAuthToken oAuthToken) { diff --git a/src/main/java/net/teumteum/user/controller/UserController.java b/src/main/java/net/teumteum/user/controller/UserController.java index 87474518..89af7e63 100644 --- a/src/main/java/net/teumteum/user/controller/UserController.java +++ b/src/main/java/net/teumteum/user/controller/UserController.java @@ -78,13 +78,13 @@ public InterestQuestionResponse getInterestQuestion(@RequestParam("user-id") Lis return userService.getInterestQuestionByUserIds(userIds, balance); } - @DeleteMapping("/withdraws") + @DeleteMapping @ResponseStatus(HttpStatus.OK) public void withdraw() { userService.withdraw(getCurrentUserId()); } - @PostMapping("/registers") + @PostMapping @ResponseStatus(HttpStatus.CREATED) public UserRegisterResponse register(@RequestBody UserRegisterRequest request) { return userService.register(request); diff --git a/src/test/java/net/teumteum/core/security/SecurityServiceTest.java b/src/test/java/net/teumteum/core/security/SecurityServiceTest.java new file mode 100644 index 00000000..4aca667c --- /dev/null +++ b/src/test/java/net/teumteum/core/security/SecurityServiceTest.java @@ -0,0 +1,73 @@ +package net.teumteum.core.security; + +import net.teumteum.Application; +import net.teumteum.core.security.service.SecurityService; +import net.teumteum.integration.Repository; +import net.teumteum.integration.SecurityContextSetting; +import net.teumteum.integration.TestLoginContext; +import net.teumteum.user.infra.GptTestServer; +import org.junit.jupiter.api.Assertions; +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.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ContextConfiguration; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@ContextConfiguration(classes = { + Application.class, + Repository.class, + GptTestServer.class, + TestLoginContext.class, + SecurityContextSetting.class}) +@DisplayName("Security Service 테스트의") +public class SecurityServiceTest { + + @Autowired + SecurityService securityService; + + @Autowired + Repository repository; + + @Nested + @DisplayName("Security Service 클래스는") + class Security_service_test { + + @Test + @DisplayName("UserAuthentication 객체를 SecurityContext 에 저장하고, userId를 반환한다.") + void Set_user_authentication_and_return_user_id() { + // given + var existUser = repository.saveAndGetUser(); + SecurityContextHolder.getContext().setAuthentication(new UserAuthentication(existUser)); + + var savedAuthentication = SecurityContextHolder.getContext().getAuthentication(); + + // when + Long savedUserId = securityService.getCurrentUserId(); + + // then + Assertions.assertEquals(savedUserId, existUser.getId()); + Assertions.assertNotNull(savedAuthentication); + } + + @Test + @DisplayName("UserAuthentication 객체를 SecurityContext 에 저장하고, OAuthId를 반환한다.") + void Set_user_authentication_return_oAuth_id() { + // given + var existUser = repository.saveAndGetUser(); + SecurityContextHolder.getContext().setAuthentication(new UserAuthentication(existUser)); + + var savedAuthentication = SecurityContextHolder.getContext().getAuthentication(); + + // when + String savedOAuthId = securityService.getCurrentUserOAuthId(); + + // then + Assertions.assertEquals(savedOAuthId, existUser.getOauth().getOauthId()); + Assertions.assertNotNull(savedAuthentication); + } + } +} diff --git a/src/test/java/net/teumteum/integration/Api.java b/src/test/java/net/teumteum/integration/Api.java index d00c4da6..693b956d 100644 --- a/src/test/java/net/teumteum/integration/Api.java +++ b/src/test/java/net/teumteum/integration/Api.java @@ -130,7 +130,7 @@ ResponseSpec reissueJwt(String accessToken, String refreshToken) { ResponseSpec withdrawUser(String accessToken) { return webTestClient.delete() - .uri("/users/withdraws") + .uri("/users") .header(HttpHeaders.AUTHORIZATION, accessToken) .exchange(); } @@ -138,7 +138,7 @@ ResponseSpec withdrawUser(String accessToken) { ResponseSpec registerUserCard(String accessToken, UserRegisterRequest userRegisterRequest) { return webTestClient .post() - .uri("/users/registers") + .uri("/users") .header(HttpHeaders.AUTHORIZATION, accessToken) .bodyValue(userRegisterRequest) .exchange(); diff --git a/src/test/java/net/teumteum/integration/IntegrationTest.java b/src/test/java/net/teumteum/integration/IntegrationTest.java index 8ea33d79..70237b6e 100644 --- a/src/test/java/net/teumteum/integration/IntegrationTest.java +++ b/src/test/java/net/teumteum/integration/IntegrationTest.java @@ -20,8 +20,8 @@ Repository.class, Application.class, GptTestServer.class, - TestLoginContext.class, - SecurityContextSetting.class}) + TestLoginContext.class +}) abstract public class IntegrationTest { @Autowired @@ -30,20 +30,14 @@ abstract public class IntegrationTest { @Autowired protected Repository repository; - @Autowired - protected SecurityContextSetting securityContextSetting; @Autowired protected TestLoginContext loginContext; + @AfterEach @BeforeEach void clearAll() { repository.clear(); } - - @BeforeEach - void setSecurityContextSetting() { - securityContextSetting.set(); - } } diff --git a/src/test/java/net/teumteum/integration/Repository.java b/src/test/java/net/teumteum/integration/Repository.java index 4ba47c7e..b5d1d20b 100644 --- a/src/test/java/net/teumteum/integration/Repository.java +++ b/src/test/java/net/teumteum/integration/Repository.java @@ -1,6 +1,7 @@ package net.teumteum.integration; +import jakarta.persistence.EntityManager; import java.util.List; import java.util.stream.Stream; import lombok.RequiredArgsConstructor; @@ -20,14 +21,13 @@ @Import(AppConfig.class) @RequiredArgsConstructor class Repository { - private final UserRepository userRepository; private final MeetingRepository meetingRepository; private final RedisService redisService; - User saveAndGetUser() { + public User saveAndGetUser() { var user = UserFixture.getNullIdUser(); return userRepository.saveAndFlush(user); } From 15daf722b4aec10f32072e2d3cf8e77f8d2a3ad7 Mon Sep 17 00:00:00 2001 From: ChoiDongKuen Date: Mon, 15 Jan 2024 10:17:14 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20Redis=20Test=20Container=20?= =?UTF-8?q?=EA=B5=AC=EC=84=B1=20=EB=B0=8F=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=A7=84=ED=96=89=20?= =?UTF-8?q?(#79)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Test Container 의존성 추가 (#73) * feat: RedisTestContainerConfig 구현 (#73) * feat: IntegrationTest 적용 (#73) * test: 회원 탈퇴 API 통합 테스트 진행 (with redis) (#73) * fix: SonarCloud 에러 수정 (#73) * fix: SonarCloud 에러 수정 (#73) * refactor: 버전 관리 분리 (코드 리뷰 반영) (#52) * fix: CI Integration Tester 에러 수정 (#73) --- gradle.properties | 2 ++ gradle/test.gradle | 6 +++--- src/test/java/net/teumteum/integration/Repository.java | 4 +++- .../java/net/teumteum/integration/UserIntegrationTest.java | 1 + 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index f478ef71..ff5dd21b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,6 +4,8 @@ projectVersion=1.0 ### TEST ### junitVersion=5.10.1 assertJVersion=3.24.2 +testContainer=1.17.6 +junitTestContainer=5.8.1 ### LOMBOK ### lombokVersion=1.18.30 ### SPRING ### diff --git a/gradle/test.gradle b/gradle/test.gradle index f7dbd28a..e6b73b26 100644 --- a/gradle/test.gradle +++ b/gradle/test.gradle @@ -7,9 +7,9 @@ allprojects { dependencies { // test container for redis - testImplementation "org.junit.jupiter:junit-jupiter:5.8.1" - testImplementation "org.testcontainers:testcontainers:1.17.6" - testImplementation "org.testcontainers:junit-jupiter:1.17.6" + testImplementation "org.junit.jupiter:junit-jupiter:${junitTestContainer}" + testImplementation "org.testcontainers:testcontainers:${testContainer}" + testImplementation "org.testcontainers:junit-jupiter:${testContainer}" testImplementation "org.junit.jupiter:junit-jupiter-api:${junitVersion}" testImplementation "org.junit.jupiter:junit-jupiter-params:${junitVersion}" diff --git a/src/test/java/net/teumteum/integration/Repository.java b/src/test/java/net/teumteum/integration/Repository.java index b5d1d20b..ec799625 100644 --- a/src/test/java/net/teumteum/integration/Repository.java +++ b/src/test/java/net/teumteum/integration/Repository.java @@ -20,7 +20,9 @@ @TestComponent @Import(AppConfig.class) @RequiredArgsConstructor -class Repository { + +public class Repository { + private final UserRepository userRepository; private final MeetingRepository meetingRepository; diff --git a/src/test/java/net/teumteum/integration/UserIntegrationTest.java b/src/test/java/net/teumteum/integration/UserIntegrationTest.java index a75f7648..638f3712 100644 --- a/src/test/java/net/teumteum/integration/UserIntegrationTest.java +++ b/src/test/java/net/teumteum/integration/UserIntegrationTest.java @@ -213,6 +213,7 @@ void Withdraw_user_info_api() { loginContext.setUserId(me.getId()); // when & then + assertThatCode(() -> api.withdrawUser(VALID_TOKEN)) .doesNotThrowAnyException(); } From 409ec67bf27e6f5b53c32a9dfd58d2c85e5904e0 Mon Sep 17 00:00:00 2001 From: ddingmin Date: Mon, 15 Jan 2024 20:44:04 +0900 Subject: [PATCH 5/7] =?UTF-8?q?feat:=20=EB=82=B4=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84=20(#78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 내 정보를 조회하는 기능 구현 * test: 내 정보를 조회하는 기능 구현 테스트 * feat: UserGetResponse 를 추가한다. --- .../user/controller/UserController.java | 30 +++----- .../domain/response/UserMeGetResponse.java | 73 +++++++++++++++++++ .../teumteum/user/service/UserService.java | 15 ++-- .../integration/UserIntegrationTest.java | 36 +++++++-- 4 files changed, 125 insertions(+), 29 deletions(-) create mode 100644 src/main/java/net/teumteum/user/domain/response/UserMeGetResponse.java diff --git a/src/main/java/net/teumteum/user/controller/UserController.java b/src/main/java/net/teumteum/user/controller/UserController.java index 89af7e63..f7255e69 100644 --- a/src/main/java/net/teumteum/user/controller/UserController.java +++ b/src/main/java/net/teumteum/user/controller/UserController.java @@ -1,32 +1,20 @@ package net.teumteum.user.controller; import io.sentry.Sentry; -import java.util.Arrays; -import java.util.List; import lombok.RequiredArgsConstructor; import net.teumteum.core.error.ErrorResponse; import net.teumteum.core.security.service.SecurityService; +import net.teumteum.user.domain.response.UserMeGetResponse; import net.teumteum.user.domain.request.UserRegisterRequest; import net.teumteum.user.domain.request.UserUpdateRequest; -import net.teumteum.user.domain.response.FriendsResponse; -import net.teumteum.user.domain.response.InterestQuestionResponse; -import net.teumteum.user.domain.response.UserGetResponse; -import net.teumteum.user.domain.response.UserRegisterResponse; -import net.teumteum.user.domain.response.UsersGetByIdResponse; +import net.teumteum.user.domain.response.*; import net.teumteum.user.service.UserService; import org.springframework.context.ApplicationContext; import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.List; @RestController @RequiredArgsConstructor @@ -53,6 +41,12 @@ public UsersGetByIdResponse getUsersById(@RequestParam("id") String userIds) { return userService.getUsersById(parsedUserIds); } + @GetMapping("/me") + @ResponseStatus(HttpStatus.OK) + public UserMeGetResponse getMe() { + return userService.getMe(getCurrentUserId()); + } + @PutMapping @ResponseStatus(HttpStatus.OK) public void updateUser(@RequestBody UserUpdateRequest request) { diff --git a/src/main/java/net/teumteum/user/domain/response/UserMeGetResponse.java b/src/main/java/net/teumteum/user/domain/response/UserMeGetResponse.java new file mode 100644 index 00000000..a9f9b9eb --- /dev/null +++ b/src/main/java/net/teumteum/user/domain/response/UserMeGetResponse.java @@ -0,0 +1,73 @@ +package net.teumteum.user.domain.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import net.teumteum.core.security.Authenticated; +import net.teumteum.user.domain.User; + + +import java.util.List; + +public record UserMeGetResponse( + Long id, + String name, + String birth, + Long characterId, + int mannerTemperature, + Authenticated authenticated, + ActivityArea activityArea, + String mbti, + String status, + String goal, + Job job, + List interests +) { + + public static UserMeGetResponse of(User user) { + return new UserMeGetResponse( + user.getId(), + user.getName(), + user.getBirth(), + user.getCharacterId(), + user.getMannerTemperature(), + user.getOauth().getAuthenticated(), + ActivityArea.of(user), + user.getMbti(), + user.getStatus().name(), + user.getGoal(), + Job.of(user), + user.getInterests() + ); + } + + public record ActivityArea( + String city, + List streets + ) { + + public static ActivityArea of(User user) { + return new ActivityArea( + user.getActivityArea().getCity(), + user.getActivityArea().getStreet() + ); + } + + } + + public record Job( + String name, + boolean certificated, + @JsonProperty("class") + String jobClass, + String detailClass + ) { + + public static Job of(User user) { + return new Job( + user.getJob().getName(), + user.getJob().isCertificated(), + user.getJob().getJobClass(), + user.getJob().getDetailJobClass() + ); + } + } +} diff --git a/src/main/java/net/teumteum/user/service/UserService.java b/src/main/java/net/teumteum/user/service/UserService.java index 55ccc3ed..3c5800d0 100644 --- a/src/main/java/net/teumteum/user/service/UserService.java +++ b/src/main/java/net/teumteum/user/service/UserService.java @@ -1,25 +1,24 @@ package net.teumteum.user.service; -import java.util.List; import lombok.RequiredArgsConstructor; import net.teumteum.core.security.Authenticated; import net.teumteum.core.security.service.RedisService; + import net.teumteum.core.security.service.SecurityService; import net.teumteum.user.domain.BalanceGameType; import net.teumteum.user.domain.InterestQuestion; import net.teumteum.user.domain.User; import net.teumteum.user.domain.UserRepository; + import net.teumteum.user.domain.request.UserRegisterRequest; import net.teumteum.user.domain.request.UserUpdateRequest; -import net.teumteum.user.domain.response.FriendsResponse; -import net.teumteum.user.domain.response.InterestQuestionResponse; -import net.teumteum.user.domain.response.UserGetResponse; -import net.teumteum.user.domain.response.UserRegisterResponse; -import net.teumteum.user.domain.response.UsersGetByIdResponse; +import net.teumteum.user.domain.response.*; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; +import java.util.List; + @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -35,6 +34,10 @@ public UserGetResponse getUserById(Long userId) { return UserGetResponse.of(existUser); } + public UserMeGetResponse getMe(Long userId) { + return UserMeGetResponse.of(getUser(userId)); + } + public UsersGetByIdResponse getUsersById(List userIds) { var existUsers = userRepository.findAllById(userIds); diff --git a/src/test/java/net/teumteum/integration/UserIntegrationTest.java b/src/test/java/net/teumteum/integration/UserIntegrationTest.java index 638f3712..40b4a7d1 100644 --- a/src/test/java/net/teumteum/integration/UserIntegrationTest.java +++ b/src/test/java/net/teumteum/integration/UserIntegrationTest.java @@ -1,19 +1,19 @@ package net.teumteum.integration; + import static org.assertj.core.api.Assertions.assertThatCode; -import java.util.List; import net.teumteum.core.error.ErrorResponse; import net.teumteum.user.domain.User; -import net.teumteum.user.domain.response.FriendsResponse; -import net.teumteum.user.domain.response.UserGetResponse; -import net.teumteum.user.domain.response.UserRegisterResponse; -import net.teumteum.user.domain.response.UsersGetByIdResponse; +import net.teumteum.user.domain.response.UserMeGetResponse; +import net.teumteum.user.domain.response.*; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import java.util.List; + @DisplayName("유저 통합테스트의") class UserIntegrationTest extends IntegrationTest { @@ -108,6 +108,32 @@ void Return_400_bad_request_if_empty_user_ids_input() { } } + @Nested + @DisplayName("내 정보 조회 API는") + class Find_my_info_api { + + @Test + @DisplayName("유효한 토큰이 주어지면, 내 정보를 응답한다.") + void Return_my_info_if_valid_token_received() { + // given + var me = repository.saveAndGetUser(); + loginContext.setUserId(me.getId()); + + var expected = UserMeGetResponse.of(me); + + // when + var result = api.getUser(VALID_TOKEN, me.getId()); + + // then + Assertions.assertThat(result.expectStatus().isOk() + .expectBody(UserMeGetResponse.class) + .returnResult() + .getResponseBody()) + .usingRecursiveComparison().isEqualTo(expected); + } + + } + @Nested @DisplayName("유저 수정 API는") class Update_user_api { From 86e9511f44bff0fd1fd5bb3d128837c97f4adc50 Mon Sep 17 00:00:00 2001 From: ChoiDongKuen Date: Mon, 15 Jan 2024 21:00:32 +0900 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20jwtService=20secretKey=20=EB=B0=94?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=EB=A1=9C=20=EB=B3=80=ED=99=98=20(#73)=20(#87?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../net/teumteum/core/security/service/JwtService.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/teumteum/core/security/service/JwtService.java b/src/main/java/net/teumteum/core/security/service/JwtService.java index 76981daa..d29371c6 100644 --- a/src/main/java/net/teumteum/core/security/service/JwtService.java +++ b/src/main/java/net/teumteum/core/security/service/JwtService.java @@ -46,7 +46,7 @@ public String extractRefreshToken(HttpServletRequest request) { public String getUserIdFromToken(String token) { try { - return Jwts.parser().setSigningKey(jwtProperty.getSecret()) + return Jwts.parser().setSigningKey(jwtProperty.getSecret().getBytes()) .parseClaimsJws(token).getBody().getSubject(); } catch (Exception exception) { throw new JwtException("Access Token is not valid"); @@ -79,13 +79,14 @@ private String createToken(String payload, Long tokenExpiration) { .setClaims(claims) .setIssuedAt(new Date()) .setExpiration(tokenExpiresIn) - .signWith(SignatureAlgorithm.HS512, jwtProperty.getSecret()) + .signWith(SignatureAlgorithm.HS512, jwtProperty.getSecret().getBytes()) .compact(); } public boolean validateToken(String token) { try { - Jws claimsJws = Jwts.parser().setSigningKey(jwtProperty.getSecret()).parseClaimsJws(token); + Jws claimsJws = Jwts.parser().setSigningKey(jwtProperty.getSecret().getBytes()) + .parseClaimsJws(token); return !claimsJws.getBody().getExpiration().before(new Date()); } catch (ExpiredJwtException exception) { log.warn("만료된 jwt 입니다."); From b97f4961a643c0f2d0c25d4824786832b8e2d62e Mon Sep 17 00:00:00 2001 From: ddingmin Date: Mon, 15 Jan 2024 21:32:30 +0900 Subject: [PATCH 7/7] =?UTF-8?q?build:=20=EC=B6=A9=EB=8F=8C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../teumteum/user/service/UserService.java | 5 +++ .../integration/UserIntegrationTest.java | 35 ++++++++++++++++--- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/main/java/net/teumteum/user/service/UserService.java b/src/main/java/net/teumteum/user/service/UserService.java index 55ccc3ed..90849cf2 100644 --- a/src/main/java/net/teumteum/user/service/UserService.java +++ b/src/main/java/net/teumteum/user/service/UserService.java @@ -14,6 +14,7 @@ import net.teumteum.user.domain.response.FriendsResponse; import net.teumteum.user.domain.response.InterestQuestionResponse; import net.teumteum.user.domain.response.UserGetResponse; +import net.teumteum.user.domain.response.UserMeGetResponse; import net.teumteum.user.domain.response.UserRegisterResponse; import net.teumteum.user.domain.response.UsersGetByIdResponse; import org.springframework.stereotype.Service; @@ -35,6 +36,10 @@ public UserGetResponse getUserById(Long userId) { return UserGetResponse.of(existUser); } + public UserMeGetResponse getMe(Long userId) { + return UserMeGetResponse.of(getUser(userId)); + } + public UsersGetByIdResponse getUsersById(List userIds) { var existUsers = userRepository.findAllById(userIds); diff --git a/src/test/java/net/teumteum/integration/UserIntegrationTest.java b/src/test/java/net/teumteum/integration/UserIntegrationTest.java index 638f3712..502f133f 100644 --- a/src/test/java/net/teumteum/integration/UserIntegrationTest.java +++ b/src/test/java/net/teumteum/integration/UserIntegrationTest.java @@ -2,18 +2,17 @@ import static org.assertj.core.api.Assertions.assertThatCode; -import java.util.List; import net.teumteum.core.error.ErrorResponse; import net.teumteum.user.domain.User; -import net.teumteum.user.domain.response.FriendsResponse; -import net.teumteum.user.domain.response.UserGetResponse; -import net.teumteum.user.domain.response.UserRegisterResponse; -import net.teumteum.user.domain.response.UsersGetByIdResponse; +import net.teumteum.user.domain.response.UserMeGetResponse; +import net.teumteum.user.domain.response.*; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import java.util.List; + @DisplayName("유저 통합테스트의") class UserIntegrationTest extends IntegrationTest { @@ -108,6 +107,32 @@ void Return_400_bad_request_if_empty_user_ids_input() { } } + @Nested + @DisplayName("내 정보 조회 API는") + class Find_my_info_api { + + @Test + @DisplayName("유효한 토큰이 주어지면, 내 정보를 응답한다.") + void Return_my_info_if_valid_token_received() { + // given + var me = repository.saveAndGetUser(); + loginContext.setUserId(me.getId()); + + var expected = UserMeGetResponse.of(me); + + // when + var result = api.getUser(VALID_TOKEN, me.getId()); + + // then + Assertions.assertThat(result.expectStatus().isOk() + .expectBody(UserMeGetResponse.class) + .returnResult() + .getResponseBody()) + .usingRecursiveComparison().isEqualTo(expected); + } + + } + @Nested @DisplayName("유저 수정 API는") class Update_user_api {