diff --git a/src/main/java/com/newzet/api/userinfo/business/service/AuthUserService.java b/src/main/java/com/newzet/api/userinfo/business/service/AuthUserService.java new file mode 100644 index 00000000..138bad86 --- /dev/null +++ b/src/main/java/com/newzet/api/userinfo/business/service/AuthUserService.java @@ -0,0 +1,49 @@ +package com.newzet.api.userinfo.business.service; + +import java.net.URI; +import java.util.UUID; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import com.newzet.api.userinfo.exception.AuthUserFailException; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class AuthUserService { + + private final RestTemplate restTemplate; + + @Value("${supabase.url}") + private String supabaseUrl; + + @Value("${supabase.service-key}") + private String supabaseServiceKey; + + public void deleteAuthUser(UUID userId) { + String url = supabaseUrl + "/auth/v1/admin/users/" + userId; + + RequestEntity requestEntity = RequestEntity + .delete(URI.create(url)) + .header("apikey", supabaseServiceKey) + .header("Authorization", "Bearer " + supabaseServiceKey) + .build(); + + try { + // DELETE 요청 보내기 + ResponseEntity response = restTemplate.exchange(requestEntity, String.class); + if (!response.getStatusCode().is2xxSuccessful()) { + throw new AuthUserFailException("Supabase 사용자 삭제 실패: " + response.getBody()); + } + + } catch (Exception e) { + // 네트워크 오류 등 처리 + throw new AuthUserFailException("Supabase 요청 중 알 수 없는 오류가 발생하였습니다."); + } + } +} diff --git a/src/main/java/com/newzet/api/userinfo/exception/AuthUserFailException.java b/src/main/java/com/newzet/api/userinfo/exception/AuthUserFailException.java new file mode 100644 index 00000000..a0bd7000 --- /dev/null +++ b/src/main/java/com/newzet/api/userinfo/exception/AuthUserFailException.java @@ -0,0 +1,12 @@ +package com.newzet.api.userinfo.exception; + +import com.newzet.api.common.exception.NewzetException; +import com.newzet.api.common.response.ResponseCode; + +public class AuthUserFailException extends NewzetException { + private static final ResponseCode responseCode = ResponseCode.SUPABASE_ERROR; + + public AuthUserFailException(String message) { + super(message, responseCode); + } +} \ No newline at end of file diff --git a/src/main/java/com/newzet/api/userinfo/orchestrator/UserinfoOrchestrator.java b/src/main/java/com/newzet/api/userinfo/orchestrator/UserinfoOrchestrator.java index d52f8a82..f5d4d495 100644 --- a/src/main/java/com/newzet/api/userinfo/orchestrator/UserinfoOrchestrator.java +++ b/src/main/java/com/newzet/api/userinfo/orchestrator/UserinfoOrchestrator.java @@ -10,6 +10,7 @@ import com.newzet.api.category.domain.Category; import com.newzet.api.usercategory.business.service.UserCategoryService; import com.newzet.api.usercategory.domain.UserCategory; +import com.newzet.api.userinfo.business.service.AuthUserService; import com.newzet.api.userinfo.business.service.UserinfoService; import com.newzet.api.userinfo.domain.Userinfo; import com.newzet.api.userinfo.presentation.dto.UniqueMailResponse; @@ -25,6 +26,7 @@ public class UserinfoOrchestrator { private final UserinfoService userinfoService; private final UserCategoryService userCategoryService; private final CategoryService categoryService; + private final AuthUserService authUserService; @Transactional(readOnly = true) public UserinfoWithCategoryListResponse getUserinfoWithCategoryList(UUID userId) { @@ -58,5 +60,6 @@ public UserinfoInitResponse checkUserInitializeCompleted(UUID userId) { @Transactional public void deleteUserinfo(UUID userId) { userinfoService.deleteUserinfoById(userId); + authUserService.deleteAuthUser(userId); } } diff --git a/src/main/java/com/newzet/api/userinfo/presentation/controller/UserinfoController.java b/src/main/java/com/newzet/api/userinfo/presentation/controller/UserinfoController.java index c2068cf3..489a6477 100644 --- a/src/main/java/com/newzet/api/userinfo/presentation/controller/UserinfoController.java +++ b/src/main/java/com/newzet/api/userinfo/presentation/controller/UserinfoController.java @@ -1,7 +1,5 @@ package com.newzet.api.userinfo.presentation.controller; -import java.util.UUID; - import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; @@ -76,6 +74,6 @@ public SuccessResponse checkEmailUniqueness( description = "유저 정보를 삭제한다.") public SuccessResponse deleteUserinfo(@Login AuthUser authUser) { userinfoOrchestrator.deleteUserinfo(authUser.getId()); - return SuccessResponse.create(ResponseCode.SUCCESS, "유저 정보를 삭제한다.", null); + return SuccessResponse.create(ResponseCode.SUCCESS, "유저정보 삭제 성공", null); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 453f4fcc..86bb1a74 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -70,4 +70,8 @@ cloud: static: ${S3_REGION} credentials: access-key: ${S3_ACCESS_KEY} - secret-key: ${S3_SECRET_KEY} \ No newline at end of file + secret-key: ${S3_SECRET_KEY} + +supabase: + url: ${SUPABASE_URL} + service-key: ${SUPABASE_SERVICE_KEY} \ No newline at end of file diff --git a/src/test/java/com/newzet/api/article/repository/batch/ArticleRedisBatchProcessorIntegrationTest.java b/src/test/java/com/newzet/api/article/repository/batch/ArticleRedisBatchProcessorIntegrationTest.java index 2a9142ff..67c68f28 100644 --- a/src/test/java/com/newzet/api/article/repository/batch/ArticleRedisBatchProcessorIntegrationTest.java +++ b/src/test/java/com/newzet/api/article/repository/batch/ArticleRedisBatchProcessorIntegrationTest.java @@ -40,10 +40,11 @@ import com.newzet.api.config.PostgresTestContainerConfig; import com.newzet.api.config.RedisTestContainerConfig; import com.newzet.api.config.S3TestConfig; +import com.newzet.api.config.SupabaseConfig; import com.newzet.api.fcm.orchestrator.FcmSenderOrchestrator; @ExtendWith({RedisTestContainerConfig.class, PostgresTestContainerConfig.class, - JwtTestConfig.class, FirebaseTestConfig.class, S3TestConfig.class}) + JwtTestConfig.class, FirebaseTestConfig.class, S3TestConfig.class, SupabaseConfig.class}) @SpringBootTest @ComponentScan(basePackages = {"com.newzet.api.article", "com.newzet.api.common"}) class ArticleRedisBatchProcessorIntegrationTest { @@ -274,7 +275,8 @@ void getBatchStatus_WhenPendingCountIsNull_ThenReturnZero() { } private Article createArticleDto(UUID userId, String title) { - return Article.createNewArticle(userId, "Newsletter", "example.com", "daily", "imageUrl@a.com", title, + return Article.createNewArticle(userId, "Newsletter", "example.com", "daily", + "imageUrl@a.com", title, "https://example.com/" + title.toLowerCase().replace(' ', '-')); } } diff --git a/src/test/java/com/newzet/api/config/SupabaseConfig.java b/src/test/java/com/newzet/api/config/SupabaseConfig.java new file mode 100644 index 00000000..465152ab --- /dev/null +++ b/src/test/java/com/newzet/api/config/SupabaseConfig.java @@ -0,0 +1,16 @@ +package com.newzet.api.config; + +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +public class SupabaseConfig implements BeforeAllCallback { + + public static final String TEST_SUPABASE_URL = "aaaaaaaaaa"; + public static final String TEST_SUPABASE_KEY = "aaaaaaaaaaaaaa"; + + @Override + public void beforeAll(ExtensionContext context) throws Exception { + System.setProperty("supabase.url", TEST_SUPABASE_URL); + System.setProperty("supabase.service-key", TEST_SUPABASE_KEY); + } +} diff --git a/src/test/java/com/newzet/api/userinfo/business/service/AuthUserServiceTest.java b/src/test/java/com/newzet/api/userinfo/business/service/AuthUserServiceTest.java new file mode 100644 index 00000000..a782ff6f --- /dev/null +++ b/src/test/java/com/newzet/api/userinfo/business/service/AuthUserServiceTest.java @@ -0,0 +1,102 @@ +package com.newzet.api.userinfo.business.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.net.URI; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.client.RestTemplate; + +import com.newzet.api.userinfo.exception.AuthUserFailException; + +@ExtendWith(MockitoExtension.class) +class AuthUserServiceTest { + + private final String supabaseUrl = "http://test-supabase.com"; + private final String supabaseServiceKey = "test-service-key"; + @InjectMocks + private AuthUserService authUserService; + @Mock + private RestTemplate restTemplate; + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(authUserService, "supabaseUrl", supabaseUrl); + ReflectionTestUtils.setField(authUserService, "supabaseServiceKey", supabaseServiceKey); + } + + @Test + @DisplayName("Supabase 사용자 삭제 성공") + void deleteAuthUser_Success() { + // given: 테스트 준비 + UUID userId = UUID.randomUUID(); + ResponseEntity successResponse = new ResponseEntity<>("User deleted", + HttpStatus.OK); + when(restTemplate.exchange(any(RequestEntity.class), eq(String.class))) + .thenReturn(successResponse); + + // when & then: 실행 및 검증 + assertDoesNotThrow(() -> authUserService.deleteAuthUser(userId)); + + // verify: RestTemplate이 올바른 인자와 함께 호출되었는지 검증 + ArgumentCaptor> requestCaptor = ArgumentCaptor.forClass( + RequestEntity.class); + verify(restTemplate).exchange(requestCaptor.capture(), eq(String.class)); + + RequestEntity capturedRequest = requestCaptor.getValue(); + assertEquals(URI.create(supabaseUrl + "/auth/v1/admin/users/" + userId), + capturedRequest.getUrl()); + assertEquals("DELETE", capturedRequest.getMethod().name()); + assertEquals(supabaseServiceKey, capturedRequest.getHeaders().getFirst("apikey")); + assertEquals("Bearer " + supabaseServiceKey, + capturedRequest.getHeaders().getFirst("Authorization")); + } + + @Test + @DisplayName("Supabase API가 에러를 반환하여 사용자 삭제 실패") + void deleteAuthUser_Fail_WhenSupabaseReturnsError() { + // given: 테스트 준비 + UUID userId = UUID.randomUUID(); + String errorBody = "{\"error\":\"User not found\"}"; + // RestTemplate이 실패(400 Bad Request) 응답을 반환하도록 설정합니다. + ResponseEntity errorResponse = new ResponseEntity<>(errorBody, + HttpStatus.BAD_REQUEST); + when(restTemplate.exchange(any(RequestEntity.class), eq(String.class))) + .thenReturn(errorResponse); + + // when & then: 실행 및 검증 + assertThrows(AuthUserFailException.class, + () -> authUserService.deleteAuthUser(userId)); + } + + @Test + @DisplayName("네트워크 오류로 인해 사용자 삭제 실패") + void deleteAuthUser_Fail_WhenNetworkErrorOccurs() { + // given: 테스트 준비 + UUID userId = UUID.randomUUID(); + // RestTemplate 호출 시 런타임 예외가 발생하도록 설정합니다. (네트워크 문제 시뮬레이션) + when(restTemplate.exchange(any(RequestEntity.class), eq(String.class))) + .thenThrow(new RuntimeException("Network error")); + + // when & then: 실행 및 검증 + AuthUserFailException exception = assertThrows(AuthUserFailException.class, + () -> authUserService.deleteAuthUser(userId)); + + // 정의된 일반 오류 메시지와 일치하는지 확인합니다. + assertEquals("Supabase 요청 중 알 수 없는 오류가 발생하였습니다.", exception.getMessage()); + } +} \ No newline at end of file