diff --git a/src/main/java/com/kuit/findyou/domain/auth/controller/AuthController.java b/src/main/java/com/kuit/findyou/domain/auth/controller/AuthController.java index 647c564f..5742096c 100644 --- a/src/main/java/com/kuit/findyou/domain/auth/controller/AuthController.java +++ b/src/main/java/com/kuit/findyou/domain/auth/controller/AuthController.java @@ -3,21 +3,22 @@ import com.kuit.findyou.domain.auth.dto.ReissueTokenRequest; import com.kuit.findyou.domain.auth.dto.ReissueTokenResponse; import com.kuit.findyou.domain.auth.dto.request.GuestLoginRequest; +import com.kuit.findyou.domain.auth.dto.response.AdminLoginResponse; import com.kuit.findyou.domain.auth.dto.response.GuestLoginResponse; import com.kuit.findyou.domain.auth.dto.request.KakaoLoginRequest; import com.kuit.findyou.domain.auth.dto.response.KakaoLoginResponse; import com.kuit.findyou.domain.auth.service.AuthServiceFacade; import com.kuit.findyou.global.common.annotation.CustomExceptionDescription; +import com.kuit.findyou.global.common.exception.CustomException; import com.kuit.findyou.global.common.response.BaseResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.*; +import static com.kuit.findyou.global.common.response.status.BaseExceptionResponseStatus.UNAUTHORIZED; import static com.kuit.findyou.global.common.swagger.SwaggerResponseDescription.*; @Tag(name = "Login", description = "로그인 관련 API") @@ -28,6 +29,9 @@ public class AuthController { private final AuthServiceFacade authServiceFacade; + @Value("${admin.api.key}") + private String adminApiKey; + @Operation( summary = "카카오 로그인 API", description = "카카오 사용자 식별자를 이용해서 유저 정보와 엑세스 토큰을 얻을 수 있습니다. 가입된 회원인지 여부를 반환합니다." @@ -58,4 +62,18 @@ public BaseResponse guestLogin(@RequestBody GuestLoginReques public BaseResponse reissueToken(@RequestBody ReissueTokenRequest request){ return BaseResponse.ok(authServiceFacade.reissueToken(request)); } + + @Operation( + summary = "서비스 계정 로그인 API (내부 자동화용)", + description = "내부 자동화/관리용 서비스 계정 토큰을 발급합니다. X-ADMIN-KEY 헤더가 필요합니다." + ) + @PostMapping("/login/admin") + public BaseResponse adminLogin( + @RequestHeader(value = "X-ADMIN-KEY", required = false) String adminKey + ) { + if (adminKey == null || adminKey.isBlank() || !adminApiKey.equals(adminKey)) { + throw new CustomException(UNAUTHORIZED); + } + return BaseResponse.ok(authServiceFacade.adminLogin()); + } } diff --git a/src/main/java/com/kuit/findyou/domain/auth/dto/response/AdminLoginResponse.java b/src/main/java/com/kuit/findyou/domain/auth/dto/response/AdminLoginResponse.java new file mode 100644 index 00000000..56b1eb61 --- /dev/null +++ b/src/main/java/com/kuit/findyou/domain/auth/dto/response/AdminLoginResponse.java @@ -0,0 +1,12 @@ +package com.kuit.findyou.domain.auth.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "관리자 로그인 응답 DTO") +public record AdminLoginResponse( + @Schema(description = "관리자 유저 식별자") + Long userId, + @Schema(description = "엑세스 토큰") + String accessToken +) { +} diff --git a/src/main/java/com/kuit/findyou/domain/auth/service/AdminLoginService.java b/src/main/java/com/kuit/findyou/domain/auth/service/AdminLoginService.java new file mode 100644 index 00000000..9af185dd --- /dev/null +++ b/src/main/java/com/kuit/findyou/domain/auth/service/AdminLoginService.java @@ -0,0 +1,7 @@ +package com.kuit.findyou.domain.auth.service; + +import com.kuit.findyou.domain.auth.dto.response.AdminLoginResponse; + +public interface AdminLoginService { + AdminLoginResponse adminLogin(); +} diff --git a/src/main/java/com/kuit/findyou/domain/auth/service/AdminLoginServiceImpl.java b/src/main/java/com/kuit/findyou/domain/auth/service/AdminLoginServiceImpl.java new file mode 100644 index 00000000..f9f8ec61 --- /dev/null +++ b/src/main/java/com/kuit/findyou/domain/auth/service/AdminLoginServiceImpl.java @@ -0,0 +1,36 @@ +package com.kuit.findyou.domain.auth.service; + +import com.kuit.findyou.domain.auth.dto.response.AdminLoginResponse; +import com.kuit.findyou.domain.user.model.Role; +import com.kuit.findyou.domain.user.model.User; +import com.kuit.findyou.domain.user.repository.UserRepository; +import com.kuit.findyou.global.common.exception.CustomException; +import com.kuit.findyou.global.jwt.util.JwtUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import static com.kuit.findyou.global.common.response.status.BaseExceptionResponseStatus.USER_NOT_FOUND; + +@RequiredArgsConstructor +@Service +public class AdminLoginServiceImpl implements AdminLoginService{ + private final JwtUtil jwtUtil; + private final UserRepository userRepository; + + @Value("${admin.admin-user-id}") + private Long adminUserId; + + @Value("${admin.access-ttl-ms}") + private Long adminAccessTtlMs; + + @Override + public AdminLoginResponse adminLogin() { + User user = userRepository.findById(adminUserId) + .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); + + String accessToken = jwtUtil.createAccessJwt(user.getId(), Role.ADMIN, adminAccessTtlMs); + + return new AdminLoginResponse(user.getId(), accessToken); + } +} diff --git a/src/main/java/com/kuit/findyou/domain/auth/service/AuthServiceFacade.java b/src/main/java/com/kuit/findyou/domain/auth/service/AuthServiceFacade.java index d6e73592..5964bed0 100644 --- a/src/main/java/com/kuit/findyou/domain/auth/service/AuthServiceFacade.java +++ b/src/main/java/com/kuit/findyou/domain/auth/service/AuthServiceFacade.java @@ -4,6 +4,7 @@ import com.kuit.findyou.domain.auth.dto.ReissueTokenResponse; import com.kuit.findyou.domain.auth.dto.request.GuestLoginRequest; import com.kuit.findyou.domain.auth.dto.request.KakaoLoginRequest; +import com.kuit.findyou.domain.auth.dto.response.AdminLoginResponse; import com.kuit.findyou.domain.auth.dto.response.GuestLoginResponse; import com.kuit.findyou.domain.auth.dto.response.KakaoLoginResponse; import lombok.RequiredArgsConstructor; @@ -14,6 +15,7 @@ public class AuthServiceFacade { private final LoginService loginService; private final ReissueTokenService reissueTokenService; + private final AdminLoginService adminLoginService; public KakaoLoginResponse kakaoLogin(KakaoLoginRequest request) { return loginService.kakaoLogin(request); @@ -26,4 +28,8 @@ public GuestLoginResponse guestLogin(GuestLoginRequest request) { public ReissueTokenResponse reissueToken(ReissueTokenRequest request) { return reissueTokenService.reissueToken(request); } + + public AdminLoginResponse adminLogin() { + return adminLoginService.adminLogin(); + } } diff --git a/src/main/java/com/kuit/findyou/domain/image/controller/ImageController.java b/src/main/java/com/kuit/findyou/domain/image/controller/ImageController.java index 20ad9bad..dafe6265 100644 --- a/src/main/java/com/kuit/findyou/domain/image/controller/ImageController.java +++ b/src/main/java/com/kuit/findyou/domain/image/controller/ImageController.java @@ -29,7 +29,7 @@ public class ImageController { @Operation(summary = "신고글 이미지 업로드 API", description = "멀티파트 이미지 업로드 후 CDN URL 리스트 반환") @CustomExceptionDescription(IMAGE_UPLOAD) - @PreAuthorize("hasRole('ROLE_USER')") + @PreAuthorize("hasAnyRole('ROLE_USER','ROLE_ADMIN')") @PostMapping(value = "/upload", consumes = MULTIPART_FORM_DATA_VALUE) public BaseResponse uploadImages(@RequestPart(value = "files", required = false) List files, @LoginUserId Long userId) { List urls = imageUploadService.uploadImages(files); diff --git a/src/main/java/com/kuit/findyou/domain/user/model/Role.java b/src/main/java/com/kuit/findyou/domain/user/model/Role.java index 49c0f113..e17743c2 100644 --- a/src/main/java/com/kuit/findyou/domain/user/model/Role.java +++ b/src/main/java/com/kuit/findyou/domain/user/model/Role.java @@ -5,7 +5,7 @@ @Getter public enum Role { - USER("회원"), GUEST("비회원"); + USER("회원"), GUEST("비회원"), ADMIN("관리자"); private final String value; diff --git a/src/main/java/com/kuit/findyou/global/config/SecurityConfig.java b/src/main/java/com/kuit/findyou/global/config/SecurityConfig.java index 6a566986..5708a572 100644 --- a/src/main/java/com/kuit/findyou/global/config/SecurityConfig.java +++ b/src/main/java/com/kuit/findyou/global/config/SecurityConfig.java @@ -1,10 +1,12 @@ package com.kuit.findyou.global.config; +import com.kuit.findyou.global.jwt.filter.AdminAllowlistFilter; import com.kuit.findyou.global.jwt.security.CustomAccessDeniedHandler; import com.kuit.findyou.global.jwt.security.CustomAuthenticationEntryPoint; import com.kuit.findyou.global.jwt.filter.JwtAuthenticationFilter; import com.kuit.findyou.global.logging.MDCLoggingFilter; import lombok.RequiredArgsConstructor; +import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; @@ -13,6 +15,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.ExceptionTranslationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @@ -24,6 +27,7 @@ public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; private final CustomAccessDeniedHandler customAccessDeniedHandler; + private final AdminAllowlistFilter adminAllowlistFilter; // MDCLoggingFilter 명시적 빈 등록 @Bean @@ -74,6 +78,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{ http .addFilterBefore(mdcLoggingFilter(), JwtAuthenticationFilter.class); + http.addFilterAfter(adminAllowlistFilter, ExceptionTranslationFilter.class); + // 토큰 검증 예외 처리 추가 http .exceptionHandling(configurer -> configurer.authenticationEntryPoint(customAuthenticationEntryPoint) @@ -85,4 +91,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{ return http.build(); } + @Bean + public FilterRegistrationBean adminAllowlistFilterRegistration(AdminAllowlistFilter filter) { + FilterRegistrationBean bean = new FilterRegistrationBean<>(filter); + bean.setEnabled(false); + return bean; + } } diff --git a/src/main/java/com/kuit/findyou/global/jwt/filter/AdminAllowlistFilter.java b/src/main/java/com/kuit/findyou/global/jwt/filter/AdminAllowlistFilter.java new file mode 100644 index 00000000..511318ef --- /dev/null +++ b/src/main/java/com/kuit/findyou/global/jwt/filter/AdminAllowlistFilter.java @@ -0,0 +1,70 @@ +package com.kuit.findyou.global.jwt.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpMethod; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class AdminAllowlistFilter extends OncePerRequestFilter { + + private final AntPathMatcher matcher = new AntPathMatcher(); + + // ADMIN에게 허용되는 API + private static final List ADMIN_ALLOWLIST = List.of( + new Allow(HttpMethod.GET.name(), "/api/v2/reports/protecting-reports/random-s3"), + new Allow(HttpMethod.GET.name(), "/api/v2/reports/missing-reports/random-s3"), + new Allow(HttpMethod.POST.name(), "/api/v2/images/upload") + ); + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + + if (auth != null && auth.isAuthenticated()) { + boolean isAdmin = auth.getAuthorities().stream() + .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN")); + + if (isAdmin) { + String method = request.getMethod(); + String path = request.getRequestURI(); + + boolean allowed = ADMIN_ALLOWLIST.stream() + .anyMatch(a -> a.method.equals(method) && matcher.match(a.pathPattern, path)); + + if (!allowed) { + throw new AccessDeniedException("ADMIN은 허용된 API만 호출할 수 있습니다."); + } + } + } + + filterChain.doFilter(request, response); + } + + private static class Allow { + final String method; + final String pathPattern; + + private Allow(String method, String pathPattern) { + this.method = method; + this.pathPattern = pathPattern; + } + } +} diff --git a/src/main/java/com/kuit/findyou/global/jwt/security/CustomAccessDeniedHandler.java b/src/main/java/com/kuit/findyou/global/jwt/security/CustomAccessDeniedHandler.java index e59be912..ea0cc62a 100644 --- a/src/main/java/com/kuit/findyou/global/jwt/security/CustomAccessDeniedHandler.java +++ b/src/main/java/com/kuit/findyou/global/jwt/security/CustomAccessDeniedHandler.java @@ -21,7 +21,10 @@ public class CustomAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { - BaseErrorResponse body = new BaseErrorResponse(FORBIDDEN); + String message = accessDeniedException.getMessage(); + BaseErrorResponse body = (message == null || message.isBlank()) + ? new BaseErrorResponse(FORBIDDEN) + : new BaseErrorResponse(FORBIDDEN, message); String json = objectMapper.writeValueAsString(body); response.setStatus(FORBIDDEN.getCode()); diff --git a/src/main/java/com/kuit/findyou/global/jwt/util/JwtUtil.java b/src/main/java/com/kuit/findyou/global/jwt/util/JwtUtil.java index bce4f5b1..286074e2 100644 --- a/src/main/java/com/kuit/findyou/global/jwt/util/JwtUtil.java +++ b/src/main/java/com/kuit/findyou/global/jwt/util/JwtUtil.java @@ -61,6 +61,17 @@ public String createAccessJwt(Long userId, Role role) { .compact(); } + public String createAccessJwt(Long userId, Role role, long expireMs) { + return Jwts.builder() + .claim(JwtClaimKey.USER_ID.getKey(), userId) + .claim(JwtClaimKey.ROLE.getKey(), role.name()) + .claim(JwtClaimKey.TOKEN_TYPE.getKey(), JwtTokenType.ACCESS_TOKEN) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + expireMs)) + .signWith(secretKey) + .compact(); + } + public String createRefreshJwt(Long userId) { return Jwts.builder() .id(UUID.randomUUID().toString()) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1c1a7505..50c4c987 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -193,3 +193,9 @@ management: exposure: include: health, info, prometheus + +admin: + admin-user-id: ${ADMIN_USER_ID} + access-ttl-ms: ${ADMIN_ACCESS_TTL_MS} + api: + key: ${ADMIN_API_KEY} diff --git a/src/test/java/com/kuit/findyou/domain/auth/controller/AuthControllerTest.java b/src/test/java/com/kuit/findyou/domain/auth/controller/AuthControllerTest.java index 649607c1..99174bf7 100644 --- a/src/test/java/com/kuit/findyou/domain/auth/controller/AuthControllerTest.java +++ b/src/test/java/com/kuit/findyou/domain/auth/controller/AuthControllerTest.java @@ -3,8 +3,9 @@ import com.kuit.findyou.domain.auth.dto.ReissueTokenRequest; import com.kuit.findyou.domain.auth.dto.ReissueTokenResponse; import com.kuit.findyou.domain.auth.dto.request.GuestLoginRequest; -import com.kuit.findyou.domain.auth.dto.response.GuestLoginResponse; import com.kuit.findyou.domain.auth.dto.request.KakaoLoginRequest; +import com.kuit.findyou.domain.auth.dto.response.AdminLoginResponse; +import com.kuit.findyou.domain.auth.dto.response.GuestLoginResponse; import com.kuit.findyou.domain.auth.dto.response.KakaoLoginResponse; import com.kuit.findyou.domain.auth.repository.RedisRefreshTokenRepository; import com.kuit.findyou.domain.user.model.Role; @@ -13,6 +14,7 @@ import com.kuit.findyou.global.common.response.BaseErrorResponse; import com.kuit.findyou.global.common.response.BaseResponse; import com.kuit.findyou.global.common.util.DatabaseCleaner; +import com.kuit.findyou.global.common.util.TestInitializer; import com.kuit.findyou.global.config.RedisTestContainersConfig; import com.kuit.findyou.global.config.TestDatabaseConfig; import com.kuit.findyou.global.jwt.util.JwtClaimKey; @@ -22,7 +24,10 @@ import io.restassured.RestAssured; import io.restassured.common.mapper.TypeRef; import io.restassured.http.ContentType; -import org.junit.jupiter.api.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; @@ -60,6 +65,15 @@ class AuthControllerTest { @Autowired private DatabaseCleaner databaseCleaner; + @Autowired + TestInitializer testInitializer; + + @Value("${admin.api.key}") + String adminApiKey; + + @Value("${admin.admin-user-id}") + Long adminUserId; + @Value("${findyou.jwt.secret-key}") String secret; @@ -255,4 +269,130 @@ void reissueToken_shouldReturnNotFound_WhenRefreshTokenIsNotMatched(){ assertThat(response.getCode()).isEqualTo(REFRESH_TOKEN_NOT_FOUND.getCode()); assertThat(response.getMessage()).isEqualTo(REFRESH_TOKEN_NOT_FOUND.getMessage()); } + + @DisplayName("관리자 키가 유효하면 관리자 로그인 성공(access 토큰 반환)") + @Test + void adminLogin_shouldReturnAccessToken_WhenValidAdminKey() { + // given + testInitializer.insertAdminUserWithFixedId(adminUserId, Role.ADMIN); + + // when + BaseResponse response = given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .header("X-ADMIN-KEY", adminApiKey) + .body("{}") + .when() + .post("/api/v2/auth/login/admin") + .then() + .statusCode(200) + .extract() + .as(new TypeRef>() {}); + + // then + String access = response.getData().accessToken(); + + assertThat(access).isNotBlank(); + + // access 토큰 검증 + assertThat(jwtUtil.getUserId(access)).isEqualTo(adminUserId); + assertThat(jwtUtil.getRole(access)).isEqualTo(Role.ADMIN); + assertThat(jwtUtil.getTokenType(access)).isEqualTo(JwtTokenType.ACCESS_TOKEN); + } + + @DisplayName("관리자 키가 틀리면 401을 반환한다") + @Test + void adminLogin_shouldReturnUnauthorized_WhenInvalidAdminKey() { + // given + testInitializer.insertAdminUserWithFixedId(adminUserId, Role.USER); + + // when + BaseErrorResponse response = given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .header("X-ADMIN-KEY", "wrong-key") + .body("{}") + .when() + .post("/api/v2/auth/login/admin") + .then() + .extract() + .as(new TypeRef() {}); + + // then + assertThat(response.getCode()).isEqualTo(UNAUTHORIZED.getCode()); + assertThat(response.getMessage()).isEqualTo(UNAUTHORIZED.getMessage()); + assertThat(response.getSuccess()).isFalse(); + } + @DisplayName("관리자 키 헤더가 없으면 401을 반환한다") + @Test + void adminLogin_shouldReturnUnauthorized_WhenAdminKeyMissing() { + // given + testInitializer.insertAdminUserWithFixedId(adminUserId, Role.USER); + + // when + BaseErrorResponse response = given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .body("{}") + .when() + .post("/api/v2/auth/login/admin") + .then() + .extract() + .as(new TypeRef() {}); + + // then + assertThat(response.getCode()).isEqualTo(UNAUTHORIZED.getCode()); + assertThat(response.getMessage()).isEqualTo(UNAUTHORIZED.getMessage()); + assertThat(response.getSuccess()).isFalse(); + } + + @DisplayName("관리자 키가 빈 문자열이면 401을 반환한다") + @Test + void adminLogin_shouldReturnUnauthorized_WhenAdminKeyIsBlank() { + // given + testInitializer.insertAdminUserWithFixedId(adminUserId, Role.USER); + + // when + BaseErrorResponse response = given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .header("X-ADMIN-KEY", "") + .body("{}") + .when() + .post("/api/v2/auth/login/admin") + .then() + .extract() + .as(new TypeRef() {}); + + // then + assertThat(response.getCode()).isEqualTo(UNAUTHORIZED.getCode()); + assertThat(response.getMessage()).isEqualTo(UNAUTHORIZED.getMessage()); + assertThat(response.getSuccess()).isFalse(); + } + + + @DisplayName("관리자 유저가 존재하지 않으면 404(USER_NOT_FOUND)를 반환한다") + @Test + void adminLogin_shouldReturnNotFound_WhenAdminUserDoesNotExist() { + // given: insertAdminUserWithFixedId 호출 안 함 + + // when + BaseErrorResponse response = given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .header("X-ADMIN-KEY", adminApiKey) + .body("{}") + .when() + .post("/api/v2/auth/login/admin") + .then() + .extract() + .as(new TypeRef() {}); + + // then + assertThat(response.getCode()).isEqualTo(USER_NOT_FOUND.getCode()); + assertThat(response.getMessage()).isEqualTo(USER_NOT_FOUND.getMessage()); + assertThat(response.getSuccess()).isFalse(); + } + + } \ No newline at end of file diff --git a/src/test/java/com/kuit/findyou/domain/auth/service/AdminLoginServiceImplTest.java b/src/test/java/com/kuit/findyou/domain/auth/service/AdminLoginServiceImplTest.java new file mode 100644 index 00000000..6b6a5bd9 --- /dev/null +++ b/src/test/java/com/kuit/findyou/domain/auth/service/AdminLoginServiceImplTest.java @@ -0,0 +1,89 @@ +package com.kuit.findyou.domain.auth.service; + +import com.kuit.findyou.domain.auth.dto.response.AdminLoginResponse; +import com.kuit.findyou.domain.auth.repository.RedisRefreshTokenRepository; +import com.kuit.findyou.domain.user.model.Role; +import com.kuit.findyou.domain.user.model.User; +import com.kuit.findyou.domain.user.repository.UserRepository; +import com.kuit.findyou.global.common.exception.CustomException; +import com.kuit.findyou.global.jwt.util.JwtUtil; +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 org.springframework.test.util.ReflectionTestUtils; + +import java.util.Optional; + +import static com.kuit.findyou.global.common.response.status.BaseExceptionResponseStatus.USER_NOT_FOUND; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class AdminLoginServiceImplTest { + + @InjectMocks + AdminLoginServiceImpl adminLoginService; + + @Mock + JwtUtil jwtUtil; + + @Mock + UserRepository userRepository; + + @DisplayName("관리자 로그인 성공 시 access 토큰을 반환한다") + @Test + void adminLogin_shouldReturnAccessToken() { + // given + Long adminUserId = 9999L; + Long adminAccessTtlMs = 5_184_000_000L; // 60일 + User admin = User.builder() + .id(adminUserId) + .role(Role.ADMIN) + .build(); + + String accessToken = "admin access"; + + ReflectionTestUtils.setField(adminLoginService, "adminUserId", adminUserId); + ReflectionTestUtils.setField(adminLoginService, "adminAccessTtlMs", adminAccessTtlMs); + + when(userRepository.findById(adminUserId)).thenReturn(Optional.of(admin)); + when(jwtUtil.createAccessJwt( + eq(adminUserId), + eq(Role.ADMIN), + eq(adminAccessTtlMs) + )).thenReturn(accessToken); + + // when + AdminLoginResponse response = adminLoginService.adminLogin(); + + // then + assertThat(response.userId()).isEqualTo(adminUserId); + assertThat(response.accessToken()).isEqualTo(accessToken); + + verify(jwtUtil).createAccessJwt( + eq(adminUserId), + eq(Role.ADMIN), + eq(adminAccessTtlMs) + ); + } + + @DisplayName("관리자 유저가 존재하지 않으면 USER_NOT_FOUND 예외를 발생시킨다") + @Test + void adminLogin_shouldThrowException_whenAdminUserNotFound() { + // given + Long adminUserId = 9999L; + ReflectionTestUtils.setField(adminLoginService, "adminUserId", adminUserId); + + when(userRepository.findById(adminUserId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> adminLoginService.adminLogin()) + .isInstanceOf(CustomException.class) + .hasMessage(USER_NOT_FOUND.getMessage()); + } +} diff --git a/src/test/java/com/kuit/findyou/global/common/util/TestInitializer.java b/src/test/java/com/kuit/findyou/global/common/util/TestInitializer.java index 59f26dff..d0364812 100644 --- a/src/test/java/com/kuit/findyou/global/common/util/TestInitializer.java +++ b/src/test/java/com/kuit/findyou/global/common/util/TestInitializer.java @@ -20,6 +20,7 @@ import com.kuit.findyou.domain.user.model.Role; import com.kuit.findyou.domain.user.model.User; import com.kuit.findyou.domain.user.repository.UserRepository; +import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -48,6 +49,8 @@ public class TestInitializer { private final SidoRepository sidoRepository; private final SigunguRepository sigunguRepository; + private final EntityManager em; + private User defaultUser; @Transactional @@ -430,4 +433,16 @@ public User createTestGuest() { return userRepository.save(user); } + + @Transactional + public void insertAdminUserWithFixedId(Long id, Role role) { + em.createNativeQuery(""" + INSERT INTO users (id, name, role, receive_notification, status, device_id, kakao_id, profile_image_url) + VALUES (?1, ?2, ?3, 'N', 'Y', NULL, NULL, NULL) + """) + .setParameter(1, id) + .setParameter(2, "관리자") + .setParameter(3, role.name()) + .executeUpdate(); + } } diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index b0ef0a06..edf27feb 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -62,3 +62,9 @@ openapi: api-key: test-key volunteer-work: api-url: http://openapi.1365.go.kr/openapi/service/rest/VolunteerPartcptnService/getVltrSearchWordList + +admin: + api: + key: test-admin-key + admin-user-id: 9999 + access-ttl-ms: 5184000000 \ No newline at end of file