diff --git a/api/build.gradle b/api/build.gradle index 1cdca9e..0744cab 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -11,5 +11,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + + implementation 'org.springframework.boot:spring-boot-starter-security' testImplementation 'org.springframework.boot:spring-boot-starter-test' } \ No newline at end of file diff --git a/api/src/main/java/com/whalewatch/controller/UserController.java b/api/src/main/java/com/whalewatch/controller/UserController.java index 57c922a..6b1a7a4 100644 --- a/api/src/main/java/com/whalewatch/controller/UserController.java +++ b/api/src/main/java/com/whalewatch/controller/UserController.java @@ -1,8 +1,10 @@ package com.whalewatch.controller; import com.whalewatch.domain.User; +import com.whalewatch.dto.TokenResponseDto; import com.whalewatch.dto.UserDto; import com.whalewatch.mapper.UserMapper; +import com.whalewatch.service.JwtService; import com.whalewatch.service.UserService; import org.springframework.web.bind.annotation.*; @@ -10,11 +12,15 @@ @RequestMapping("/api/users") public class UserController { private final UserService userService; + private final JwtService jwtService; private final UserMapper userMapper; - public UserController(UserService userService,UserMapper userMapper) { + public UserController(UserService userService, + UserMapper userMapper, + JwtService jwtService) { this.userService = userService; this.userMapper = userMapper; + this.jwtService = jwtService; } @PostMapping @@ -25,14 +31,14 @@ public UserDto registerUser(@RequestBody UserDto userDto) { } @PostMapping("/login") - public UserDto loginUser(@RequestBody UserDto userDto) { - User user = userService.loginUser(userDto.getEmail(),userDto.getPassword()); - - if (!user.getPassword().equals(userDto.getPassword())){ - throw new RuntimeException("Invalid password"); - } + public TokenResponseDto loginUser(@RequestBody UserDto userDto) { + // JwtService로 로그인 + 토큰 발급 + return jwtService.login(userDto.getEmail(),userDto.getPassword()); + } - return userMapper.toDto(user); + @PostMapping("/refresh") + public TokenResponseDto refreshToken(@RequestBody TokenResponseDto tokenDto){ + return jwtService.refreshAccessToken(tokenDto.getRefreshToken()); } @GetMapping("{id}") @@ -40,4 +46,4 @@ public UserDto getUserInfo(@PathVariable int id) { User user = userService.getUserInfo(id); return userMapper.toDto(user); } -} +} \ No newline at end of file diff --git a/api/src/test/java/com/whalewatch/controller/UserControllerTest.java b/api/src/test/java/com/whalewatch/controller/UserControllerTest.java index 1618a72..a664bfb 100644 --- a/api/src/test/java/com/whalewatch/controller/UserControllerTest.java +++ b/api/src/test/java/com/whalewatch/controller/UserControllerTest.java @@ -2,7 +2,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.whalewatch.domain.User; +import com.whalewatch.dto.TokenResponseDto; import com.whalewatch.dto.UserDto; +import com.whalewatch.repository.JwtTokenRepository; import com.whalewatch.repository.UserRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -10,6 +12,8 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.servlet.MockMvc; import java.util.List; @@ -22,6 +26,11 @@ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureMockMvc +@TestPropertySource(properties = { + "jwt.secret-key=zTjEp7AUmDS+bUZKV5OFIVUtFL7EQCMflxiZ3gxpxo0=", + "jwt.access-token-validity-in-seconds=600", + "jwt.refresh-token-validity-in-seconds=1209600" +}) public class UserControllerTest { @Autowired @@ -33,18 +42,27 @@ public class UserControllerTest { @Autowired private ObjectMapper objectMapper; + @Autowired + private JwtTokenRepository jwtTokenRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + @BeforeEach void setup() { + jwtTokenRepository.deleteAll(); userRepository.deleteAll(); - userRepository.save(new User("test@test.com", "tester", "1234")); + + String hashed = passwordEncoder.encode("1234"); + userRepository.save(new User("test@test.com", "tester", hashed)); } @Test void testRegisterUser() throws Exception { // given UserDto request = new UserDto(); - request.setEmail("test@test.com"); - request.setUsername("tester"); + request.setEmail("test2@test.com"); + request.setUsername("tester2"); request.setPassword("1234"); // when @@ -53,8 +71,8 @@ void testRegisterUser() throws Exception { .content(objectMapper.writeValueAsString(request))) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").exists()) - .andExpect(jsonPath("$.email").value("test@test.com")) - .andExpect(jsonPath("$.username").value("tester")); + .andExpect(jsonPath("$.email").value("test2@test.com")) + .andExpect(jsonPath("$.username").value("tester2")); List all = userRepository.findAll(); assertEquals(2, all.size()); @@ -72,20 +90,63 @@ void testLoginUser() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isOk()) - .andExpect(jsonPath("$.id").exists()) - .andExpect(jsonPath("$.email").value("test@test.com")) - .andExpect(jsonPath("$.username").value("tester")); + .andExpect(jsonPath("$.accessToken").exists()) + .andExpect(jsonPath("$.refreshToken").exists()); } @Test void testGetUserInfo() throws Exception { // given - User first = userRepository.findAll().get(0); + User user = userRepository.findAll().get(0); + + // 로그인 + UserDto loginRequest = new UserDto(); + loginRequest.setEmail("test@test.com"); + loginRequest.setPassword("1234"); + + String loginResponse = mockMvc.perform(post("/api/users/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(loginRequest))) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + TokenResponseDto tokens = objectMapper.readValue(loginResponse, TokenResponseDto.class); + String accessToken = tokens.getAccessToken(); // when & then - mockMvc.perform(get("/api/users/" + first.getId())) + mockMvc.perform(get("/api/users/" + user.getId()) + .header("Authorization", "Bearer " + accessToken)) .andExpect(status().isOk()) .andExpect(jsonPath("$.email").value("test@test.com")) .andExpect(jsonPath("$.username").value("tester")); } + + @Test + void testRefreshToken() throws Exception { + //given + UserDto loginRequest = new UserDto(); + loginRequest.setEmail("test@test.com"); + loginRequest.setPassword("1234"); + + //when + String loginResponse = mockMvc.perform(post("/api/users/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(loginRequest))) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + TokenResponseDto tokens = objectMapper.readValue(loginResponse, TokenResponseDto.class); + String refreshToken = tokens.getRefreshToken(); + + //given - accesstoken 재발급 + TokenResponseDto refreshRequest = new TokenResponseDto("", refreshToken); + + // then + mockMvc.perform(post("/api/users/refresh") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(refreshRequest))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.accessToken").exists()) + .andExpect(jsonPath("$.refreshToken").value(refreshToken)); + } } diff --git a/application.yml b/application.yml index de50b8c..37d8925 100644 --- a/application.yml +++ b/application.yml @@ -17,3 +17,8 @@ spring: console: enabled: true path: /h2-console + +jwt: + secret-key: "${JWT_SECRET_KEY}" + access-token-validity-in-seconds: 600 + refresh-token-validity-in-seconds: 1209600 diff --git a/common/build.gradle b/common/build.gradle index ac2939b..7a118a0 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -1,7 +1,18 @@ plugins { id 'java' + id 'org.springframework.boot' version '3.3.4' + id 'io.spring.dependency-management' version '1.1.6' } dependencies { + // Spring Security + implementation 'org.springframework.boot:spring-boot-starter-security' + // JWT 설정 + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + implementation 'org.springframework.boot:spring-boot-starter-web' + testImplementation 'org.springframework.boot:spring-boot-starter-test' } \ No newline at end of file diff --git a/common/src/main/java/com/whalewatch/config/JwtAuthentication.java b/common/src/main/java/com/whalewatch/config/JwtAuthentication.java new file mode 100644 index 0000000..4b97d0c --- /dev/null +++ b/common/src/main/java/com/whalewatch/config/JwtAuthentication.java @@ -0,0 +1,50 @@ +package com.whalewatch.config; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +public class JwtAuthentication extends OncePerRequestFilter { + + private final JwtTokenProvider tokenProvider; + + public JwtAuthentication(JwtTokenProvider tokenProvider) { + this.tokenProvider = tokenProvider; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) + throws IOException, jakarta.servlet.ServletException { + + String jwt = resolveToken(request); + + if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { + String email = tokenProvider.getEmailFromToken(jwt); + + // DB 조회 후 권한 설정 가능 + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(email, null, null); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + filterChain.doFilter(request, response); + } + + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + // "Authorization: Bearer <토큰값>" 형태일 때 추출 + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } +} diff --git a/common/src/main/java/com/whalewatch/config/JwtTokenProvider.java b/common/src/main/java/com/whalewatch/config/JwtTokenProvider.java new file mode 100644 index 0000000..23e247a --- /dev/null +++ b/common/src/main/java/com/whalewatch/config/JwtTokenProvider.java @@ -0,0 +1,74 @@ +package com.whalewatch.config; + +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Date; + +@Component +public class JwtTokenProvider { + @Value("${jwt.secret-key}") + private String secretKey; + + @Value("${jwt.access-token-validity-in-seconds}") + private long accessTokenValidity; + + @Value("${jwt.refresh-token-validity-in-seconds}") + private long refreshTokenValidity; + + public JwtTokenProvider() { + } + + // Access Token 생성 + public String generateAccessToken(String email) { + Date now = new Date(); + Date expiry = new Date(now.getTime() + accessTokenValidity * 1000); + + return Jwts.builder() + .setSubject(email) + .setIssuedAt(now) + .setExpiration(expiry) + .signWith(SignatureAlgorithm.HS256, secretKey) + .compact(); + } + + // Refresh Token 생성 + public String generateRefreshToken(String email) { + Date now = new Date(); + Date expiry = new Date(now.getTime() + refreshTokenValidity * 1000); + + return Jwts.builder() + .setSubject(email) + .setIssuedAt(now) + .setExpiration(expiry) + .signWith(SignatureAlgorithm.HS256, secretKey) + .compact(); + } + + // 토큰에서 Subject 추출 + public String getEmailFromToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .getBody() + .getSubject(); + } + + // 토큰 유효성 검증 + public boolean validateToken(String token) { + try { + Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token); + return true; + } catch (JwtException | IllegalArgumentException e) { + // 유효하지 않은 토큰 + } + return false; + } +} diff --git a/common/src/main/java/com/whalewatch/config/SecurityConfig.java b/common/src/main/java/com/whalewatch/config/SecurityConfig.java new file mode 100644 index 0000000..7b7e788 --- /dev/null +++ b/common/src/main/java/com/whalewatch/config/SecurityConfig.java @@ -0,0 +1,49 @@ +package com.whalewatch.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +public class SecurityConfig { + + private final JwtTokenProvider tokenProvider; + + @Autowired + public SecurityConfig(JwtTokenProvider tokenProvider) { + this.tokenProvider = tokenProvider; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + // Spring Security 설정 + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf().disable() + // 세션 사용 안 함 + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + // URL별 권한 설정 + .authorizeHttpRequests(auth -> auth + .requestMatchers("/h2-console/**").permitAll() + .requestMatchers("/api/users", "/api/users/login").permitAll() // 회원가입 및 로그인 허용 + .requestMatchers("/api/users/**").authenticated() + .anyRequest().authenticated() + ) + // JWT 필터 추가 + .addFilterBefore(new JwtAuthentication(tokenProvider), + UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} diff --git a/common/src/main/java/com/whalewatch/dto/TokenResponseDto.java b/common/src/main/java/com/whalewatch/dto/TokenResponseDto.java new file mode 100644 index 0000000..df159b8 --- /dev/null +++ b/common/src/main/java/com/whalewatch/dto/TokenResponseDto.java @@ -0,0 +1,26 @@ +package com.whalewatch.dto; + +public class TokenResponseDto { + private String accessToken; + private String refreshToken; + + public TokenResponseDto() {} + + public TokenResponseDto(String accessToken, String refreshToken) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } + + public String getAccessToken() { + return accessToken; + } + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + public String getRefreshToken() { + return refreshToken; + } + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } +} diff --git a/service/build.gradle b/service/build.gradle index e7739d0..6b4476f 100644 --- a/service/build.gradle +++ b/service/build.gradle @@ -17,5 +17,8 @@ dependencies { implementation 'org.mapstruct:mapstruct:1.5.5.Final' annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final' + //Security + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.boot:spring-boot-starter-test' } \ No newline at end of file diff --git a/service/src/main/java/com/whalewatch/domain/JwtToken.java b/service/src/main/java/com/whalewatch/domain/JwtToken.java new file mode 100644 index 0000000..f302c2d --- /dev/null +++ b/service/src/main/java/com/whalewatch/domain/JwtToken.java @@ -0,0 +1,35 @@ +package com.whalewatch.domain; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "jwt_tokens") +public class JwtToken { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String token; + private String email; + private LocalDateTime expiry; + + protected JwtToken() {} + + public JwtToken(String token, String email, LocalDateTime expiry) { + this.token = token; + this.email = email; + this.expiry = expiry; + } + + public Long getId() { return id; } + public String getToken() { return token; } + public String getEmail() { return email; } + public LocalDateTime getExpiry() { return expiry; } + + public void setId(Long id) { this.id = id; } + public void setToken(String token) { this.token = token; } + public void setEmail(String email) { this.email = email; } + public void setExpiry(LocalDateTime expiry) { this.expiry = expiry; } +} diff --git a/service/src/main/java/com/whalewatch/repository/JwtTokenRepository.java b/service/src/main/java/com/whalewatch/repository/JwtTokenRepository.java new file mode 100644 index 0000000..80863d7 --- /dev/null +++ b/service/src/main/java/com/whalewatch/repository/JwtTokenRepository.java @@ -0,0 +1,11 @@ +package com.whalewatch.repository; + +import com.whalewatch.domain.JwtToken; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface JwtTokenRepository extends JpaRepository { + Optional findByToken(String token); + void deleteByEmail(String email); +} diff --git a/service/src/main/java/com/whalewatch/service/JwtService.java b/service/src/main/java/com/whalewatch/service/JwtService.java new file mode 100644 index 0000000..d76a14d --- /dev/null +++ b/service/src/main/java/com/whalewatch/service/JwtService.java @@ -0,0 +1,77 @@ +package com.whalewatch.service; + +import com.whalewatch.config.JwtTokenProvider; +import com.whalewatch.domain.JwtToken; +import com.whalewatch.domain.User; +import com.whalewatch.dto.TokenResponseDto; +import com.whalewatch.repository.JwtTokenRepository; +import com.whalewatch.repository.UserRepository; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; + +@Service +public class JwtService { + private final UserRepository userRepository; + private final JwtTokenRepository jwtTokenRepository; + private final JwtTokenProvider tokenProvider; + private final PasswordEncoder passwordEncoder; + + public JwtService(UserRepository userRepository, + JwtTokenRepository jwtTokenRepository, + JwtTokenProvider tokenProvider, + PasswordEncoder passwordEncoder) { + this.userRepository = userRepository; + this.jwtTokenRepository = jwtTokenRepository; + this.tokenProvider = tokenProvider; + this.passwordEncoder = passwordEncoder; + } + + public TokenResponseDto login(String email, String Password) { + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new RuntimeException("Invalid email or password")); + + if (!passwordEncoder.matches(Password, user.getPassword())) { + throw new RuntimeException("Invalid password"); + } + + String accessToken = tokenProvider.generateAccessToken(user.getEmail()); + String refreshToken = tokenProvider.generateRefreshToken(user.getEmail()); + + //기존 refreshToken 있으면 제거 + jwtTokenRepository.deleteByEmail(user.getEmail()); + + // refreshToken 저장 + JwtToken refreshTokenEntity = new JwtToken( + refreshToken, + user.getEmail(), + LocalDateTime.now().plusSeconds(1209600) + ); + jwtTokenRepository.save(refreshTokenEntity); + + return new TokenResponseDto(accessToken, refreshToken); + } + + public TokenResponseDto refreshAccessToken(String refreshToken) { + JwtToken stored = jwtTokenRepository.findByToken(refreshToken) + .orElseThrow(() -> new RuntimeException("Invalid token")); + + if (stored.getExpiry().isBefore(LocalDateTime.now())) { + jwtTokenRepository.delete(stored); + throw new RuntimeException("Token expired."); + } + + // RefreshToken 자체가 유효한지 + if (!tokenProvider.validateToken(refreshToken)) { + jwtTokenRepository.delete(stored); + throw new RuntimeException("Invalid token signature."); + } + + // 새 Access Token 발급 + String email = tokenProvider.getEmailFromToken(refreshToken); + String newAccessToken = tokenProvider.generateAccessToken(email); + + return new TokenResponseDto(newAccessToken, refreshToken); + } +} diff --git a/service/src/main/java/com/whalewatch/service/UserService.java b/service/src/main/java/com/whalewatch/service/UserService.java index d304e48..d7c05fd 100644 --- a/service/src/main/java/com/whalewatch/service/UserService.java +++ b/service/src/main/java/com/whalewatch/service/UserService.java @@ -2,23 +2,25 @@ import com.whalewatch.domain.User; import com.whalewatch.repository.UserRepository; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @Service public class UserService { private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; - public UserService(UserRepository userRepository) { + public UserService(UserRepository userRepository, + PasswordEncoder passwordEncoder) { this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; } public User registerUser(User user) { - return userRepository.save(user); - } + String hashed = passwordEncoder.encode(user.getPassword()); + user.setPassword(hashed); - public User loginUser(String email,String password) { - return userRepository.findByEmailAndPassword(email, password) - .orElseThrow(() -> new RuntimeException("Invalid email or password")); + return userRepository.save(user); } public User getUserInfo(int id) { diff --git a/service/src/test/java/com/whalewatch/service/JwtServiceTest.java b/service/src/test/java/com/whalewatch/service/JwtServiceTest.java new file mode 100644 index 0000000..ad31f6e --- /dev/null +++ b/service/src/test/java/com/whalewatch/service/JwtServiceTest.java @@ -0,0 +1,91 @@ +package com.whalewatch.service; + +import com.whalewatch.config.JwtTokenProvider; +import com.whalewatch.domain.JwtToken; +import com.whalewatch.domain.User; +import com.whalewatch.dto.TokenResponseDto; +import com.whalewatch.repository.JwtTokenRepository; +import com.whalewatch.repository.UserRepository; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.time.LocalDateTime; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +@ExtendWith(SpringExtension.class) +public class JwtServiceTest { + + @Mock + private UserRepository userRepository; + @Mock + private JwtTokenRepository refreshTokenRepository; + @Mock + private JwtTokenProvider jwtTokenProvider; + @Mock + private PasswordEncoder passwordEncoder; + + @InjectMocks + private JwtService jwtService; + + @Test + void loginSuccess() { + // given + String email = "test@test.com"; + String Password = "test"; + String encodedPassword = "$2a$10$ABCD123..."; // bcrypt 해싱된 값 가정 + User user = new User(email, "logtester", encodedPassword); + + given(userRepository.findByEmail(email)).willReturn(Optional.of(user)); + // 비밀번호 매칭 + given(passwordEncoder.matches(Password, encodedPassword)).willReturn(true); + + // JWT 생성 + given(jwtTokenProvider.generateAccessToken(email)).willReturn("access-token"); + given(jwtTokenProvider.generateRefreshToken(email)).willReturn("refresh-token"); + + // when + TokenResponseDto result = jwtService.login(email, Password); + + // then + assertNotNull(result); + assertEquals("access-token", result.getAccessToken()); + assertEquals("refresh-token", result.getRefreshToken()); + verify(refreshTokenRepository).deleteByEmail(email); + verify(refreshTokenRepository).save(any(JwtToken.class)); + } + + @Test + void refreshAccessTokenSuccess() { + // given + String refreshToken = "valid-refresh-token"; + JwtToken stored = new JwtToken(refreshToken, "refresh@test.com", LocalDateTime.now().plusDays(1)); + given(refreshTokenRepository.findByToken(refreshToken)).willReturn(Optional.of(stored)); + + // 토큰 서명/만료 검증 + given(jwtTokenProvider.validateToken(refreshToken)).willReturn(true); + + // 토큰에서 이메일 추출 + given(jwtTokenProvider.getEmailFromToken(refreshToken)).willReturn("refresh@test.com"); + + // 새 Access Token 발급 + given(jwtTokenProvider.generateAccessToken("refresh@test.com")).willReturn("new-access-token"); + + // when + TokenResponseDto result = jwtService.refreshAccessToken(refreshToken); + + // then + assertNotNull(result); + assertEquals("new-access-token", result.getAccessToken()); + assertEquals("valid-refresh-token", result.getRefreshToken()); + } +} diff --git a/service/src/test/java/com/whalewatch/service/UserServiceTest.java b/service/src/test/java/com/whalewatch/service/UserServiceTest.java index e8a8720..192b103 100644 --- a/service/src/test/java/com/whalewatch/service/UserServiceTest.java +++ b/service/src/test/java/com/whalewatch/service/UserServiceTest.java @@ -39,27 +39,6 @@ void registerUser() { assertEquals("1234", result.getPassword()); // 비밀번호 검증 } - @Test - void loginUser() { - // given - String email = "login@test.com"; - String password = "1234"; - User user = new User(email, "logtester", password); - - given(userRepository.findByEmailAndPassword(email, password)) - .willReturn(Optional.of(user)); - - // when - User result = userService.loginUser(email, password); - - // then - assertNotNull(result); // 반환값이 null이 아님을 확인 - assertEquals(email, result.getEmail()); // 이메일 검증 - assertEquals("logtester", result.getUsername()); // 이름 검증 - assertEquals(password, result.getPassword()); // 비밀번호 검증 - } - - @Test void getUserInfo() { // given