diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index d0aea34..7a8dedf 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -73,4 +73,5 @@ jobs: sudo docker compose stop app sudo docker compose rm -f app sudo docker rmi ${{env.DOCKER_IMAGE_NAME}}:latest + sudo docker compose pull app sudo docker compose up -d app diff --git a/src/main/java/com/dnd/moddo/ModdoApplication.java b/src/main/java/com/dnd/moddo/ModdoApplication.java index 25ebbc2..3923d8d 100644 --- a/src/main/java/com/dnd/moddo/ModdoApplication.java +++ b/src/main/java/com/dnd/moddo/ModdoApplication.java @@ -3,8 +3,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; @SpringBootApplication(exclude = SecurityAutoConfiguration.class) +@ConfigurationPropertiesScan public class ModdoApplication { public static void main(String[] args) { diff --git a/src/main/java/com/dnd/moddo/domain/auth/controller/AuthController.java b/src/main/java/com/dnd/moddo/domain/auth/controller/AuthController.java index 3486865..4aed35f 100644 --- a/src/main/java/com/dnd/moddo/domain/auth/controller/AuthController.java +++ b/src/main/java/com/dnd/moddo/domain/auth/controller/AuthController.java @@ -1,9 +1,12 @@ package com.dnd.moddo.domain.auth.controller; -import org.springframework.boot.context.properties.EnableConfigurationProperties; +import java.util.Collections; + import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestHeader; @@ -11,31 +14,30 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import com.dnd.moddo.domain.auth.dto.KakaoTokenResponse; import com.dnd.moddo.domain.auth.service.AuthService; -import com.dnd.moddo.domain.auth.service.KakaoClient; import com.dnd.moddo.domain.auth.service.RefreshTokenService; +import com.dnd.moddo.global.config.CookieProperties; import com.dnd.moddo.global.jwt.dto.RefreshResponse; import com.dnd.moddo.global.jwt.dto.TokenResponse; -import com.dnd.moddo.global.jwt.properties.CookieProperties; +import com.dnd.moddo.global.jwt.service.JwtService; import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @RestController -@EnableConfigurationProperties(CookieProperties.class) +@Validated @RequestMapping("/api/v1") public class AuthController { private final AuthService authService; private final RefreshTokenService refreshTokenService; - private final KakaoClient kakaoClient; private final CookieProperties cookieProperties; + private final JwtService jwtService; @GetMapping("/user/guest/token") public ResponseEntity getGuestToken() { - TokenResponse tokenResponse = authService.createGuestUser(); + TokenResponse tokenResponse = authService.loginWithGuest(); String cookie = createCookie("accessToken", tokenResponse.accessToken()).toString(); @@ -50,10 +52,9 @@ public RefreshResponse reissueAccessToken(@RequestHeader(value = "Authorization" } @GetMapping("/login/oauth2/callback") - public ResponseEntity kakaoLoginCallback(@RequestParam String code) { - KakaoTokenResponse kakaoTokenResponse = kakaoClient.join(code); + public ResponseEntity kakaoLoginCallback(@RequestParam @NotBlank String code) { - TokenResponse tokenResponse = authService.getOrCreateKakaoUserToken(kakaoTokenResponse.access_token()); + TokenResponse tokenResponse = authService.loginOrRegisterWithKakao(code); String cookie = createCookie("accessToken", tokenResponse.accessToken()).toString(); @@ -63,12 +64,13 @@ public ResponseEntity kakaoLoginCallback(@RequestParam String code) { } @GetMapping("/logout") - public ResponseEntity kakaoLogout() { + public ResponseEntity kakaoLogout(@CookieValue(value = "accessToken") String token) { String cookie = expireCookie("accessToken").toString(); - + Long userId = jwtService.getUserId(token); + authService.logout(userId); return ResponseEntity.ok() .header(HttpHeaders.SET_COOKIE, cookie) - .build(); + .body(Collections.singletonMap("message", "Logout successful")); } private ResponseCookie createCookie(String name, String key) { @@ -76,7 +78,6 @@ private ResponseCookie createCookie(String name, String key) { .httpOnly(cookieProperties.httpOnly()) .secure(cookieProperties.secure()) .path(cookieProperties.path()) - .domain(cookieProperties.domain()) .sameSite(cookieProperties.sameSite()) .maxAge(cookieProperties.maxAge()) .build(); @@ -88,9 +89,9 @@ private ResponseCookie expireCookie(String name) { .httpOnly(cookieProperties.httpOnly()) .secure(cookieProperties.secure()) .path(cookieProperties.path()) - .domain(cookieProperties.domain()) .sameSite(cookieProperties.sameSite()) .maxAge(0L) .build(); } + } \ No newline at end of file diff --git a/src/main/java/com/dnd/moddo/domain/auth/dto/KakaoLogoutResponse.java b/src/main/java/com/dnd/moddo/domain/auth/dto/KakaoLogoutResponse.java new file mode 100644 index 0000000..14fa575 --- /dev/null +++ b/src/main/java/com/dnd/moddo/domain/auth/dto/KakaoLogoutResponse.java @@ -0,0 +1,4 @@ +package com.dnd.moddo.domain.auth.dto; + +public record KakaoLogoutResponse(Long id) { +} diff --git a/src/main/java/com/dnd/moddo/domain/auth/dto/KakaoProfile.java b/src/main/java/com/dnd/moddo/domain/auth/dto/KakaoProfile.java index 0f61a89..2cf1866 100644 --- a/src/main/java/com/dnd/moddo/domain/auth/dto/KakaoProfile.java +++ b/src/main/java/com/dnd/moddo/domain/auth/dto/KakaoProfile.java @@ -1,36 +1,25 @@ package com.dnd.moddo.domain.auth.dto; +import com.fasterxml.jackson.annotation.JsonProperty; + public record KakaoProfile( Long id, - String connected_at, - Properties properties, - KakaoAccount kakao_account + @JsonProperty("kakao_account") KakaoAccount kakaoAccount, + Properties properties ) { public record Properties( - String nickname, - String profile_image, - String thumbnail_image + String nickname ) { } public record KakaoAccount( - Boolean profile_nickname_needs_agreement, - Boolean profile_image_needs_agreement, - Profile profile, - Boolean has_email, - Boolean email_needs_agreement, - Boolean is_email_valid, - Boolean is_email_verified, - String email + String email, + Profile profile ) { } public record Profile( - String nickname, - String thumbnail_image_url, - String profile_image_url, - Boolean is_default_image, - Boolean is_default_nickname + String nickname ) { } -} +} \ No newline at end of file diff --git a/src/main/java/com/dnd/moddo/domain/auth/dto/KakaoTokenResponse.java b/src/main/java/com/dnd/moddo/domain/auth/dto/KakaoTokenResponse.java index c1d45b5..7e19764 100644 --- a/src/main/java/com/dnd/moddo/domain/auth/dto/KakaoTokenResponse.java +++ b/src/main/java/com/dnd/moddo/domain/auth/dto/KakaoTokenResponse.java @@ -1,11 +1,9 @@ package com.dnd.moddo.domain.auth.dto; +import com.fasterxml.jackson.annotation.JsonProperty; + public record KakaoTokenResponse( - String access_token, - String token_type, - String refresh_token, - int expires_in, - String scope, - int refresh_token_expires_in + @JsonProperty("access_token") String accessToken, + @JsonProperty("expires_in") int expiresIn ) { } \ No newline at end of file diff --git a/src/main/java/com/dnd/moddo/domain/auth/service/AuthService.java b/src/main/java/com/dnd/moddo/domain/auth/service/AuthService.java index 82cbf1e..8aba9c9 100644 --- a/src/main/java/com/dnd/moddo/domain/auth/service/AuthService.java +++ b/src/main/java/com/dnd/moddo/domain/auth/service/AuthService.java @@ -1,16 +1,20 @@ package com.dnd.moddo.domain.auth.service; -import java.time.LocalDateTime; import java.util.UUID; -import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.dnd.moddo.domain.auth.dto.KakaoLogoutResponse; import com.dnd.moddo.domain.auth.dto.KakaoProfile; +import com.dnd.moddo.domain.auth.dto.KakaoTokenResponse; +import com.dnd.moddo.domain.user.dto.request.GuestUserSaveRequest; +import com.dnd.moddo.domain.user.dto.request.UserSaveRequest; import com.dnd.moddo.domain.user.entity.User; -import com.dnd.moddo.domain.user.entity.type.Authority; -import com.dnd.moddo.domain.user.repository.UserRepository; +import com.dnd.moddo.domain.user.service.CommandUserService; +import com.dnd.moddo.domain.user.service.QueryUserService; +import com.dnd.moddo.global.exception.ModdoException; import com.dnd.moddo.global.jwt.dto.TokenResponse; import com.dnd.moddo.global.jwt.utill.JwtProvider; @@ -22,54 +26,52 @@ @Slf4j public class AuthService { - private final UserRepository userRepository; + private final CommandUserService commandUserService; + private final QueryUserService queryUserService; private final JwtProvider jwtProvider; private final KakaoClient kakaoClient; - @Value("${kakao.auth.client_id}") - String client_id; - - @Value("${kakao.auth.redirect_uri}") - String redirect_uri; - @Transactional - public TokenResponse createGuestUser() { + public TokenResponse loginWithGuest() { String guestEmail = "guest-" + UUID.randomUUID() + "@guest.com"; + GuestUserSaveRequest request = new GuestUserSaveRequest(guestEmail, "Guest"); - User guestUser = createUser(guestEmail, "Guest", false); + User user = commandUserService.createGuestUser(request); - return jwtProvider.generateToken(guestUser.getId(), guestUser.getEmail(), guestUser.getAuthority().toString(), - guestUser.getIsMember()); - } - - private User createUser(String email, String name, boolean isMember) { - User user = User.builder() - .email(email) - .name(name) - .profile(null) - .createdAt(LocalDateTime.now()) - .expiredAt(LocalDateTime.now().plusMonths(1)) - .authority(Authority.USER) - .isMember(isMember) - .build(); - - return userRepository.save(user); + return jwtProvider.generateToken(user); } @Transactional - public TokenResponse getOrCreateKakaoUserToken(String token) { - KakaoProfile kakaoProfile = kakaoClient.getKakaoProfile(token); + public TokenResponse loginOrRegisterWithKakao(String code) { + KakaoTokenResponse tokenResponse = kakaoClient.join(code); + KakaoProfile kakaoProfile = kakaoClient.getKakaoProfile(tokenResponse.accessToken()); - String email = kakaoProfile.kakao_account().email(); + String email = kakaoProfile.kakaoAccount().email(); String nickname = kakaoProfile.properties().nickname(); + Long kakaoId = kakaoProfile.id(); + + if (email == null || nickname == null || kakaoId == null) { + throw new ModdoException(HttpStatus.BAD_REQUEST, "카카오 프로필 정보가 누락되었습니다."); + } + + UserSaveRequest request = new UserSaveRequest(email, nickname, kakaoId); + User user = commandUserService.getOrCreateUser(request); + + log.info("[USER_LOGIN] 로그인 성공 : code = {}, kakaoId = {}, nickname = {}", code, kakaoId, nickname); + + return jwtProvider.generateToken(user); + } - User kakaoUser = userRepository.findByEmail(email) - .orElseGet(() -> createUser(email, nickname, true)); + public void logout(Long userId) { + queryUserService.findKakaoIdById(userId).ifPresent(kakaoId -> { + KakaoLogoutResponse logoutResponse = kakaoClient.logout(kakaoId); - log.info("[USER_LOGIN] 로그인 성공 : email={}, name={}", kakaoUser.getEmail(), kakaoUser.getName()); + if (!kakaoId.equals(logoutResponse.id())) { + throw new ModdoException(HttpStatus.INTERNAL_SERVER_ERROR, "카카오 로그아웃 실패: id 불일치"); + } - return jwtProvider.generateToken(kakaoUser.getId(), kakaoUser.getEmail(), kakaoUser.getAuthority().toString(), - kakaoUser.getIsMember()); + log.info("[USER_LOGOUT] 카카오 로그아웃 성공: userId={}, kakaoId={}", userId, kakaoId); + }); } } \ No newline at end of file diff --git a/src/main/java/com/dnd/moddo/domain/auth/service/KakaoClient.java b/src/main/java/com/dnd/moddo/domain/auth/service/KakaoClient.java index aa845b9..f846bd6 100644 --- a/src/main/java/com/dnd/moddo/domain/auth/service/KakaoClient.java +++ b/src/main/java/com/dnd/moddo/domain/auth/service/KakaoClient.java @@ -1,6 +1,5 @@ package com.dnd.moddo.domain.auth.service; -import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -10,8 +9,10 @@ import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClientResponseException; +import com.dnd.moddo.domain.auth.dto.KakaoLogoutResponse; import com.dnd.moddo.domain.auth.dto.KakaoProfile; import com.dnd.moddo.domain.auth.dto.KakaoTokenResponse; +import com.dnd.moddo.global.config.KakaoProperties; import com.dnd.moddo.global.exception.ModdoException; import lombok.RequiredArgsConstructor; @@ -21,30 +22,24 @@ @Slf4j @Component public class KakaoClient { - - @Value("${kakao.auth.client_id}") - String client_id; - - @Value("${kakao.auth.redirect_uri}") - String redirect_uri; - + private final KakaoProperties kakaoProperties; private final RestClient.Builder builder; public KakaoTokenResponse join(String code) { RestClient restClient = builder.build(); - String uri = "https://kauth.kakao.com/oauth/token"; + String uri = kakaoProperties.tokenRequestUri(); MultiValueMap params = new LinkedMultiValueMap<>(); params.add("grant_type", "authorization_code"); - params.add("client_id", client_id); - params.add("redirect_uri", redirect_uri); + params.add("client_id", kakaoProperties.clientId()); + params.add("redirect_uri", kakaoProperties.redirectUri()); params.add("code", code); try { return restClient.post() .uri(uri) - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) .body(params) .retrieve() .body(KakaoTokenResponse.class); @@ -62,13 +57,12 @@ public KakaoTokenResponse join(String code) { public KakaoProfile getKakaoProfile(String token) { RestClient restClient = builder.build(); - String uri = "https://kapi.kakao.com/v2/user/me"; + String uri = kakaoProperties.profileRequestUri(); try { return restClient.get() .uri(uri) .header("Authorization", "Bearer " + token) - .header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8") .retrieve() .body(KakaoProfile.class); @@ -81,4 +75,26 @@ public KakaoProfile getKakaoProfile(String token) { throw new ModdoException(HttpStatus.INTERNAL_SERVER_ERROR, "카카오 콜백 처리 실패"); } } + + public KakaoLogoutResponse logout(Long kakaoId) { + RestClient restClient = builder.build(); + String uri = kakaoProperties.logoutRequestUri(); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("target_id_type", "user_id"); + params.add("target_id", kakaoId.toString()); + + try { + return restClient.post() + .uri(uri) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .header(HttpHeaders.AUTHORIZATION, "KakaoAK " + kakaoProperties.adminKey()) + .body(params) + .retrieve() + .body(KakaoLogoutResponse.class); + } catch (Exception e) { + log.error("[KAKAO_CALLBACK_ERROR] 카카오 콜백 처리 실패", e.getMessage()); + throw new ModdoException(HttpStatus.INTERNAL_SERVER_ERROR, "카카오 콜백 처리 실패"); + } + } } diff --git a/src/main/java/com/dnd/moddo/domain/auth/service/RefreshTokenService.java b/src/main/java/com/dnd/moddo/domain/auth/service/RefreshTokenService.java index 47b2504..458e6ef 100644 --- a/src/main/java/com/dnd/moddo/domain/auth/service/RefreshTokenService.java +++ b/src/main/java/com/dnd/moddo/domain/auth/service/RefreshTokenService.java @@ -1,5 +1,7 @@ package com.dnd.moddo.domain.auth.service; +import org.springframework.stereotype.Service; + import com.dnd.moddo.domain.auth.exception.TokenInvalidException; import com.dnd.moddo.domain.user.entity.User; import com.dnd.moddo.domain.user.repository.UserRepository; @@ -7,32 +9,32 @@ import com.dnd.moddo.global.jwt.properties.JwtConstants; import com.dnd.moddo.global.jwt.utill.JwtProvider; import com.dnd.moddo.global.jwt.utill.JwtUtil; + import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; @RequiredArgsConstructor @Service public class RefreshTokenService { - private final JwtUtil jwtUtil; - private final UserRepository userRepository; - private final JwtProvider jwtProvider; + private final JwtUtil jwtUtil; + private final UserRepository userRepository; + private final JwtProvider jwtProvider; - public RefreshResponse execute(String token) { + public RefreshResponse execute(String token) { - String email; + String email; - try { - email = jwtUtil.getJwt(jwtUtil.parseToken(token)).getBody().get(JwtConstants.EMAIL.message).toString(); - } catch (Exception e) { - throw new TokenInvalidException(); - } + try { + email = jwtUtil.getJwt(jwtUtil.parseToken(token)).getBody().get(JwtConstants.EMAIL.message).toString(); + } catch (Exception e) { + throw new TokenInvalidException(); + } - User user = userRepository.getByEmail(email); - String newAccessToken = jwtProvider.generateAccessToken(user.getId(), user.getEmail(), user.getAuthority().toString()); + User user = userRepository.getByEmail(email); + String newAccessToken = jwtProvider.generateAccessToken(user.getId(), user.getAuthority().toString()); - return RefreshResponse.builder() - .accessToken(newAccessToken) - .build(); - } + return RefreshResponse.builder() + .accessToken(newAccessToken) + .build(); + } } diff --git a/src/main/java/com/dnd/moddo/domain/user/dto/request/GuestUserSaveRequest.java b/src/main/java/com/dnd/moddo/domain/user/dto/request/GuestUserSaveRequest.java new file mode 100644 index 0000000..a5f4b67 --- /dev/null +++ b/src/main/java/com/dnd/moddo/domain/user/dto/request/GuestUserSaveRequest.java @@ -0,0 +1,21 @@ +package com.dnd.moddo.domain.user.dto.request; + +import java.time.LocalDateTime; + +import com.dnd.moddo.domain.user.entity.User; +import com.dnd.moddo.domain.user.entity.type.Authority; + +public record GuestUserSaveRequest(String email, String name) { + public User toEntity() { + return User.builder() + .email(email) + .name(name) + .kakaoId(null) + .isMember(false) + .authority(Authority.USER) + .profile(null) + .createdAt(LocalDateTime.now()) + .expiredAt(LocalDateTime.now().plusMonths(1)) + .build(); + } +} diff --git a/src/main/java/com/dnd/moddo/domain/user/dto/request/UserSaveRequest.java b/src/main/java/com/dnd/moddo/domain/user/dto/request/UserSaveRequest.java new file mode 100644 index 0000000..0b91093 --- /dev/null +++ b/src/main/java/com/dnd/moddo/domain/user/dto/request/UserSaveRequest.java @@ -0,0 +1,25 @@ +package com.dnd.moddo.domain.user.dto.request; + +import java.time.LocalDateTime; + +import com.dnd.moddo.domain.user.entity.User; +import com.dnd.moddo.domain.user.entity.type.Authority; + +public record UserSaveRequest( + String email, + String name, + Long kakaoId +) { + public User toEntity() { + return User.builder() + .email(email) + .name(name) + .kakaoId(kakaoId) + .isMember(true) + .authority(Authority.USER) + .profile(null) + .createdAt(LocalDateTime.now()) + .expiredAt(LocalDateTime.now().plusMonths(1)) + .build(); + } +} diff --git a/src/main/java/com/dnd/moddo/domain/user/entity/User.java b/src/main/java/com/dnd/moddo/domain/user/entity/User.java index 8005d6d..88aa99c 100644 --- a/src/main/java/com/dnd/moddo/domain/user/entity/User.java +++ b/src/main/java/com/dnd/moddo/domain/user/entity/User.java @@ -1,45 +1,60 @@ package com.dnd.moddo.domain.user.entity; +import java.time.LocalDateTime; + import com.dnd.moddo.domain.user.entity.type.Authority; -import jakarta.persistence.*; -import lombok.*; -import java.time.LocalDateTime; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; @Entity @Getter @AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "users") -public class User{ +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + private String name; - private String name; + private String email; - private String email; + private String profile; - private String profile; + private Boolean isMember; - private Boolean isMember; + private Long kakaoId; - private LocalDateTime createdAt; + private LocalDateTime createdAt; - private LocalDateTime expiredAt; + private LocalDateTime expiredAt; - @Enumerated(EnumType.STRING) - private Authority authority; + @Enumerated(EnumType.STRING) + private Authority authority; - @Builder - public User(String name, String email, String profile, Boolean isMember, Authority authority, LocalDateTime createdAt, LocalDateTime expiredAt) { - this.name = name; - this.email = email; - this.profile = profile; - this.isMember = isMember; - this.createdAt = createdAt; - this.expiredAt = expiredAt; - this.authority = authority; - } + @Builder + public User(String name, String email, String profile, Boolean isMember, Authority authority, Long kakaoId, + LocalDateTime createdAt, LocalDateTime expiredAt) { + this.name = name; + this.email = email; + this.profile = profile; + this.isMember = isMember; + this.kakaoId = kakaoId; + this.createdAt = createdAt; + this.expiredAt = expiredAt; + this.authority = authority; + } } diff --git a/src/main/java/com/dnd/moddo/domain/user/repository/UserRepository.java b/src/main/java/com/dnd/moddo/domain/user/repository/UserRepository.java index d2788cb..65afb20 100644 --- a/src/main/java/com/dnd/moddo/domain/user/repository/UserRepository.java +++ b/src/main/java/com/dnd/moddo/domain/user/repository/UserRepository.java @@ -1,26 +1,31 @@ package com.dnd.moddo.domain.user.repository; +import java.util.Optional; -import com.dnd.moddo.domain.user.entity.User; -import com.dnd.moddo.domain.user.exception.UserNotFoundException; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; -import java.util.Optional; +import com.dnd.moddo.domain.user.entity.User; +import com.dnd.moddo.domain.user.exception.UserNotFoundException; @Repository public interface UserRepository extends JpaRepository { - Optional findByEmail(String email); + Optional findByEmail(String email); + + Optional findByKakaoId(Long kakaoId); - default User getByEmail(String email) { - return findByEmail(email) - .orElseThrow(() -> new UserNotFoundException(email)); - } + @Query("SELECT u.kakaoId FROM User u WHERE u.id = :userId") + Optional findKakaoIdById(Long userId); + default User getByEmail(String email) { + return findByEmail(email) + .orElseThrow(() -> new UserNotFoundException(email)); + } - default User getById(Long id) { - return findById(id) - .orElseThrow(() -> new UserNotFoundException(id)); - } + default User getById(Long id) { + return findById(id) + .orElseThrow(() -> new UserNotFoundException(id)); + } } diff --git a/src/main/java/com/dnd/moddo/domain/user/service/CommandUserService.java b/src/main/java/com/dnd/moddo/domain/user/service/CommandUserService.java new file mode 100644 index 0000000..bcafbb0 --- /dev/null +++ b/src/main/java/com/dnd/moddo/domain/user/service/CommandUserService.java @@ -0,0 +1,35 @@ +package com.dnd.moddo.domain.user.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.dnd.moddo.domain.user.dto.request.GuestUserSaveRequest; +import com.dnd.moddo.domain.user.dto.request.UserSaveRequest; +import com.dnd.moddo.domain.user.entity.User; +import com.dnd.moddo.domain.user.service.implementation.UserCreator; +import com.dnd.moddo.domain.user.service.implementation.UserReader; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Service +public class CommandUserService { + private final UserCreator userCreator; + private final UserReader userReader; + + @Transactional + public User createGuestUser(GuestUserSaveRequest request) { + return userCreator.createUser(request.toEntity()); + } + + @Transactional + public User createKakaoUser(UserSaveRequest request) { + return userCreator.createUser(request.toEntity()); + } + + @Transactional + public User getOrCreateUser(UserSaveRequest request) { + return userReader.findByKakaoId(request.kakaoId()) + .orElseGet(() -> createKakaoUser(request)); + } +} diff --git a/src/main/java/com/dnd/moddo/domain/user/service/QueryUserService.java b/src/main/java/com/dnd/moddo/domain/user/service/QueryUserService.java new file mode 100644 index 0000000..d127d6d --- /dev/null +++ b/src/main/java/com/dnd/moddo/domain/user/service/QueryUserService.java @@ -0,0 +1,20 @@ +package com.dnd.moddo.domain.user.service; + +import java.util.Optional; + +import org.springframework.stereotype.Service; + +import com.dnd.moddo.domain.user.service.implementation.UserReader; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Service +public class QueryUserService { + private final UserReader userReader; + + public Optional findKakaoIdById(Long userId) { + return userReader.findKakaoIdById(userId); + } +} + diff --git a/src/main/java/com/dnd/moddo/domain/user/service/implementation/UserCreator.java b/src/main/java/com/dnd/moddo/domain/user/service/implementation/UserCreator.java new file mode 100644 index 0000000..feb9fda --- /dev/null +++ b/src/main/java/com/dnd/moddo/domain/user/service/implementation/UserCreator.java @@ -0,0 +1,21 @@ +package com.dnd.moddo.domain.user.service.implementation; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.dnd.moddo.domain.user.entity.User; +import com.dnd.moddo.domain.user.repository.UserRepository; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Service +@Transactional +public class UserCreator { + + private final UserRepository userRepository; + + public User createUser(User user) { + return userRepository.save(user); + } +} diff --git a/src/main/java/com/dnd/moddo/domain/user/service/implementation/UserReader.java b/src/main/java/com/dnd/moddo/domain/user/service/implementation/UserReader.java new file mode 100644 index 0000000..3a5327d --- /dev/null +++ b/src/main/java/com/dnd/moddo/domain/user/service/implementation/UserReader.java @@ -0,0 +1,26 @@ +package com.dnd.moddo.domain.user.service.implementation; + +import java.util.Optional; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.dnd.moddo.domain.user.entity.User; +import com.dnd.moddo.domain.user.repository.UserRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class UserReader { + private final UserRepository userRepository; + + public Optional findByKakaoId(Long kakaoId) { + return userRepository.findByKakaoId(kakaoId); + } + + public Optional findKakaoIdById(Long userId) { + return userRepository.findKakaoIdById(userId); + } +} diff --git a/src/main/java/com/dnd/moddo/global/jwt/properties/CookieProperties.java b/src/main/java/com/dnd/moddo/global/config/CookieProperties.java similarity index 81% rename from src/main/java/com/dnd/moddo/global/jwt/properties/CookieProperties.java rename to src/main/java/com/dnd/moddo/global/config/CookieProperties.java index d511368..53ae9f6 100644 --- a/src/main/java/com/dnd/moddo/global/jwt/properties/CookieProperties.java +++ b/src/main/java/com/dnd/moddo/global/config/CookieProperties.java @@ -1,4 +1,4 @@ -package com.dnd.moddo.global.jwt.properties; +package com.dnd.moddo.global.config; import java.time.Duration; @@ -8,7 +8,6 @@ public record CookieProperties( boolean httpOnly, boolean secure, - String domain, String path, String sameSite, Duration maxAge diff --git a/src/main/java/com/dnd/moddo/global/config/KakaoProperties.java b/src/main/java/com/dnd/moddo/global/config/KakaoProperties.java new file mode 100644 index 0000000..cfe47d4 --- /dev/null +++ b/src/main/java/com/dnd/moddo/global/config/KakaoProperties.java @@ -0,0 +1,15 @@ +package com.dnd.moddo.global.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "kakao") +public record KakaoProperties( + String redirectUri, + String clientId, + String adminKey, + String tokenRequestUri, + String profileRequestUri, + String logoutRequestUri +) { + +} diff --git a/src/main/java/com/dnd/moddo/global/config/PropertiesConfig.java b/src/main/java/com/dnd/moddo/global/config/PropertiesConfig.java deleted file mode 100644 index 57019aa..0000000 --- a/src/main/java/com/dnd/moddo/global/config/PropertiesConfig.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.dnd.moddo.global.config; - -import com.dnd.moddo.global.jwt.properties.JwtProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -@Configuration -@EnableConfigurationProperties({JwtProperties.class}) -public class PropertiesConfig { -} \ No newline at end of file diff --git a/src/main/java/com/dnd/moddo/global/jwt/properties/JwtProperties.java b/src/main/java/com/dnd/moddo/global/jwt/properties/JwtProperties.java index fbf5440..edc07f7 100644 --- a/src/main/java/com/dnd/moddo/global/jwt/properties/JwtProperties.java +++ b/src/main/java/com/dnd/moddo/global/jwt/properties/JwtProperties.java @@ -1,27 +1,29 @@ package com.dnd.moddo.global.jwt.properties; +import javax.crypto.SecretKey; + +import org.springframework.boot.context.properties.ConfigurationProperties; + import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; import lombok.Getter; -import org.springframework.boot.context.properties.ConfigurationProperties; - -import javax.crypto.SecretKey; @Getter @ConfigurationProperties(prefix = "jwt") public class JwtProperties { - private final String header; - private final String prefix; - private final SecretKey secretKey; - private final Long accessExpiration; - private final Long refreshExpiration; + private final String header; + private final String prefix; + private final SecretKey secretKey; + private final Long accessExpiration; + private final Long refreshExpiration; - public JwtProperties(String header, String prefix, String secretKey, Long accessExpiration, Long refreshExpiration) { - this.header = header; - this.prefix = prefix; - this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey)); - this.accessExpiration = accessExpiration; - this.refreshExpiration = refreshExpiration; - } + public JwtProperties(String header, String prefix, String secretKey, Long accessExpiration, + Long refreshExpiration) { + this.header = header; + this.prefix = prefix; + this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey)); + this.accessExpiration = accessExpiration; + this.refreshExpiration = refreshExpiration; + } } diff --git a/src/main/java/com/dnd/moddo/global/jwt/service/JwtService.java b/src/main/java/com/dnd/moddo/global/jwt/service/JwtService.java index d4d04b9..8ca40c4 100644 --- a/src/main/java/com/dnd/moddo/global/jwt/service/JwtService.java +++ b/src/main/java/com/dnd/moddo/global/jwt/service/JwtService.java @@ -1,25 +1,31 @@ package com.dnd.moddo.global.jwt.service; +import org.springframework.stereotype.Service; + import com.dnd.moddo.global.jwt.utill.JwtUtil; + import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class JwtService { - private final JwtUtil jwtUtil; + private final JwtUtil jwtUtil; + + public Long getId(HttpServletRequest request, String key) { + String token = jwtUtil.resolveToken(request); + return jwtUtil.getIdFromToken(token, key); + } - public Long getId(HttpServletRequest request, String key) { - String token = jwtUtil.resolveToken(request); - return jwtUtil.getIdFromToken(token, key); - } + public Long getUserId(HttpServletRequest request) { + return getId(request, "userId"); + } - public Long getUserId(HttpServletRequest request) { - return getId(request, "userId"); - } + public Long getUserId(String token) { + return jwtUtil.getIdFromToken(token, "userId"); + } - public Long getGroupId(String groupToken) { - return jwtUtil.getIdFromToken(groupToken, "groupId"); - } + public Long getGroupId(String groupToken) { + return jwtUtil.getIdFromToken(groupToken, "groupId"); + } } diff --git a/src/main/java/com/dnd/moddo/global/jwt/utill/JwtProvider.java b/src/main/java/com/dnd/moddo/global/jwt/utill/JwtProvider.java index 96a69c4..eea1801 100644 --- a/src/main/java/com/dnd/moddo/global/jwt/utill/JwtProvider.java +++ b/src/main/java/com/dnd/moddo/global/jwt/utill/JwtProvider.java @@ -9,6 +9,7 @@ import org.springframework.stereotype.Component; +import com.dnd.moddo.domain.user.entity.User; import com.dnd.moddo.global.jwt.dto.TokenResponse; import com.dnd.moddo.global.jwt.properties.JwtProperties; @@ -22,14 +23,18 @@ public class JwtProvider { private final JwtProperties jwtProperties; - public String generateAccessToken(Long id, String email, String role) { - return generateToken(id, email, role, ACCESS_KEY.getMessage(), jwtProperties.getAccessExpiration()); + public String generateAccessToken(Long id, String role) { + return generateToken(id, role, ACCESS_KEY.getMessage(), jwtProperties.getAccessExpiration()); } - public TokenResponse generateToken(Long id, String email, String role, Boolean isMember) { - String accessToken = generateToken(id, email, role, ACCESS_KEY.getMessage(), + public TokenResponse generateToken(User user) { + return generateToken(user.getId(), user.getAuthority().toString(), user.getIsMember()); + } + + public TokenResponse generateToken(Long id, String role, Boolean isMember) { + String accessToken = generateToken(id, role, ACCESS_KEY.getMessage(), jwtProperties.getAccessExpiration()); - String refreshToken = generateToken(id, email, role, REFRESH_KEY.getMessage(), + String refreshToken = generateToken(id, role, REFRESH_KEY.getMessage(), jwtProperties.getRefreshExpiration()); return new TokenResponse(accessToken, refreshToken, getExpiredTime(), isMember); @@ -39,10 +44,9 @@ public String generateGroupToken(Long groupId) { return generateGroupToken(groupId, GROUP_KEY.getMessage()); } - private String generateToken(Long id, String email, String role, String type, Long exp) { + private String generateToken(Long id, String role, String type, Long exp) { return Jwts.builder() .claim(AUTH_ID.getMessage(), id) - .claim(EMAIL.getMessage(), email) .setHeaderParam(TYPE.message, type) .claim(ROLE.getMessage(), role) .signWith(jwtProperties.getSecretKey(), SignatureAlgorithm.HS256) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1560799..74d9a77 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -25,7 +25,13 @@ management: exposure: include: health,info,prometheus +kakao: + token-request-uri: https://kauth.kakao.com/oauth/token + profile-request-uri: https://kapi.kakao.com/v2/user/me + logout-request-uri: https://kapi.kakao.com/v1/user/logout + cookie: + secure: true http-only: false path: / same-site: none diff --git a/src/main/resources/config b/src/main/resources/config index 1ec2dcd..6515d7b 160000 --- a/src/main/resources/config +++ b/src/main/resources/config @@ -1 +1 @@ -Subproject commit 1ec2dcd3d9e19a35e2488ca6dc2946014c8b162c +Subproject commit 6515d7b8afc3d52cef1f35923ab9cf0429b9302e diff --git a/src/test/java/com/dnd/moddo/domain/auth/controller/AuthControllerTest.java b/src/test/java/com/dnd/moddo/domain/auth/controller/AuthControllerTest.java index ad55b20..3a440ac 100644 --- a/src/test/java/com/dnd/moddo/domain/auth/controller/AuthControllerTest.java +++ b/src/test/java/com/dnd/moddo/domain/auth/controller/AuthControllerTest.java @@ -1,6 +1,7 @@ package com.dnd.moddo.domain.auth.controller; import static org.mockito.BDDMockito.*; +import static org.springframework.restdocs.cookies.CookieDocumentation.*; import static org.springframework.restdocs.headers.HeaderDocumentation.*; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; @@ -18,6 +19,8 @@ import com.dnd.moddo.global.jwt.dto.TokenResponse; import com.dnd.moddo.global.util.RestDocsTestSupport; +import jakarta.servlet.http.Cookie; + class AuthControllerTest extends RestDocsTestSupport { @Test @@ -30,7 +33,7 @@ void getGuestToken() throws Exception { ZonedDateTime.now().plusDays(30), false ); - given(authService.createGuestUser()).willReturn(response); + given(authService.loginWithGuest()).willReturn(response); // when & then mockMvc.perform(get("/api/v1/user/guest/token")) @@ -79,14 +82,12 @@ void reissueAccessToken() throws Exception { @DisplayName("카카오에서 인가코드를 통해 토큰을 발급받아 사용자 정보를 가져와 등록시킨 뒤 엑세스 토큰을 발급하여 쿠키로 전달한다.") void kakaoLoginCallback() throws Exception { //given - KakaoTokenResponse kakaoTokenResponse = new KakaoTokenResponse("kakao-access-token", "bearer", - "kakao-refresh-token", - 3600, "profile", 7200); + KakaoTokenResponse kakaoTokenResponse = new KakaoTokenResponse("kakao-access-token", 3600); given(kakaoClient.join(anyString())).willReturn(kakaoTokenResponse); TokenResponse tokenResponse = new TokenResponse("access-token", "refresh-token", ZonedDateTime.now().plusMonths(1), true); - given(authService.getOrCreateKakaoUserToken(anyString())).willReturn(tokenResponse); + given(authService.loginOrRegisterWithKakao(anyString())).willReturn(tokenResponse); //when & then mockMvc.perform(get("/api/v1/login/oauth2/callback") @@ -101,4 +102,25 @@ void kakaoLoginCallback() throws Exception { ) )); } + + @Test + @DisplayName("액세스 토큰 쿠키를 통해 카카오 로그아웃을 성공적으로 수행한다.") + void kakaoLogout() throws Exception { + //given + given(jwtService.getUserId(anyString())).willReturn(1L); + doNothing().when(authService).logout(any()); + + //when & then + mockMvc.perform(get("/api/v1/logout") + .cookie(new Cookie("accessToken", "access-token"))) + .andExpect(status().isOk()) + .andDo(restDocs.document( + requestCookies( + cookieWithName("accessToken").description("액세스 토큰") + ), + responseFields( + fieldWithPath("message").type(JsonFieldType.STRING).description("로그아웃 성공 메시지") + ) + )); + } } diff --git a/src/test/java/com/dnd/moddo/domain/auth/service/AuthServiceTest.java b/src/test/java/com/dnd/moddo/domain/auth/service/AuthServiceTest.java index e574fa0..deb65e0 100644 --- a/src/test/java/com/dnd/moddo/domain/auth/service/AuthServiceTest.java +++ b/src/test/java/com/dnd/moddo/domain/auth/service/AuthServiceTest.java @@ -1,6 +1,7 @@ package com.dnd.moddo.domain.auth.service; import static com.dnd.moddo.global.support.UserTestFactory.*; +import static org.assertj.core.api.AssertionsForClassTypes.*; import static org.mockito.Mockito.*; import java.util.Optional; @@ -12,19 +13,24 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import com.dnd.moddo.domain.auth.dto.KakaoLogoutResponse; import com.dnd.moddo.domain.auth.dto.KakaoProfile; +import com.dnd.moddo.domain.auth.dto.KakaoTokenResponse; import com.dnd.moddo.domain.user.entity.User; -import com.dnd.moddo.domain.user.repository.UserRepository; +import com.dnd.moddo.domain.user.service.CommandUserService; +import com.dnd.moddo.domain.user.service.QueryUserService; import com.dnd.moddo.global.jwt.dto.TokenResponse; import com.dnd.moddo.global.jwt.utill.JwtProvider; @ExtendWith(MockitoExtension.class) public class AuthServiceTest { - @Mock - private UserRepository userRepository; @Mock private JwtProvider jwtProvider; @Mock + private CommandUserService commandUserService; + @Mock + private QueryUserService queryUserService; + @Mock private KakaoClient kakaoClient; @InjectMocks private AuthService authService; @@ -34,97 +40,83 @@ public class AuthServiceTest { void whenCreateGuestUser_thenSaveAndIssueToken() { //given User user = createGuestDefault(); - when(userRepository.save(any(User.class))).thenReturn(user); + when(commandUserService.createGuestUser(any())).thenReturn(user); //when - TokenResponse response = authService.createGuestUser(); + TokenResponse response = authService.loginWithGuest(); //then - verify(userRepository, times(1)).save(any(User.class)); + verify(jwtProvider, times(1)).generateToken(any()); + verify(commandUserService, times(1)).createGuestUser(any()); } - @DisplayName("기존 카카오 사용자가 로그인하면 토큰을 발급한다") + @DisplayName("카카오 사용자가 로그인하면 토큰을 발급한다") @Test void whenKakaoUserExists_thenTokenIsIssued() { //given String token = "test_token"; KakaoProfile kakaoProfile = new KakaoProfile( 12345L, - "2025.06.29T00:00:00", - new KakaoProfile.Properties( - "테스트유저", - "profile_image", - "thumbnail_image" - ), new KakaoProfile.KakaoAccount( - true, - true, + "test@example.com", new KakaoProfile.Profile( - "테스트 유저", - "thumbnail_image_url", - "profile_image_url", - true, - true - ), - true, - true, - true, - true, - "test@example.com" + "테스트 유저" + ) + ), + new KakaoProfile.Properties( + "테스트유저" ) ); - String email = kakaoProfile.kakao_account().email(); + KakaoTokenResponse kakaoTokenResponse = new KakaoTokenResponse("access-token", 3600); + String email = kakaoProfile.kakaoAccount().email(); User user = createWithEmail(email); + + when(kakaoClient.join(anyString())).thenReturn(kakaoTokenResponse); when(kakaoClient.getKakaoProfile(anyString())).thenReturn(kakaoProfile); - when(userRepository.findByEmail(anyString())).thenReturn(Optional.of(user)); + when(commandUserService.getOrCreateUser(any())).thenReturn(user); //when - TokenResponse response = authService.getOrCreateKakaoUserToken(token); + TokenResponse response = authService.loginOrRegisterWithKakao(token); //then - verify(jwtProvider, times(1)).generateToken(any(), anyString(), anyString(), anyBoolean()); + verify(jwtProvider, times(1)).generateToken(any()); } - @DisplayName("신규 카카오 사용자가 로그인하면 회원가입 후 토큰을 발급한다") + @DisplayName("카카오ID와 응답ID가 같을 때 카카오 로그아웃 성공한다.") @Test - void whenNewKakaoUser_thenRegisterAndIssueToken() { + void whenKakaoIdMatches_thenKakaoLogoutSuccess() { //given - String token = "test_token"; - KakaoProfile kakaoProfile = new KakaoProfile( - 12345L, - "2025.06.29T00:00:00", - new KakaoProfile.Properties( - "테스트유저", - "profile_image", - "thumbnail_image" - ), - new KakaoProfile.KakaoAccount( - true, - true, - new KakaoProfile.Profile( - "테스트 유저", - "thumbnail_image_url", - "profile_image_url", - true, - true - ), - true, - true, - true, - true, - "test@example.com" - ) - ); - String email = kakaoProfile.kakao_account().email(); - User user = createWithEmail(email); - - when(kakaoClient.getKakaoProfile(anyString())).thenReturn(kakaoProfile); - when(userRepository.findByEmail(anyString())).thenReturn(Optional.empty()); - when(userRepository.save(any(User.class))).thenReturn(user); - + Long kakaoId = 123456L; + when(queryUserService.findKakaoIdById(any())).thenReturn(Optional.of(kakaoId)); + when(kakaoClient.logout(any())).thenReturn(new KakaoLogoutResponse(kakaoId)); //when - TokenResponse response = authService.getOrCreateKakaoUserToken(token); + authService.logout(1L); + //then + verify(queryUserService, times(1)).findKakaoIdById(1L); + verify(kakaoClient, times(1)).logout(kakaoId); + } + @DisplayName("카카오ID가 null일 때 게스트 로그아웃 성공한다.") + @Test + void whenKakaoIdNull_thenNoAction() { + //given + when(queryUserService.findKakaoIdById(any())).thenReturn(Optional.empty()); + //when + authService.logout(1L); //then - verify(userRepository, times(1)).save(any(User.class)); - verify(jwtProvider, times(1)).generateToken(any(), anyString(), anyString(), anyBoolean()); + verify(queryUserService, times(1)).findKakaoIdById(1L); + verify(kakaoClient, times(0)).logout(any()); + } + + @DisplayName("카카오ID와 응답ID가 다를 때 예외 발생한다.") + @Test + void whenKakaoIdDiffers_thenThrowsException() { + //given + Long kakaoId = 123456L; + when(queryUserService.findKakaoIdById(any())).thenReturn(Optional.of(kakaoId)); + when(kakaoClient.logout(any())).thenReturn(new KakaoLogoutResponse(234567L)); + + //when & then + assertThatThrownBy(() -> authService.logout(1L)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("로그아웃 실패"); } } diff --git a/src/test/java/com/dnd/moddo/domain/auth/service/KakaoClientTest.java b/src/test/java/com/dnd/moddo/domain/auth/service/KakaoClientTest.java index ed85bf1..52c8bd7 100644 --- a/src/test/java/com/dnd/moddo/domain/auth/service/KakaoClientTest.java +++ b/src/test/java/com/dnd/moddo/domain/auth/service/KakaoClientTest.java @@ -4,12 +4,11 @@ import static org.springframework.test.web.client.match.MockRestRequestMatchers.*; import static org.springframework.test.web.client.response.MockRestResponseCreators.*; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -19,12 +18,15 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import com.dnd.moddo.domain.auth.dto.KakaoLogoutResponse; import com.dnd.moddo.domain.auth.dto.KakaoProfile; import com.dnd.moddo.domain.auth.dto.KakaoTokenResponse; +import com.dnd.moddo.global.config.KakaoProperties; import com.dnd.moddo.global.exception.ModdoException; @ExtendWith(SpringExtension.class) @RestClientTest(value = KakaoClient.class) +@EnableConfigurationProperties(KakaoProperties.class) public class KakaoClientTest { @Autowired private KakaoClient kakaoClient; @@ -32,16 +34,8 @@ public class KakaoClientTest { @Autowired private MockRestServiceServer mockServer; - @Value("${kakao.auth.client_id}") - private String clientId; - - @Value("${kakao.auth.redirect_uri}") - private String redirectUri; - - @AfterEach - void tearDown() { - mockServer.reset(); - } + @Autowired + private KakaoProperties kakaoProperties; @DisplayName("카카오 인가 코드로 토큰 요청하면 OauthToken을 반환한다") @Test @@ -52,21 +46,17 @@ void whenRequestKakaoAccessToken_thenReturnOauthToken() throws Exception { MultiValueMap params = new LinkedMultiValueMap<>(); params.add("code", "test_code"); params.add("grant_type", "authorization_code"); - params.add("client_id", clientId); - params.add("redirect_uri", redirectUri); + params.add("client_id", kakaoProperties.clientId()); + params.add("redirect_uri", kakaoProperties.redirectUri()); String expectResponse = """ { "access_token": "test_token", - "token_type": "bearer", - "refresh_token": "refresh_token", - "expires_in": 3600, - "scope": "profile", - "refresh_token_expires_in": 7200 + "expires_in": 3600 } """; - mockServer.expect(requestTo("https://kauth.kakao.com/oauth/token")) + mockServer.expect(requestTo(kakaoProperties.tokenRequestUri())) .andExpect(method(HttpMethod.POST)) .andExpect(header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")) .andExpect(content().formData(params)) @@ -76,7 +66,7 @@ void whenRequestKakaoAccessToken_thenReturnOauthToken() throws Exception { // then assertThat(result).isNotNull(); - assertThat(result.access_token()).isEqualTo("test_token"); + assertThat(result.accessToken()).isEqualTo("test_token"); } @DisplayName("잘못된 인가 코드로 토큰 요청 시 IllegalArgumentException이 발생한다") @@ -87,11 +77,11 @@ void whenRequestKakaoAccessTokenWithInvalidCode_thenThrowException() { MultiValueMap params = new LinkedMultiValueMap<>(); params.add("grant_type", "authorization_code"); - params.add("client_id", clientId); - params.add("redirect_uri", redirectUri); + params.add("client_id", kakaoProperties.clientId()); + params.add("redirect_uri", kakaoProperties.redirectUri()); params.add("code", "invalid_code"); - mockServer.expect(requestTo("https://kauth.kakao.com/oauth/token")) + mockServer.expect(requestTo(kakaoProperties.tokenRequestUri())) .andExpect(method(HttpMethod.POST)) .andExpect(header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")) .andExpect(content().formData(params)) @@ -112,34 +102,20 @@ void whenGetKakaoProfile_thenReturnKakaoProfile() { String expectResponse = """ { "id": 12345, - "connected_at": "2025.06.29T00:00:00", "properties": { - "nickname": "테스트유저", - "profile_image": "profile_image", - "thumbnail_image": "thumbnail_image" + "nickname": "테스트유저" }, "kakao_account": { - "profile_nickname_needs_agreement": true, - "profile_image_needs_agreement": true, + "email": "test@example.com", "profile": { - "nickname": "테스트 유저", - "thumbnail_image_url": "thumbnail_image_url", - "profile_image_url": "profile_image_url", - "is_default_image": true, - "is_default_nickname": true - }, - "has_email": true, - "email_needs_agreement": true, - "is_email_valid": true, - "is_email_verified": true, - "email": "test@example.com" + "nickname": "테스트 유저" + } } } """; - mockServer.expect(requestTo("https://kapi.kakao.com/v2/user/me")) + mockServer.expect(requestTo(kakaoProperties.profileRequestUri())) .andExpect(method(HttpMethod.GET)) - .andExpect(header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")) .andExpect(header("Authorization", "Bearer " + token)) .andRespond(withSuccess(expectResponse, MediaType.APPLICATION_JSON)); @@ -149,7 +125,7 @@ void whenGetKakaoProfile_thenReturnKakaoProfile() { // then assertThat(profile).isNotNull(); assertThat(profile.id()).isEqualTo(12345L); - assertThat(profile.kakao_account().email()).isEqualTo("test@example.com"); + assertThat(profile.kakaoAccount().email()).isEqualTo("test@example.com"); assertThat(profile.properties().nickname()).isEqualTo("테스트유저"); } @@ -159,9 +135,8 @@ void whenGetKakaoProfileWithHttpError_thenThrowException() { // given String token = "test_token"; - mockServer.expect(requestTo("https://kapi.kakao.com/v2/user/me")) + mockServer.expect(requestTo(kakaoProperties.profileRequestUri())) .andExpect(method(HttpMethod.GET)) - .andExpect(header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")) .andExpect(header("Authorization", "Bearer " + token)) .andRespond(withServerError()); @@ -169,4 +144,45 @@ void whenGetKakaoProfileWithHttpError_thenThrowException() { assertThatThrownBy(() -> kakaoClient.getKakaoProfile(token)) .isInstanceOf(ModdoException.class); } + + @DisplayName("카카오 로그아웃 API 호출 시 정상 응답을 반환한다") + @Test + void whenCallKakaoLogout_thenReturnValidResponse() { + //given + Long kakaoId = 123456L; + String expectResponse = """ + { + "id": 123456 + } + """; + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("target_id_type", "user_id"); + params.add("target_id", "123456"); + + mockServer.expect(requestTo(kakaoProperties.logoutRequestUri())) + .andExpect(method(HttpMethod.POST)) + .andExpect(header("Authorization", "KakaoAK " + kakaoProperties.adminKey())) + .andExpect(header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")) + .andRespond(withSuccess(expectResponse, MediaType.APPLICATION_JSON)); + //when + KakaoLogoutResponse response = kakaoClient.logout(123456L); + //then + assertThat(response.id()).isEqualTo(kakaoId); + } + + @DisplayName("카카오 로그아웃 API 호출 시 서버 오류가 발생하면 예외를 던진다") + @Test + void henCallKakaoLogout_withServerError_thenThrowException() { + //given + mockServer.expect(requestTo(kakaoProperties.logoutRequestUri())) + .andExpect(method(HttpMethod.POST)) + .andExpect(header("Authorization", "KakaoAK " + kakaoProperties.adminKey())) + .andExpect(header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")) + .andRespond(withServerError()); + //when & then + + assertThatThrownBy(() -> kakaoClient.logout(123456L)) + .hasMessageContaining("카카오 콜백 처리 실패"); + } } diff --git a/src/test/java/com/dnd/moddo/domain/auth/service/RefreshTokenServiceTest.java b/src/test/java/com/dnd/moddo/domain/auth/service/RefreshTokenServiceTest.java index f00adbb..9d73188 100644 --- a/src/test/java/com/dnd/moddo/domain/auth/service/RefreshTokenServiceTest.java +++ b/src/test/java/com/dnd/moddo/domain/auth/service/RefreshTokenServiceTest.java @@ -1,10 +1,9 @@ package com.dnd.moddo.domain.auth.service; +import static com.dnd.moddo.global.support.UserTestFactory.*; import static org.assertj.core.api.BDDAssertions.*; import static org.mockito.Mockito.*; -import java.time.LocalDateTime; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -15,7 +14,6 @@ import com.dnd.moddo.domain.auth.exception.TokenInvalidException; import com.dnd.moddo.domain.user.entity.User; -import com.dnd.moddo.domain.user.entity.type.Authority; import com.dnd.moddo.domain.user.repository.UserRepository; import com.dnd.moddo.global.jwt.dto.RefreshResponse; import com.dnd.moddo.global.jwt.properties.JwtConstants; @@ -60,12 +58,11 @@ public void reissueAccessToken() { when(mockJws.getBody()).thenReturn(mockClaims); when(mockClaims.get(JwtConstants.EMAIL.message)).thenReturn(email); - User user = new User("name", email, role, true, Authority.USER, LocalDateTime.now(), - LocalDateTime.now().plusDays(1)); + User user = createGuestDefault(); ReflectionTestUtils.setField(user, "id", userId); when(userRepository.getByEmail(email)).thenReturn(user); - when(jwtProvider.generateAccessToken(userId, email, role)).thenReturn(newAccessToken); + when(jwtProvider.generateAccessToken(userId, role)).thenReturn(newAccessToken); // when RefreshResponse response = refreshTokenService.execute(validToken); @@ -73,7 +70,7 @@ public void reissueAccessToken() { // then then(response.getAccessToken()).isEqualTo(newAccessToken); verify(userRepository, times(1)).getByEmail(email); - verify(jwtProvider, times(1)).generateAccessToken(userId, email, role); + verify(jwtProvider, times(1)).generateAccessToken(userId, role); } @Test diff --git a/src/test/java/com/dnd/moddo/domain/group/controller/GroupControllerTest.java b/src/test/java/com/dnd/moddo/domain/group/controller/GroupControllerTest.java index e326ed6..21c6be0 100644 --- a/src/test/java/com/dnd/moddo/domain/group/controller/GroupControllerTest.java +++ b/src/test/java/com/dnd/moddo/domain/group/controller/GroupControllerTest.java @@ -23,6 +23,8 @@ import com.dnd.moddo.domain.groupMember.dto.response.GroupMemberResponse; import com.dnd.moddo.global.util.RestDocsTestSupport; +import jakarta.servlet.http.HttpServletRequest; + public class GroupControllerTest extends RestDocsTestSupport { @Test @@ -34,7 +36,7 @@ void saveGroup() throws Exception { 1L, MANAGER, "김모또", "https://moddo-s3.s3.amazonaws.com/profile/MODDO.png", true, LocalDateTime.now() )); - given(jwtService.getUserId(any())).willReturn(1L); + given(jwtService.getUserId(any(HttpServletRequest.class))).willReturn(1L); given(commandGroupService.createGroup(any(), eq(1L))).willReturn(response); // when & then @@ -55,7 +57,7 @@ void updateAccount() throws Exception { LocalDateTime.now().plusDays(1) ); - given(jwtService.getUserId(any())).willReturn(1L); + given(jwtService.getUserId(any(HttpServletRequest.class))).willReturn(1L); given(queryGroupService.findIdByCode(anyString())).willReturn(100L); given(commandGroupService.updateAccount(any(), eq(1L), eq(100L))).willReturn(response); @@ -76,7 +78,7 @@ void getGroup() throws Exception { LocalDateTime.now()) )); - given(jwtService.getUserId(any())).willReturn(1L); + given(jwtService.getUserId(any(HttpServletRequest.class))).willReturn(1L); given(queryGroupService.findIdByCode(anyString())).willReturn(100L); given(queryGroupService.findOne(100L, 1L)).willReturn(response); @@ -93,7 +95,7 @@ void isPasswordMatch() throws Exception { GroupPasswordRequest request = new GroupPasswordRequest("1234"); GroupPasswordResponse response = GroupPasswordResponse.from("확인되었습니다."); - given(jwtService.getUserId(any())).willReturn(1L); + given(jwtService.getUserId(any(HttpServletRequest.class))).willReturn(1L); given(queryGroupService.findIdByCode(anyString())).willReturn(100L); given(commandGroupService.isPasswordMatch(100L, 1L, request)).willReturn(response); diff --git a/src/test/java/com/dnd/moddo/domain/group/service/implementation/GroupCreatorTest.java b/src/test/java/com/dnd/moddo/domain/group/service/implementation/GroupCreatorTest.java index 1c72ad1..dd27754 100644 --- a/src/test/java/com/dnd/moddo/domain/group/service/implementation/GroupCreatorTest.java +++ b/src/test/java/com/dnd/moddo/domain/group/service/implementation/GroupCreatorTest.java @@ -1,5 +1,6 @@ package com.dnd.moddo.domain.group.service.implementation; +import static com.dnd.moddo.global.support.UserTestFactory.*; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; @@ -22,7 +23,6 @@ import com.dnd.moddo.domain.image.dto.CharacterResponse; import com.dnd.moddo.domain.image.service.implementation.ImageReader; import com.dnd.moddo.domain.user.entity.User; -import com.dnd.moddo.domain.user.entity.type.Authority; import com.dnd.moddo.domain.user.repository.UserRepository; @ExtendWith(MockitoExtension.class) @@ -57,8 +57,7 @@ void setUp() { userId = 1L; request = new GroupRequest("groupName", "password"); - mockUser = new User(userId, "test@example.com", "닉네임", "프로필", false, LocalDateTime.now(), - LocalDateTime.now().plusDays(1), Authority.USER); + mockUser = createGuestDefault(); encodedPassword = "encryptedPassword"; diff --git a/src/test/java/com/dnd/moddo/domain/user/entity/UserTest.java b/src/test/java/com/dnd/moddo/domain/user/entity/UserTest.java index 36e14f7..f1f5638 100644 --- a/src/test/java/com/dnd/moddo/domain/user/entity/UserTest.java +++ b/src/test/java/com/dnd/moddo/domain/user/entity/UserTest.java @@ -1,57 +1,53 @@ package com.dnd.moddo.domain.user.entity; -import com.dnd.moddo.ModdoApplication; -import com.dnd.moddo.domain.user.exception.UserNotFoundException; -import com.dnd.moddo.domain.user.repository.UserRepository; +import static com.dnd.moddo.global.support.UserTestFactory.*; +import static org.assertj.core.api.AssertionsForClassTypes.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalDateTime; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; - -import static com.dnd.moddo.domain.user.entity.type.Authority.USER; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; +import com.dnd.moddo.domain.user.exception.UserNotFoundException; +import com.dnd.moddo.domain.user.repository.UserRepository; @ExtendWith(SpringExtension.class) @DataJpaTest public class UserTest { - @Autowired - private UserRepository userRepository; - - @DisplayName("이메일로 사용자를 조회할 수 있다.") - @Test - public void findByUser() { - // Given - LocalDateTime time = LocalDateTime.now(); + @Autowired + private UserRepository userRepository; - User user1 = new User("홍길동", "guest-UUID1@guest.com", "profile.png", false, USER, time, time.plusDays(7)); - User user2 = new User("심청이", "guest-UUID2@guest.com", "profile.png", false, USER, time, time.plusDays(7)); + @DisplayName("이메일로 사용자를 조회할 수 있다.") + @Test + public void findByUser() { + // Given + LocalDateTime time = LocalDateTime.now(); - userRepository.save(user1); - userRepository.save(user2); + User user1 = createGuestWithNameAndEmail("홍길동", "guest-UUID1@guest.com"); + User user2 = createGuestWithNameAndEmail("심청이", "guest-UUID2@guest.com"); - // When - User foundUser = userRepository.getByEmail("guest-UUID2@guest.com"); + userRepository.save(user1); + userRepository.save(user2); - // Then - assertThat(foundUser.getName()).isEqualTo("심청이"); - assertThat(foundUser.getEmail()).isEqualTo("guest-UUID2@guest.com"); - } + // When + User foundUser = userRepository.getByEmail("guest-UUID2@guest.com"); + // Then + assertThat(foundUser.getName()).isEqualTo("심청이"); + assertThat(foundUser.getEmail()).isEqualTo("guest-UUID2@guest.com"); + } - @DisplayName("이메일로 사용자를 조회할 때, 사용자가 없으면 예외를 발생시킨다.") - @Test - public void getByEmailNotFound() { - // When & Then - assertThrows(UserNotFoundException.class, () -> userRepository.getByEmail("exception@guest.com")); - } + @DisplayName("이메일로 사용자를 조회할 때, 사용자가 없으면 예외를 발생시킨다.") + @Test + public void getByEmailNotFound() { + // When & Then + assertThrows(UserNotFoundException.class, () -> userRepository.getByEmail("exception@guest.com")); + } } diff --git a/src/test/java/com/dnd/moddo/domain/user/service/CommandUserServiceTest.java b/src/test/java/com/dnd/moddo/domain/user/service/CommandUserServiceTest.java new file mode 100644 index 0000000..13c1d94 --- /dev/null +++ b/src/test/java/com/dnd/moddo/domain/user/service/CommandUserServiceTest.java @@ -0,0 +1,90 @@ +package com.dnd.moddo.domain.user.service; + +import static org.assertj.core.api.AssertionsForClassTypes.*; +import static org.mockito.Mockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.dnd.moddo.domain.user.dto.request.GuestUserSaveRequest; +import com.dnd.moddo.domain.user.dto.request.UserSaveRequest; +import com.dnd.moddo.domain.user.entity.User; +import com.dnd.moddo.domain.user.service.implementation.UserCreator; +import com.dnd.moddo.domain.user.service.implementation.UserReader; + +@ExtendWith(MockitoExtension.class) +public class CommandUserServiceTest { + @Mock + private UserCreator userCreator; + @Mock + private UserReader userReader; + @InjectMocks + private CommandUserService commandUserService; + + @DisplayName("유효한 요청으로 게스트 유저를 생성할 수 있다.") + @Test + void whenSaveRequestIsValid_thenGuestUserIsSaved() { + //given + GuestUserSaveRequest request = new GuestUserSaveRequest("email", "Guest"); + when(userCreator.createUser(any(User.class))).thenReturn(request.toEntity()); + //when + User result = commandUserService.createGuestUser(request); + //then + assertThat(result.getEmail()).isEqualTo("email"); + assertThat(result.getName()).isEqualTo("Guest"); + assertThat(result.getIsMember()).isFalse(); + } + + @DisplayName("유효한 요청으로 카카오 유저를 생성할 수 있다.") + @Test + void whenSaveRequestIsValid_thenKakaoUserIsSaved() { + //given + UserSaveRequest request = new UserSaveRequest("email", "Kakao", 123456L); + when(userCreator.createUser(any(User.class))).thenReturn(request.toEntity()); + //when + User result = commandUserService.createKakaoUser(request); + //then + assertThat(result.getEmail()).isEqualTo("email"); + assertThat(result.getName()).isEqualTo("Kakao"); + assertThat(result.getKakaoId()).isEqualTo(123456L); + } + + @DisplayName("카카오 ID로 조회 시 유저가 없으면 새로 생성한다") + @Test + void whenUserDoesNotExist_thenCreateNewUser() { + //given + UserSaveRequest request = new UserSaveRequest("email", "Kakao", 123456L); + + when(userReader.findByKakaoId(anyLong())).thenReturn(Optional.empty()); + when(userCreator.createUser(any(User.class))).thenReturn(request.toEntity()); + //when + User result = commandUserService.getOrCreateUser(request); + + //then + assertThat(result.getEmail()).isEqualTo("email"); + assertThat(result.getName()).isEqualTo("Kakao"); + assertThat(result.getKakaoId()).isEqualTo(123456L); + } + + @DisplayName("카카오 ID로 유저 조회 시 이미 존재하면 기존 유저를 반환한다") + @Test + void getOrCreateUser() { + //given + UserSaveRequest request = new UserSaveRequest("email", "Kakao", 123456L); + + when(userReader.findByKakaoId(anyLong())).thenReturn(Optional.of(request.toEntity())); + //when + User result = commandUserService.getOrCreateUser(request); + + //then + assertThat(result.getEmail()).isEqualTo("email"); + assertThat(result.getName()).isEqualTo("Kakao"); + assertThat(result.getKakaoId()).isEqualTo(123456L); + } +} diff --git a/src/test/java/com/dnd/moddo/domain/user/service/implementation/UserCreatorTest.java b/src/test/java/com/dnd/moddo/domain/user/service/implementation/UserCreatorTest.java new file mode 100644 index 0000000..ca14b3d --- /dev/null +++ b/src/test/java/com/dnd/moddo/domain/user/service/implementation/UserCreatorTest.java @@ -0,0 +1,40 @@ +package com.dnd.moddo.domain.user.service.implementation; + +import static com.dnd.moddo.global.support.UserTestFactory.*; +import static org.assertj.core.api.AssertionsForClassTypes.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.dnd.moddo.domain.user.entity.User; +import com.dnd.moddo.domain.user.repository.UserRepository; + +@ExtendWith(MockitoExtension.class) +public class UserCreatorTest { + @Mock + private UserRepository userRepository; + @InjectMocks + private UserCreator userCreator; + + @Test + @DisplayName("사용자 생성 시 userRepository.save가 호출되고 저장된 User를 반환한다") + void whenCreateUser_thenReturnSavedUser() { + // given + User user = createWithEmail("test@example.com"); + + when(userRepository.save(user)).thenReturn(user); + + // when + User result = userCreator.createUser(user); + + // then + assertThat(result).isNotNull(); + assertThat(result.getEmail()).isEqualTo("test@example.com"); + verify(userRepository, times(1)).save(user); + } +} diff --git a/src/test/java/com/dnd/moddo/domain/user/service/implementation/UserReaderTest.java b/src/test/java/com/dnd/moddo/domain/user/service/implementation/UserReaderTest.java new file mode 100644 index 0000000..b38ecc9 --- /dev/null +++ b/src/test/java/com/dnd/moddo/domain/user/service/implementation/UserReaderTest.java @@ -0,0 +1,41 @@ +package com.dnd.moddo.domain.user.service.implementation; + +import static com.dnd.moddo.global.support.UserTestFactory.*; +import static org.assertj.core.api.AssertionsForClassTypes.*; +import static org.mockito.Mockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.dnd.moddo.domain.user.entity.User; +import com.dnd.moddo.domain.user.repository.UserRepository; + +@ExtendWith(MockitoExtension.class) +public class UserReaderTest { + @Mock + private UserRepository userRepository; + @InjectMocks + private UserReader userReader; + + @DisplayName("kakaoId로 User를 조회하면 해당 User를 반환한다") + @Test + void whenFindByKakaoId_thenReturnUser() { + //given + User user = createWithEmail("test@example.com"); + Long kakaoId = user.getKakaoId(); + + when(userRepository.findByKakaoId(kakaoId)).thenReturn(Optional.of(user)); + //when + Optional result = userReader.findByKakaoId(kakaoId); + //then + assertThat(result).isPresent(); + assertThat(result.get().getKakaoId()).isEqualTo(kakaoId); + verify(userRepository, times(1)).findByKakaoId(kakaoId); + } +} diff --git a/src/test/java/com/dnd/moddo/domain/user/service/queryUserServiceTest.java b/src/test/java/com/dnd/moddo/domain/user/service/queryUserServiceTest.java new file mode 100644 index 0000000..6ada626 --- /dev/null +++ b/src/test/java/com/dnd/moddo/domain/user/service/queryUserServiceTest.java @@ -0,0 +1,52 @@ +package com.dnd.moddo.domain.user.service; + +import static org.assertj.core.api.AssertionsForClassTypes.*; +import static org.mockito.Mockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.dnd.moddo.domain.user.service.implementation.UserReader; + +@ExtendWith(MockitoExtension.class) +public class queryUserServiceTest { + @Mock + private UserReader userReader; + @InjectMocks + private QueryUserService queryUserService; + + @DisplayName("userId로 kakaoId를 조회하면 해당 kakaoId를 반환한다") + @Test + void whenFindKakaoIdById_thenReturnKakaoId() { + //given + Long userId = 1L; + Long kakaoId = 123456L; + when(userReader.findKakaoIdById(any())).thenReturn(Optional.of(kakaoId)); + //when + Optional result = queryUserService.findKakaoIdById(userId); + //then + assertThat(result).isPresent(); + assertThat(result.get()).isEqualTo(kakaoId); + verify(userReader, times(1)).findKakaoIdById(userId); + } + + @DisplayName("userId가 없을 때 null을 반환한다") + @Test + void whenUserIdNotFound_thenReturnNull() { + //given + when(userReader.findKakaoIdById(any())).thenReturn(Optional.empty()); + + //when + Optional result = queryUserService.findKakaoIdById(1L); + + //then + assertThat(result).isEmpty(); + verify(userReader, times(1)).findKakaoIdById(1L); + } +} diff --git a/src/test/java/com/dnd/moddo/global/support/UserTestFactory.java b/src/test/java/com/dnd/moddo/global/support/UserTestFactory.java index 12c756f..dcd1041 100644 --- a/src/test/java/com/dnd/moddo/global/support/UserTestFactory.java +++ b/src/test/java/com/dnd/moddo/global/support/UserTestFactory.java @@ -3,6 +3,7 @@ import static com.dnd.moddo.domain.user.entity.type.Authority.*; import java.time.LocalDateTime; +import java.util.UUID; import com.dnd.moddo.domain.user.entity.User; @@ -10,26 +11,45 @@ public class UserTestFactory { public static User createGuestDefault() { LocalDateTime time = LocalDateTime.now(); - return new User( - "연노른자", - "guest-UUID1@guest.com", - "profile.png", - false, - USER, - time, - time.plusDays(7)); + return User + .builder() + .name("연노른자") + .email("guest-" + UUID.randomUUID() + "@guest.com") + .profile("profile.png") + .isMember(false) + .authority(USER) + .createdAt(time) + .expiredAt(time.plusDays(7)) + .build(); } - public static User createWithEmail(String email) { + public static User createGuestWithNameAndEmail(String name, String email) { LocalDateTime time = LocalDateTime.now(); - return new User( - "연노른자", - email, - "profile.png", - true, - USER, - time, - time.plusDays(7)); + return User + .builder() + .name(name) + .email(email) + .profile("profile.png") + .isMember(false) + .authority(USER) + .createdAt(time) + .expiredAt(time.plusDays(7)) + .build(); + } + + public static User createWithEmail(String email) { + LocalDateTime time = LocalDateTime.now(); + return User + .builder() + .name("연노른자") + .email(email) + .profile("profile.png") + .isMember(true) + .kakaoId(1234565L) + .authority(USER) + .createdAt(time) + .expiredAt(time.plusDays(7)) + .build(); } } diff --git a/src/test/java/com/dnd/moddo/global/util/ControllerTest.java b/src/test/java/com/dnd/moddo/global/util/ControllerTest.java index 0006619..fed0773 100644 --- a/src/test/java/com/dnd/moddo/global/util/ControllerTest.java +++ b/src/test/java/com/dnd/moddo/global/util/ControllerTest.java @@ -25,6 +25,7 @@ import com.dnd.moddo.domain.image.service.CommandImageService; import com.dnd.moddo.domain.memberExpense.controller.MemberExpenseController; import com.dnd.moddo.domain.memberExpense.service.QueryMemberExpenseService; +import com.dnd.moddo.global.config.CookieProperties; import com.dnd.moddo.global.jwt.auth.JwtAuth; import com.dnd.moddo.global.jwt.auth.JwtFilter; import com.dnd.moddo.global.jwt.service.JwtService; @@ -88,7 +89,9 @@ public abstract class ControllerTest { @MockBean protected QueryMemberExpenseService queryMemberExpenseService; - + + @MockBean + protected CookieProperties cookieProperties; // Jwt @MockBean protected JwtAuth jwtAuth; diff --git a/src/test/java/com/dnd/moddo/integration/CacheIntegrationTest.java b/src/test/java/com/dnd/moddo/integration/CacheIntegrationTest.java index 5771c45..9221917 100644 --- a/src/test/java/com/dnd/moddo/integration/CacheIntegrationTest.java +++ b/src/test/java/com/dnd/moddo/integration/CacheIntegrationTest.java @@ -3,7 +3,7 @@ import static org.assertj.core.api.AssertionsForClassTypes.*; import static org.mockito.Mockito.*; -import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -64,8 +64,8 @@ void setUp() { groupRepository.save(GroupTestFactory.createDefault()); } - @AfterEach - void tearDown() { + @AfterAll + static void tearDown() { redis.close(); } diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 18cea29..a5f2c38 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -46,6 +46,9 @@ cookie: kakao: - auth: - client_id: clientidclientidclientidclientidclientidclientidclientid - redirect_uri: http://localhost:8080/api/v1/login/kakao/callback \ No newline at end of file + client-id: clientidclientidclientidclientidclientidclientidclientid + admin-key: adminkeyadminkeyadminkeyadminkeyadminkey + redirect-uri: http://localhost:8080/api/v1/login/kakao/callback + token-request-uri: https://kauth.kakao.com/oauth/token + profile-request-uri: https://kapi.kakao.com/v2/user/me + logout-request-uri: https://kapi.kakao.com/v1/user/logout \ No newline at end of file