diff --git a/src/main/kotlin/com/back/koreaTravelGuide/common/security/CustomOAuth2LoginSuccessHandler.kt b/src/main/kotlin/com/back/koreaTravelGuide/common/security/CustomOAuth2LoginSuccessHandler.kt index aaa6449..f8c6b1c 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/common/security/CustomOAuth2LoginSuccessHandler.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/common/security/CustomOAuth2LoginSuccessHandler.kt @@ -39,8 +39,6 @@ class CustomOAuth2LoginSuccessHandler( redirectStrategy.sendRedirect(request, response, targetUrl) } else { - val accessToken = jwtTokenProvider.createAccessToken(user.id!!, user.role) - val refreshToken = jwtTokenProvider.createRefreshToken(user.id!!) val redisKey = "refreshToken:${user.id}" @@ -60,7 +58,7 @@ class CustomOAuth2LoginSuccessHandler( response.addCookie(cookie) - val targetUrl = "http://localhost:3000/oauth/callback?accessToken=$accessToken" + val targetUrl = "http://localhost:3000/oauth/callback" redirectStrategy.sendRedirect(request, response, targetUrl) } diff --git a/src/main/kotlin/com/back/koreaTravelGuide/common/security/JwtTokenProvider.kt b/src/main/kotlin/com/back/koreaTravelGuide/common/security/JwtTokenProvider.kt index a7c81df..8e47441 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/common/security/JwtTokenProvider.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/common/security/JwtTokenProvider.kt @@ -1,10 +1,10 @@ package com.back.koreaTravelGuide.common.security +import com.back.koreaTravelGuide.common.logging.log import com.back.koreaTravelGuide.domain.user.enums.UserRole import io.jsonwebtoken.Claims import io.jsonwebtoken.Jwts import io.jsonwebtoken.security.Keys -import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.core.Authentication @@ -20,10 +20,7 @@ class JwtTokenProvider( @Value("\${jwt.access-token-expiration-minutes}") private val accessTokenExpirationMinutes: Long, @Value("\${jwt.refresh-token-expiration-days}") private val refreshTokenExpirationDays: Long, ) { - private val logger = LoggerFactory.getLogger(JwtTokenProvider::class.java) - private val key: SecretKey by lazy { - Keys.hmacShaKeyFor(Base64.getEncoder().encode(secretKey.toByteArray())) } @@ -76,7 +73,7 @@ class JwtTokenProvider( return true } catch (e: Exception) { - logger.error("Token validation error: ${e.message}") + log.error("Token validation error: ${e.message}") return false } diff --git a/src/main/kotlin/com/back/koreaTravelGuide/common/security/SecurityConfig.kt b/src/main/kotlin/com/back/koreaTravelGuide/common/security/SecurityConfig.kt index c854245..3d123ec 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/common/security/SecurityConfig.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/common/security/SecurityConfig.kt @@ -1,8 +1,8 @@ package com.back.koreaTravelGuide.common.config -import com.back.koreaTravelGuide.security.CustomOAuth2LoginSuccessHandler -import com.back.koreaTravelGuide.security.CustomOAuth2UserService -import com.back.koreaTravelGuide.security.JwtAuthenticationFilter +import com.back.koreaTravelGuide.common.security.CustomOAuth2LoginSuccessHandler +import com.back.koreaTravelGuide.common.security.CustomOAuth2UserService +import com.back.koreaTravelGuide.common.security.JwtAuthenticationFilter import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.core.env.Environment diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/user/entity/User.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/entity/User.kt index 7124b57..382d922 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/user/entity/User.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/entity/User.kt @@ -13,7 +13,8 @@ import jakarta.persistence.Table import org.springframework.data.annotation.CreatedDate import org.springframework.data.annotation.LastModifiedDate import org.springframework.data.jpa.domain.support.AuditingEntityListener -import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZonedDateTime @Entity @Table(name = "users") @@ -41,8 +42,8 @@ class User( var description: String? = null, @CreatedDate @Column(name = "created_at", nullable = false, updatable = false) - val createdAt: LocalDateTime = LocalDateTime.now(), + val createdAt: ZonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Seoul")), @LastModifiedDate @Column(name = "last_login_at") - var lastLoginAt: LocalDateTime = LocalDateTime.now(), + var lastLoginAt: ZonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Seoul")), ) diff --git a/src/test/kotlin/com/back/koreaTravelGuide/domain/auth/controller/AuthControllerTest.kt b/src/test/kotlin/com/back/koreaTravelGuide/domain/auth/controller/AuthControllerTest.kt new file mode 100644 index 0000000..b116eef --- /dev/null +++ b/src/test/kotlin/com/back/koreaTravelGuide/domain/auth/controller/AuthControllerTest.kt @@ -0,0 +1,135 @@ +package com.back.koreaTravelGuide.domain.auth.controller + +import com.back.koreaTravelGuide.common.security.JwtTokenProvider +import com.back.koreaTravelGuide.domain.user.entity.User +import com.back.koreaTravelGuide.domain.user.enums.UserRole +import com.back.koreaTravelGuide.domain.user.repository.UserRepository +import com.fasterxml.jackson.databind.ObjectMapper +import jakarta.servlet.http.Cookie +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +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.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.data.redis.core.RedisTemplate +import org.springframework.http.MediaType +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import org.springframework.transaction.annotation.Transactional +import java.util.concurrent.TimeUnit + +@ActiveProfiles("test") +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +class AuthControllerTest { + @Autowired + private lateinit var mockMvc: MockMvc + + @Autowired + private lateinit var userRepository: UserRepository + + @Autowired + private lateinit var jwtTokenProvider: JwtTokenProvider + + @Autowired + private lateinit var redisTemplate: RedisTemplate + + @Autowired + private lateinit var objectMapper: ObjectMapper + + private lateinit var pendingUser: User + private lateinit var generalUser: User + + @BeforeEach + fun setUp() { + pendingUser = + userRepository.save( + User( + email = "pending@test.com", + nickname = "pendingUser", + role = UserRole.PENDING, + oauthProvider = "test", + oauthId = "test1234", + ), + ) + + generalUser = + userRepository.save( + User( + email = "user@test.com", + nickname = "generalUser", + role = UserRole.USER, + oauthProvider = "test", + oauthId = "test5678", + ), + ) + } + + @Test + @DisplayName("신규 사용자 역할 선택 성공") + fun t1() { + // given + val registerToken = jwtTokenProvider.createRegisterToken(pendingUser.id!!) + val requestBody = mapOf("role" to UserRole.USER) + + // when & then + mockMvc.perform( + post("/api/auth/role") + .header("Authorization", "Bearer $registerToken") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestBody)), + ) + .andDo(print()) + .andExpect(status().isOk) + .andExpect(jsonPath("$.msg").value("역할이 선택되었으며 로그인에 성공했습니다.")) + .andExpect(jsonPath("$.data.accessToken").exists()) + } + + @Test + @DisplayName("로그아웃 성공") + fun t2() { + // given + val accessToken = jwtTokenProvider.createAccessToken(generalUser.id!!, generalUser.role) + + // when & then + mockMvc.perform( + post("/api/auth/logout") + .header("Authorization", "Bearer $accessToken"), + ) + .andDo(print()) + .andExpect(status().isOk) + .andExpect(jsonPath("$.msg").value("로그아웃 되었습니다.")) + + // verify + val isBlacklisted = redisTemplate.opsForValue().get(accessToken) != null + assertTrue(isBlacklisted) + } + + @Test + @DisplayName("토큰 재발급 성공") + fun t3() { + // given + val refreshToken = jwtTokenProvider.createRefreshToken(generalUser.id!!) + val redisKey = "refreshToken:${generalUser.id}" + redisTemplate.opsForValue().set(redisKey, refreshToken, 7, TimeUnit.DAYS) + + // when & then + mockMvc.perform( + post("/api/auth/refresh") + .cookie(Cookie("refreshToken", refreshToken)), + ) + .andDo(print()) + .andExpect(status().isOk) + .andExpect(jsonPath("$.msg").value("Access Token이 성공적으로 재발급되었습니다.")) + .andExpect(jsonPath("$.data.accessToken").exists()) + .andExpect(cookie().exists("refreshToken")) + } +} diff --git a/src/test/kotlin/com/back/koreaTravelGuide/domain/user/controller/UserControllerTest.kt b/src/test/kotlin/com/back/koreaTravelGuide/domain/user/controller/UserControllerTest.kt new file mode 100644 index 0000000..d451547 --- /dev/null +++ b/src/test/kotlin/com/back/koreaTravelGuide/domain/user/controller/UserControllerTest.kt @@ -0,0 +1,110 @@ +package com.back.koreaTravelGuide.domain.user.controller + +import com.back.koreaTravelGuide.common.security.JwtTokenProvider +import com.back.koreaTravelGuide.domain.user.entity.User +import com.back.koreaTravelGuide.domain.user.enums.UserRole +import com.back.koreaTravelGuide.domain.user.repository.UserRepository +import com.fasterxml.jackson.databind.ObjectMapper +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.BeforeEach +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.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.http.MediaType +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch +import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import org.springframework.transaction.annotation.Transactional + +@ActiveProfiles("test") +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +class UserControllerTest { + @Autowired + private lateinit var mockMvc: MockMvc + + @Autowired + private lateinit var userRepository: UserRepository + + @Autowired + private lateinit var jwtTokenProvider: JwtTokenProvider + + @Autowired + private lateinit var objectMapper: ObjectMapper + + private lateinit var testUser: User + private lateinit var accessToken: String + + @BeforeEach + fun setUp() { + testUser = + userRepository.save( + User( + email = "testuser@test.com", + nickname = "testUser", + role = UserRole.USER, + oauthProvider = "test", + oauthId = "test12345", + ), + ) + accessToken = jwtTokenProvider.createAccessToken(testUser.id!!, testUser.role) + } + + @Test + @DisplayName("내 정보 조회 성공") + fun t1() { + // when & then + mockMvc.perform( + get("/api/users/me") + .header("Authorization", "Bearer $accessToken"), + ) + .andDo(print()) + .andExpect(status().isOk) + .andExpect(jsonPath("$.data.email").value(testUser.email)) + .andExpect(jsonPath("$.data.nickname").value(testUser.nickname)) + } + + @Test + @DisplayName("내 프로필 수정 성공") + fun t2() { + // given + val updatedNickname = "updatedUser" + val requestBody = mapOf("nickname" to updatedNickname) + + // when & then + mockMvc.perform( + patch("/api/users/me") + .header("Authorization", "Bearer $accessToken") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestBody)), + ) + .andDo(print()) + .andExpect(status().isOk) + .andExpect(jsonPath("$.data.nickname").value(updatedNickname)) + } + + @Test + @DisplayName("회원 탈퇴 성공") + fun t3() { + // when & then + mockMvc.perform( + delete("/api/users/me") + .header("Authorization", "Bearer $accessToken"), + ) + .andDo(print()) + .andExpect(status().isOk) + .andExpect(jsonPath("$.msg").value("회원 탈퇴가 완료되었습니다.")) + + // verify + val userExists = userRepository.existsById(testUser.id!!) + assertFalse(userExists) + } +}