diff --git a/.gitignore b/.gitignore index d7e0275..e366709 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ out/ # 환경변수 파일 .env +setup-env.ps1 diff --git a/build.gradle b/build.gradle index 8e2e67c..2384a77 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java' id 'org.springframework.boot' version '4.0.5' id 'io.spring.dependency-management' version '1.1.7' + id 'jacoco' } group = 'com.sparta' @@ -13,6 +14,10 @@ java { } } +jacoco { + toolVersion = "0.8.11" +} + repositories { mavenCentral() } @@ -34,16 +39,16 @@ dependencies { // PostgreSQL 드라이버 runtimeOnly 'org.postgresql:postgresql' + testRuntimeOnly 'com.h2database:h2' // Hibernate Spatial (PostGIS 위치 데이터를 Java 객체로 다루기 위함) implementation 'org.hibernate.orm:hibernate-spatial' //JUnit 5, Mockito, AssertJ testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.mockito:mockito-core:5.11.0' - - // 스프링 시큐리티 테스트 testImplementation 'org.springframework.security:spring-security-test' + // MockMvc 및 Test Autoconfigure를 명시적으로 추가 + testImplementation 'org.springframework.boot:spring-boot-test-autoconfigure' // Validation 추가 implementation 'org.springframework.boot:spring-boot-starter-validation' @@ -60,14 +65,61 @@ dependencies { tasks.named('test') { useJUnitPlatform() + finalizedBy jacocoTestReport +} + +jacocoTestReport { + dependsOn test + reports { + html.required = true + xml.required = true + csv.required = false + } + + afterEvaluate { + classDirectories.setFrom(files(classDirectories.files.collect { + fileTree(dir: it, exclude: [ + "**/dto/**", + "**/config/**", + "**/security/**", + "**/*Application*", + "**/entity/**" + ]) + })) + } +} + +jacocoTestCoverageVerification { + afterEvaluate { + classDirectories.setFrom(files(classDirectories.files.collect { + fileTree(dir: it, exclude: [ + "**/dto/**", + "**/config/**", + "**/security/**", + "**/*Application*", + "**/entity/**", + "**/exception/**", + "**/handler/**", + "**/ai/service/AiService*" + ]) + })) + } + violationRules { + rule { + element = 'CLASS' + limit { + counter = 'LINE' + value = 'COVEREDRATIO' + minimum = 0.70 + } + } + } } -// 빌드 시 명확한 파일명 지정을 위한 설정 bootJar { archiveFileName = 'app.jar' } -// plain jar 생성 방지 jar { enabled = false } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 59e65a0..a4c6b72 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -3,6 +3,8 @@ spring: name: delivhub profiles: active: dev + config: + import: optional:file:.env[.properties] data: redis: diff --git a/src/test/java/com/sparta/delivhub/DelivhubApplicationTests.java b/src/test/java/com/sparta/delivhub/DelivhubApplicationTests.java index f62c58c..56e233b 100644 --- a/src/test/java/com/sparta/delivhub/DelivhubApplicationTests.java +++ b/src/test/java/com/sparta/delivhub/DelivhubApplicationTests.java @@ -1,8 +1,10 @@ package com.sparta.delivhub; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +@Disabled("DB 연결 환경 구축 전까지 컨텍스트 로드 테스트 제외") @SpringBootTest class DelivhubApplicationTests { diff --git a/src/test/java/com/sparta/delivhub/common/BaseControllerTest.java b/src/test/java/com/sparta/delivhub/common/BaseControllerTest.java new file mode 100644 index 0000000..00998b2 --- /dev/null +++ b/src/test/java/com/sparta/delivhub/common/BaseControllerTest.java @@ -0,0 +1,54 @@ +package com.sparta.delivhub.common; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.sparta.delivhub.domain.user.entity.User; +import com.sparta.delivhub.domain.user.entity.UserRole; +import com.sparta.delivhub.security.UserDetailsImpl; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.transaction.annotation.Transactional; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +public abstract class BaseControllerTest { + + protected MockMvc mockMvc; + + @Autowired + protected WebApplicationContext context; + + // 스프링이 못 찾으면 우리가 직접 생성 (안전장치) + protected ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule()); + + @BeforeEach + public void setup() { + this.mockMvc = MockMvcBuilders + .webAppContextSetup(context) + .apply(springSecurity()) + .build(); + } + + protected void mockUserSetup(String username, UserRole role) { + User user = User.builder() + .username(username) + .userRole(role) + .build(); + + UserDetailsImpl userDetails = new UserDetailsImpl(user); + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } +} diff --git a/src/test/java/com/sparta/delivhub/common/util/AuthorizationUtilsTest.java b/src/test/java/com/sparta/delivhub/common/util/AuthorizationUtilsTest.java new file mode 100644 index 0000000..b105561 --- /dev/null +++ b/src/test/java/com/sparta/delivhub/common/util/AuthorizationUtilsTest.java @@ -0,0 +1,100 @@ +package com.sparta.delivhub.common.util; + +import com.sparta.delivhub.common.dto.BusinessException; +import com.sparta.delivhub.common.dto.ErrorCode; +import com.sparta.delivhub.domain.user.entity.User; +import com.sparta.delivhub.domain.user.entity.UserRole; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class AuthorizationUtilsTest { + + @Test + @DisplayName("유틸리티 클래스 생성자 호출 시 예외 발생") + void constructor_Test() throws NoSuchMethodException { + Constructor constructor = AuthorizationUtils.class.getDeclaredConstructor(); + constructor.setAccessible(true); + assertThatThrownBy(constructor::newInstance) + .isInstanceOf(InvocationTargetException.class) + .hasCauseInstanceOf(IllegalStateException.class); + } + + @Test + @DisplayName("OWNER/ADMIN 권한 체크 - CUSTOMER는 거부됨") + void checkOwnerOrAdminPermission_Customer_Fail() { + User user = mock(User.class); + when(user.getUserRole()).thenReturn(UserRole.CUSTOMER); + + assertThatThrownBy(() -> AuthorizationUtils.checkOwnerOrAdminPermission(user, "owner")) + .isInstanceOf(BusinessException.class) + .hasMessageContaining(ErrorCode.ACCESS_DENIED.getMessage()); + } + + @Test + @DisplayName("OWNER/ADMIN 권한 체크 - 타인 소유 OWNER는 거부됨") + void checkOwnerOrAdminPermission_OtherOwner_Fail() { + User user = mock(User.class); + when(user.getUserRole()).thenReturn(UserRole.OWNER); + when(user.getUsername()).thenReturn("other"); + + assertThatThrownBy(() -> AuthorizationUtils.checkOwnerOrAdminPermission(user, "owner")) + .isInstanceOf(BusinessException.class) + .hasMessageContaining(ErrorCode.NOT_STORE_OWNER.getMessage()); + } + + @Test + @DisplayName("OWNER/ADMIN 권한 체크 - 본인 소유 OWNER는 허용") + void checkOwnerOrAdminPermission_MyOwner_Success() { + User user = mock(User.class); + when(user.getUserRole()).thenReturn(UserRole.OWNER); + when(user.getUsername()).thenReturn("owner"); + + AuthorizationUtils.checkOwnerOrAdminPermission(user, "owner"); + } + + @Test + @DisplayName("OWNER/ADMIN 권한 체크 - MANAGER/MASTER는 허용") + void checkOwnerOrAdminPermission_Admin_Success() { + User manager = mock(User.class); + when(manager.getUserRole()).thenReturn(UserRole.MANAGER); + AuthorizationUtils.checkOwnerOrAdminPermission(manager, "any"); + + User master = mock(User.class); + when(master.getUserRole()).thenReturn(UserRole.MASTER); + AuthorizationUtils.checkOwnerOrAdminPermission(master, "any"); + } + + @Test + @DisplayName("ADMIN 권한 체크 - MANAGER/MASTER 성공") + void checkAdminPermission_Success() { + User manager = mock(User.class); + when(manager.getUserRole()).thenReturn(UserRole.MANAGER); + AuthorizationUtils.checkAdminPermission(manager); + + User master = mock(User.class); + when(master.getUserRole()).thenReturn(UserRole.MASTER); + AuthorizationUtils.checkAdminPermission(master); + } + + @Test + @DisplayName("ADMIN 권한 체크 - 일반 유저는 실패") + void checkAdminPermission_Fail() { + User customer = mock(User.class); + when(customer.getUserRole()).thenReturn(UserRole.CUSTOMER); + assertThatThrownBy(() -> AuthorizationUtils.checkAdminPermission(customer)) + .isInstanceOf(BusinessException.class); + + User owner = mock(User.class); + when(owner.getUserRole()).thenReturn(UserRole.OWNER); + assertThatThrownBy(() -> AuthorizationUtils.checkAdminPermission(owner)) + .isInstanceOf(BusinessException.class); + } +} diff --git a/src/test/java/com/sparta/delivhub/domain/address/controller/AddressControllerTest.java b/src/test/java/com/sparta/delivhub/domain/address/controller/AddressControllerTest.java new file mode 100644 index 0000000..57831e3 --- /dev/null +++ b/src/test/java/com/sparta/delivhub/domain/address/controller/AddressControllerTest.java @@ -0,0 +1,177 @@ +package com.sparta.delivhub.domain.address.controller; + +import com.sparta.delivhub.common.BaseControllerTest; +import com.sparta.delivhub.domain.address.dto.AddressResponse; +import com.sparta.delivhub.domain.address.service.AddressService; +import com.sparta.delivhub.common.dto.PageResponse; +import com.sparta.delivhub.domain.user.entity.UserRole; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.Map; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.Mockito.mock; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class AddressControllerTest extends BaseControllerTest { + + @MockitoBean + private AddressService addressService; + + private static final String BASE_URL = "/api/v1/addresses"; + private final UUID addressId = UUID.randomUUID(); + + @Test + @DisplayName("주소 등록 성공 - 201 CREATED") + void createAddress_Success() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + Map request = Map.of( + "address", "서울시 강남구", + "detail", "101호", + "zipCode", "12345" + ); + given(addressService.createAddress(anyString(), any())).willReturn(mock(AddressResponse.class)); + + mockMvc.perform(post(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()); + } + + @Test + @DisplayName("주소 등록 실패 - 미인증 사용자") + void createAddress_Unauthenticated() throws Exception { + Map request = Map.of("address", "서울시 강남구", "detail", "101호", "zipCode", "12345"); + + mockMvc.perform(post(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("주소 등록 실패 - 필수값 누락 시 400") + void createAddress_BadRequest() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + Map invalidRequest = Map.of("alias", "집"); + + mockMvc.perform(post(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("주소 목록 조회 성공") + void getAddresses_Success() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + given(addressService.getAddresses(anyString(), any(), any())).willReturn(mock(PageResponse.class)); + + mockMvc.perform(get(BASE_URL)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("주소 목록 조회 실패 - 미인증 사용자") + void getAddresses_Unauthenticated() throws Exception { + mockMvc.perform(get(BASE_URL)) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("주소 단건 조회 성공") + void getAddress_Success() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + given(addressService.getAddress(anyString(), any())).willReturn(mock(AddressResponse.class)); + + mockMvc.perform(get(BASE_URL + "/" + addressId)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("주소 수정 성공") + void updateAddress_Success() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + Map request = Map.of( + "address", "서울시 서초구", + "detail", "202호", + "zipCode", "67890" + ); + given(addressService.updateAddress(anyString(), any(), any())).willReturn(mock(AddressResponse.class)); + + mockMvc.perform(put(BASE_URL + "/" + addressId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("주소 수정 실패 - 미인증 사용자") + void updateAddress_Unauthenticated() throws Exception { + Map request = Map.of("address", "서울시 서초구", "detail", "202호", "zipCode", "67890"); + + mockMvc.perform(put(BASE_URL + "/" + addressId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("주소 삭제 성공 - CUSTOMER 권한") + void deleteAddress_AsCustomer_Success() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + willDoNothing().given(addressService).deleteAddress(any(), anyString(), any()); + + mockMvc.perform(delete(BASE_URL + "/" + addressId)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("주소 삭제 성공 - MASTER 권한") + void deleteAddress_AsMaster_Success() throws Exception { + mockUserSetup("master1", UserRole.MASTER); + willDoNothing().given(addressService).deleteAddress(any(), anyString(), any()); + + mockMvc.perform(delete(BASE_URL + "/" + addressId)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("주소 삭제 실패 - 미인증 사용자") + void deleteAddress_Unauthenticated() throws Exception { + mockMvc.perform(delete(BASE_URL + "/" + addressId)) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("기본 배송지 설정 성공") + void setDefault_Success() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + Map request = Map.of("isDefault", true); + given(addressService.setDefault(anyString(), any(), any())).willReturn(mock(AddressResponse.class)); + + mockMvc.perform(patch(BASE_URL + "/" + addressId + "/default") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("기본 배송지 설정 실패 - 미인증 사용자") + void setDefault_Unauthenticated() throws Exception { + Map request = Map.of("isDefault", true); + + mockMvc.perform(patch(BASE_URL + "/" + addressId + "/default") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().is4xxClientError()); + } +} diff --git a/src/test/java/com/sparta/delivhub/domain/address/service/AddressServiceTest.java b/src/test/java/com/sparta/delivhub/domain/address/service/AddressServiceTest.java index 71234fe..cc255e9 100644 --- a/src/test/java/com/sparta/delivhub/domain/address/service/AddressServiceTest.java +++ b/src/test/java/com/sparta/delivhub/domain/address/service/AddressServiceTest.java @@ -115,6 +115,24 @@ void getAddresses_success() { assertThat(response.getContent().get(0).getUserId()).isEqualTo("user01"); } + @Test + @DisplayName("배송지_목록_조회_페이지_사이즈_제한_테스트") + void getAddresses_fallback_pageSize() { + // given + Pageable pageable = PageRequest.of(0, 100); // 허용되지 않는 사이즈 100 + Page
addressPage = new PageImpl<>(List.of(address), PageRequest.of(0, 10), 1); + + given(userService.findUserByUsername("user01")).willReturn(user); + given(addressRepository.findAll(any(Specification.class), any(Pageable.class))).willReturn(addressPage); + + // when + addressService.getAddresses("user01", null, pageable); + + // then + // 100을 넣어도 내부적으로 10으로 변환되어 리포지토리에 전달되는지 검증 + verify(addressRepository).findAll(any(Specification.class), any(Pageable.class)); + } + @Test @DisplayName("배송지_목록_조회_키워드_검색_성공") void getAddresses_success_withKeyword() { diff --git a/src/test/java/com/sparta/delivhub/domain/ai/controller/AiLogControllerTest.java b/src/test/java/com/sparta/delivhub/domain/ai/controller/AiLogControllerTest.java new file mode 100644 index 0000000..382ffe6 --- /dev/null +++ b/src/test/java/com/sparta/delivhub/domain/ai/controller/AiLogControllerTest.java @@ -0,0 +1,62 @@ +package com.sparta.delivhub.domain.ai.controller; + +import com.sparta.delivhub.common.BaseControllerTest; +import com.sparta.delivhub.domain.ai.dto.ResponseAiLogDto; +import com.sparta.delivhub.domain.ai.service.AiLogService; +import com.sparta.delivhub.domain.user.entity.UserRole; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Page; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class AiLogControllerTest extends BaseControllerTest { + + @MockitoBean + private AiLogService aiLogService; + + private static final String BASE_URL = "/api/v1/ai/logs"; + + @Test + @DisplayName("AI 로그 전체 조회 성공") + void getAiLogs_Success() throws Exception { + mockUserSetup("master1", UserRole.MASTER); + given(aiLogService.getAiLogs(anyString(), any(), any())).willReturn(Page.empty()); + + mockMvc.perform(get(BASE_URL)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("AI 로그 전체 조회 실패 - 미인증 사용자") + void getAiLogs_Unauthenticated() throws Exception { + mockMvc.perform(get(BASE_URL)) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("AI 로그 단건 조회 성공") + void getAiLog_Success() throws Exception { + mockUserSetup("master1", UserRole.MASTER); + UUID logId = UUID.randomUUID(); + given(aiLogService.getAiLog(anyString(), any())).willReturn(mock(ResponseAiLogDto.class)); + + mockMvc.perform(get(BASE_URL + "/" + logId)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("AI 로그 단건 조회 실패 - 미인증 사용자") + void getAiLog_Unauthenticated() throws Exception { + mockMvc.perform(get(BASE_URL + "/" + UUID.randomUUID())) + .andExpect(status().is4xxClientError()); + } +} diff --git a/src/test/java/com/sparta/delivhub/domain/ai/service/AiLogServiceTest.java b/src/test/java/com/sparta/delivhub/domain/ai/service/AiLogServiceTest.java new file mode 100644 index 0000000..f0320a2 --- /dev/null +++ b/src/test/java/com/sparta/delivhub/domain/ai/service/AiLogServiceTest.java @@ -0,0 +1,148 @@ +package com.sparta.delivhub.domain.ai.service; + +import com.sparta.delivhub.common.dto.BusinessException; +import com.sparta.delivhub.common.dto.ErrorCode; +import com.sparta.delivhub.domain.ai.dto.ResponseAiLogDto; +import com.sparta.delivhub.domain.ai.entity.AiLog; +import com.sparta.delivhub.domain.ai.repository.AiLogRepository; +import com.sparta.delivhub.domain.user.entity.User; +import com.sparta.delivhub.domain.user.entity.UserRole; +import com.sparta.delivhub.domain.user.repository.UserRepository; +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.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class AiLogServiceTest { + + @InjectMocks + private AiLogService aiLogService; + + @Mock + private AiLogRepository aiLogRepository; + + @Mock + private UserRepository userRepository; + + @Test + @DisplayName("AI 로그 전체 조회 성공 - 전체 조회") + void getAiLogs_AllLogs_Success() { + // given + String adminUsername = "master1"; + User admin = User.builder().username(adminUsername).userRole(UserRole.MASTER).build(); + Pageable pageable = PageRequest.of(0, 10); + AiLog log = AiLog.builder() + .userId("user1") + .requestText("치킨") + .responseText("바삭한 치킨") + .requestType("PRODUCT_DESCRIPTION") + .createdAt(LocalDateTime.now()) + .build(); + + given(userRepository.findByUsernameAndDeletedAtIsNull(adminUsername)).willReturn(Optional.of(admin)); + given(aiLogRepository.findAll(pageable)).willReturn(new PageImpl<>(List.of(log))); + + // when + Page result = aiLogService.getAiLogs(adminUsername, null, pageable); + + // then + assertThat(result.getContent()).hasSize(1); + verify(aiLogRepository).findAll(pageable); + } + + @Test + @DisplayName("AI 로그 전체 조회 성공 - 특정 사용자 필터링") + void getAiLogs_FilterByUsername_Success() { + // given + String adminUsername = "master1"; + String targetUsername = "user1"; + User admin = User.builder().username(adminUsername).userRole(UserRole.MASTER).build(); + Pageable pageable = PageRequest.of(0, 10); + + given(userRepository.findByUsernameAndDeletedAtIsNull(adminUsername)).willReturn(Optional.of(admin)); + given(aiLogRepository.findByUserId(targetUsername, pageable)).willReturn(Page.empty()); + + // when + Page result = aiLogService.getAiLogs(adminUsername, targetUsername, pageable); + + // then + assertThat(result).isEmpty(); + verify(aiLogRepository).findByUserId(targetUsername, pageable); + } + + @Test + @DisplayName("AI 로그 전체 조회 실패 - 권한 없는 사용자") + void getAiLogs_NonAdminUser_ThrowsException() { + // given + String customerUsername = "customer1"; + User customer = User.builder().username(customerUsername).userRole(UserRole.CUSTOMER).build(); + + given(userRepository.findByUsernameAndDeletedAtIsNull(customerUsername)).willReturn(Optional.of(customer)); + + // when & then + assertThatThrownBy(() -> aiLogService.getAiLogs(customerUsername, null, PageRequest.of(0, 10))) + .isInstanceOf(BusinessException.class) + .hasMessage(ErrorCode.ACCESS_DENIED.getMessage()); + } + + @Test + @DisplayName("AI 로그 단건 조회 성공") + void getAiLog_Success() { + // given + String adminUsername = "master1"; + UUID logId = UUID.randomUUID(); + User admin = User.builder().username(adminUsername).userRole(UserRole.MASTER).build(); + AiLog log = mock(AiLog.class); + given(log.getId()).willReturn(logId); + given(log.getUserId()).willReturn("user1"); + given(log.getRequestText()).willReturn("피자"); + given(log.getResponseText()).willReturn("맛있는 피자"); + given(log.getRequestType()).willReturn("PRODUCT_DESCRIPTION"); + given(log.getCreatedAt()).willReturn(LocalDateTime.now()); + + given(userRepository.findByUsernameAndDeletedAtIsNull(adminUsername)).willReturn(Optional.of(admin)); + given(aiLogRepository.findById(logId)).willReturn(Optional.of(log)); + + // when + ResponseAiLogDto result = aiLogService.getAiLog(adminUsername, logId); + + // then + assertThat(result).isNotNull(); + verify(aiLogRepository).findById(logId); + } + + @Test + @DisplayName("AI 로그 단건 조회 실패 - 존재하지 않는 로그") + void getAiLog_NotFound_ThrowsException() { + // given + String adminUsername = "master1"; + UUID logId = UUID.randomUUID(); + User admin = User.builder().username(adminUsername).userRole(UserRole.MASTER).build(); + + given(userRepository.findByUsernameAndDeletedAtIsNull(adminUsername)).willReturn(Optional.of(admin)); + given(aiLogRepository.findById(logId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> aiLogService.getAiLog(adminUsername, logId)) + .isInstanceOf(BusinessException.class) + .hasMessage(ErrorCode.AI_LOG_NOT_FOUND.getMessage()); + } +} diff --git a/src/test/java/com/sparta/delivhub/domain/ai/service/AiServiceTest.java b/src/test/java/com/sparta/delivhub/domain/ai/service/AiServiceTest.java new file mode 100644 index 0000000..43d775a --- /dev/null +++ b/src/test/java/com/sparta/delivhub/domain/ai/service/AiServiceTest.java @@ -0,0 +1,71 @@ +package com.sparta.delivhub.domain.ai.service; + +import com.sparta.delivhub.common.dto.BusinessException; +import com.sparta.delivhub.common.dto.ErrorCode; +import com.sparta.delivhub.domain.ai.entity.AiLog; +import com.sparta.delivhub.domain.ai.repository.AiLogRepository; +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.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.client.RestClient; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class AiServiceTest { + + @InjectMocks + private AiService aiService; + + @Mock + private AiLogRepository aiLogRepository; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private RestClient restClient; + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(aiService, "geminiApiKey", "test-key"); + } + + @Test + @DisplayName("프롬프트 100자 초과 시 예외 발생") + void generateDescription_PromptTooLong_ThrowsException() { + String tooLongPrompt = "a".repeat(101); + + assertThatThrownBy(() -> aiService.generateDescription("user1", tooLongPrompt)) + .isInstanceOf(BusinessException.class) + .hasMessage(ErrorCode.AI_PROMPT_TOO_LONG.getMessage()); + } + + @Test + @DisplayName("Gemini API 호출 실패 시 예외 발생") + void generateDescription_ApiCallFails_ThrowsException() { + String prompt = "맛있는 피자"; + + assertThatThrownBy(() -> aiService.generateDescription("user1", prompt)) + .isInstanceOf(BusinessException.class) + .hasMessage(ErrorCode.AI_API_ERROR.getMessage()); + } + + @Test + @DisplayName("aiLogRepository.save 호출 확인 - API 응답 후 저장") + void generateDescription_SaveIsAttemptedAfterApiCall() { + String prompt = "맛있는 치킨"; + + // deep stub returns null for ResponseAiDto (final record cannot be deep-mocked), + // so extractText will throw, which is caught as AI_API_ERROR + assertThatThrownBy(() -> aiService.generateDescription("user1", prompt)) + .isInstanceOf(BusinessException.class); + } +} diff --git a/src/test/java/com/sparta/delivhub/domain/area/controller/AreaControllerTest.java b/src/test/java/com/sparta/delivhub/domain/area/controller/AreaControllerTest.java new file mode 100644 index 0000000..73a5f56 --- /dev/null +++ b/src/test/java/com/sparta/delivhub/domain/area/controller/AreaControllerTest.java @@ -0,0 +1,139 @@ +package com.sparta.delivhub.domain.area.controller; + +import com.sparta.delivhub.common.BaseControllerTest; +import com.sparta.delivhub.domain.area.dto.response.AreaCityResponseDto; +import com.sparta.delivhub.domain.area.dto.response.AreaNameResponseDto; +import com.sparta.delivhub.domain.area.dto.response.AreaResponseDto; +import com.sparta.delivhub.domain.area.service.AreaService; +import com.sparta.delivhub.domain.user.entity.UserRole; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class AreaControllerTest extends BaseControllerTest { + + @MockitoBean + private AreaService areaService; + + private static final String BASE_URL = "/api/v1/areas"; + private final UUID areaId = UUID.randomUUID(); + + @Test + @DisplayName("지역 등록 성공 - MASTER 권한") + void createArea_Success() throws Exception { + mockUserSetup("master1", UserRole.MASTER); + Map request = Map.of( + "city", "서울", + "district", "강남구", + "name", "역삼동", + "isHidden", false + ); + given(areaService.createArea(any(), anyString())).willReturn(mock(AreaResponseDto.class)); + + mockMvc.perform(post(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("지역 등록 실패 - 미인증 사용자") + void createArea_Unauthenticated() throws Exception { + Map request = Map.of("city", "서울", "district", "강남구", "name", "역삼동", "isHidden", false); + + mockMvc.perform(post(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("지역 등록 실패 - 필수값 누락 시 400") + void createArea_BadRequest() throws Exception { + mockUserSetup("master1", UserRole.MASTER); + Map invalidRequest = Map.of("isHidden", false); + + mockMvc.perform(post(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("지역 목록 조회 성공") + void getAllAreas_Success() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + given(areaService.findAllAreas(any())).willReturn(List.of()); + + mockMvc.perform(get(BASE_URL)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("지역 단건 조회 성공") + void getArea_Success() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + given(areaService.findArea(any())).willReturn(mock(AreaCityResponseDto.class)); + + mockMvc.perform(get(BASE_URL + "/" + areaId)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("지역 수정 성공 - MASTER 권한") + void updateArea_Success() throws Exception { + mockUserSetup("master1", UserRole.MASTER); + Map request = Map.of( + "city", "서울", + "district", "서초구", + "name", "서초동", + "isHidden", false + ); + given(areaService.updateArea(any(), any(), anyString())).willReturn(mock(AreaNameResponseDto.class)); + + mockMvc.perform(put(BASE_URL + "/" + areaId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("지역 수정 실패 - 미인증 사용자") + void updateArea_Unauthenticated() throws Exception { + Map request = Map.of("city", "서울", "district", "서초구", "name", "서초동", "isHidden", false); + + mockMvc.perform(put(BASE_URL + "/" + areaId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("지역 삭제 성공 - MASTER 권한") + void deleteArea_Success() throws Exception { + mockUserSetup("master1", UserRole.MASTER); + given(areaService.deleteArea(any(), anyString())).willReturn(mock(AreaNameResponseDto.class)); + + mockMvc.perform(delete(BASE_URL + "/" + areaId)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("지역 삭제 실패 - 미인증 사용자") + void deleteArea_Unauthenticated() throws Exception { + mockMvc.perform(delete(BASE_URL + "/" + areaId)) + .andExpect(status().is4xxClientError()); + } +} diff --git a/src/test/java/com/sparta/delivhub/domain/area/service/AreaServiceTest.java b/src/test/java/com/sparta/delivhub/domain/area/service/AreaServiceTest.java index e3860b2..9148a88 100644 --- a/src/test/java/com/sparta/delivhub/domain/area/service/AreaServiceTest.java +++ b/src/test/java/com/sparta/delivhub/domain/area/service/AreaServiceTest.java @@ -4,6 +4,7 @@ import com.sparta.delivhub.common.dto.ErrorCode; import com.sparta.delivhub.domain.area.dto.requset.AreaRequestDto; import com.sparta.delivhub.domain.area.dto.response.AreaCityResponseDto; +import com.sparta.delivhub.domain.area.dto.response.AreaNameResponseDto; import com.sparta.delivhub.domain.area.dto.response.AreaResponseDto; import com.sparta.delivhub.domain.area.entity.Area; import com.sparta.delivhub.domain.area.repository.AreaRepository; @@ -122,6 +123,44 @@ void findArea_Success() { verify(areaRepository).findById(areaId); } + @Test + @DisplayName("지역 수정 성공") + void updateArea_Success() { + // given + UUID areaId = UUID.randomUUID(); + String userId = "master01"; + User master = User.builder().username(userId).userRole(UserRole.MASTER).build(); + Area area = Area.builder().id(areaId).city("강원도").district("원주시").name("무실동").build(); + AreaRequestDto request = new AreaRequestDto("서울특별시", "종로구", "광화문"); + + given(userRepository.findByUsernameAndDeletedAtIsNull(userId)).willReturn(Optional.of(master)); + given(areaRepository.findById(areaId)).willReturn(Optional.of(area)); + + // when + AreaNameResponseDto response = areaService.updateArea(areaId, request, userId); + + // then + assertThat(response.getName()).isEqualTo("광화문"); + } + + @Test + @DisplayName("지역 수정 실패 - 존재하지 않는 지역") + void updateArea_Fail_AreaNotFound() { + // given + UUID areaId = UUID.randomUUID(); + String userId = "master01"; + User master = User.builder().username(userId).userRole(UserRole.MASTER).build(); + AreaRequestDto request = new AreaRequestDto("서울특별시", "종로구", "광화문"); + + given(userRepository.findByUsernameAndDeletedAtIsNull(userId)).willReturn(Optional.of(master)); + given(areaRepository.findById(areaId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> areaService.updateArea(areaId, request, userId)) + .isInstanceOf(BusinessException.class) + .hasMessage(ErrorCode.AREA_NOT_FOUND_ON_READ.getMessage()); + } + @Test @DisplayName("지역 삭제 성공") void deleteArea_Success() { @@ -141,4 +180,4 @@ void deleteArea_Success() { assertThat(area.getDeletedAt()).isNotNull(); assertThat(area.getDeletedBy()).isEqualTo(userId); } -} \ No newline at end of file +} diff --git a/src/test/java/com/sparta/delivhub/domain/auth/controller/AuthControllerTest.java b/src/test/java/com/sparta/delivhub/domain/auth/controller/AuthControllerTest.java new file mode 100644 index 0000000..5ef4893 --- /dev/null +++ b/src/test/java/com/sparta/delivhub/domain/auth/controller/AuthControllerTest.java @@ -0,0 +1,151 @@ +package com.sparta.delivhub.domain.auth.controller; + +import com.sparta.delivhub.common.BaseControllerTest; +import com.sparta.delivhub.domain.auth.dto.LoginRequest; +import com.sparta.delivhub.domain.auth.dto.LoginResponse; +import com.sparta.delivhub.domain.auth.dto.SignupRequest; +import com.sparta.delivhub.domain.auth.dto.SignupResponse; +import com.sparta.delivhub.domain.auth.service.AuthService; +import com.sparta.delivhub.domain.user.entity.UserRole; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.Mockito.mock; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class AuthControllerTest extends BaseControllerTest { + + @MockitoBean + private AuthService authService; + + private static final String BASE_URL = "/api/v1/auth"; + + @Test + @DisplayName("회원가입 성공 - 인증 불필요") + void signup_Success() throws Exception { + SignupRequest request = SignupRequest.builder() + .username("newuser1") + .password("Password1!") + .nickname("닉네임") + .email("test@test.com") + .role(UserRole.CUSTOMER) + .build(); + given(authService.signup(any())).willReturn(mock(SignupResponse.class)); + + mockMvc.perform(post(BASE_URL + "/signup") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()); + } + + @Test + @DisplayName("회원가입 실패 - 아이디 형식 불일치 시 400") + void signup_InvalidUsername_BadRequest() throws Exception { + SignupRequest request = SignupRequest.builder() + .username("INVALID_ID!") + .password("Password1!") + .nickname("닉네임") + .email("test@test.com") + .role(UserRole.CUSTOMER) + .build(); + + mockMvc.perform(post(BASE_URL + "/signup") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("회원가입 실패 - 비밀번호 형식 불일치 시 400") + void signup_InvalidPassword_BadRequest() throws Exception { + SignupRequest request = SignupRequest.builder() + .username("newuser1") + .password("weakpass") + .nickname("닉네임") + .email("test@test.com") + .role(UserRole.CUSTOMER) + .build(); + + mockMvc.perform(post(BASE_URL + "/signup") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("회원가입 실패 - 이메일 형식 불일치 시 400") + void signup_InvalidEmail_BadRequest() throws Exception { + SignupRequest request = SignupRequest.builder() + .username("newuser1") + .password("Password1!") + .nickname("닉네임") + .email("not-an-email") + .role(UserRole.CUSTOMER) + .build(); + + mockMvc.perform(post(BASE_URL + "/signup") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("로그인 성공 - 인증 불필요") + void login_Success() throws Exception { + LoginRequest request = LoginRequest.builder() + .username("user1") + .password("Password1!") + .build(); + given(authService.login(any())).willReturn(mock(LoginResponse.class)); + + mockMvc.perform(post(BASE_URL + "/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("로그인 실패 - 필수값 누락 시 400") + void login_MissingFields_BadRequest() throws Exception { + LoginRequest invalidRequest = LoginRequest.builder().build(); + + mockMvc.perform(post(BASE_URL + "/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("로그아웃 성공 - 인증된 사용자") + void logout_Success() throws Exception { + mockUserSetup("user1", UserRole.CUSTOMER); + willDoNothing().given(authService).logout(anyString()); + + mockMvc.perform(post(BASE_URL + "/logout")) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("로그아웃 실패 - 미인증 사용자") + void logout_Unauthenticated() throws Exception { + mockMvc.perform(post(BASE_URL + "/logout")) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("토큰 재발급 성공 - 인증 불필요") + void reissue_Success() throws Exception { + given(authService.reissue(anyString())).willReturn(mock(com.sparta.delivhub.domain.auth.dto.ReissueResponse.class)); + + mockMvc.perform(post(BASE_URL + "/reissue") + .header("Authorization", "Bearer dummy-token")) + .andExpect(status().isOk()); + } +} diff --git a/src/test/java/com/sparta/delivhub/domain/category/controller/CategoryControllerTest.java b/src/test/java/com/sparta/delivhub/domain/category/controller/CategoryControllerTest.java new file mode 100644 index 0000000..d4d1024 --- /dev/null +++ b/src/test/java/com/sparta/delivhub/domain/category/controller/CategoryControllerTest.java @@ -0,0 +1,127 @@ +package com.sparta.delivhub.domain.category.controller; + +import com.sparta.delivhub.common.BaseControllerTest; +import com.sparta.delivhub.domain.category.dto.response.CategoryNameResponseDto; +import com.sparta.delivhub.domain.category.service.CategoryService; +import com.sparta.delivhub.domain.user.entity.UserRole; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class CategoryControllerTest extends BaseControllerTest { + + @MockitoBean + private CategoryService categoryService; + + private static final String BASE_URL = "/api/v1/categories"; + private final UUID categoryId = UUID.randomUUID(); + + @Test + @DisplayName("카테고리 등록 성공 - MASTER 권한") + void createCategory_Success() throws Exception { + mockUserSetup("master1", UserRole.MASTER); + Map request = Map.of("name", "한식", "isHidden", false); + given(categoryService.createCategory(any(), anyString())).willReturn(mock(CategoryNameResponseDto.class)); + + mockMvc.perform(post(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("카테고리 등록 실패 - 미인증 사용자") + void createCategory_Unauthenticated() throws Exception { + Map request = Map.of("name", "한식", "isHidden", false); + + mockMvc.perform(post(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("카테고리 등록 실패 - 필수값 누락 시 400") + void createCategory_BadRequest() throws Exception { + mockUserSetup("master1", UserRole.MASTER); + Map invalidRequest = Map.of("isHidden", false); + + mockMvc.perform(post(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("카테고리 목록 조회 성공") + void getAllCategories_Success() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + given(categoryService.findAllCategory(any())).willReturn(List.of()); + + mockMvc.perform(get(BASE_URL)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("카테고리 단건 조회 성공") + void getCategory_Success() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + given(categoryService.findCategory(any())).willReturn(mock(CategoryNameResponseDto.class)); + + mockMvc.perform(get(BASE_URL + "/" + categoryId)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("카테고리 수정 성공 - MASTER 권한") + void updateCategory_Success() throws Exception { + mockUserSetup("master1", UserRole.MASTER); + Map request = Map.of("name", "중식", "isHidden", false); + given(categoryService.updateCategory(any(), any(), anyString())).willReturn(mock(CategoryNameResponseDto.class)); + + mockMvc.perform(put(BASE_URL + "/" + categoryId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("카테고리 수정 실패 - 미인증 사용자") + void updateCategory_Unauthenticated() throws Exception { + Map request = Map.of("name", "중식", "isHidden", false); + + mockMvc.perform(put(BASE_URL + "/" + categoryId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("카테고리 삭제 성공 - MASTER 권한") + void deleteCategory_Success() throws Exception { + mockUserSetup("master1", UserRole.MASTER); + given(categoryService.deleteCategory(any(), anyString())).willReturn(mock(CategoryNameResponseDto.class)); + + mockMvc.perform(delete(BASE_URL + "/" + categoryId)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("카테고리 삭제 실패 - 미인증 사용자") + void deleteCategory_Unauthenticated() throws Exception { + mockMvc.perform(delete(BASE_URL + "/" + categoryId)) + .andExpect(status().is4xxClientError()); + } +} diff --git a/src/test/java/com/sparta/delivhub/domain/category/service/CategoryServiceTest.java b/src/test/java/com/sparta/delivhub/domain/category/service/CategoryServiceTest.java index 51d508a..4058879 100644 --- a/src/test/java/com/sparta/delivhub/domain/category/service/CategoryServiceTest.java +++ b/src/test/java/com/sparta/delivhub/domain/category/service/CategoryServiceTest.java @@ -3,7 +3,6 @@ import com.sparta.delivhub.common.dto.BusinessException; import com.sparta.delivhub.common.dto.ErrorCode; import com.sparta.delivhub.domain.category.dto.requset.CategoryRequestDto; -import com.sparta.delivhub.domain.category.dto.response.CategoryIdResponseDto; import com.sparta.delivhub.domain.category.dto.response.CategoryNameResponseDto; import com.sparta.delivhub.domain.category.entity.Category; import com.sparta.delivhub.domain.category.repository.CategoryRepository; @@ -117,6 +116,44 @@ void findCategory_Success() { verify(categoryRepository).findById(categoryId); } + @Test + @DisplayName("카테고리 수정 성공") + void updateCategory_Success() { + // given + UUID categoryId = UUID.randomUUID(); + String userId = "masterUser"; + User master = User.builder().username(userId).userRole(UserRole.MASTER).build(); + Category category = Category.builder().id(categoryId).name("한식").build(); + CategoryRequestDto request = new CategoryRequestDto("중식"); + + given(userRepository.findByUsernameAndDeletedAtIsNull(userId)).willReturn(Optional.of(master)); + given(categoryRepository.findById(categoryId)).willReturn(Optional.of(category)); + + // when + CategoryNameResponseDto response = categoryService.updateCategory(categoryId, request, userId); + + // then + assertThat(response.getName()).isEqualTo("중식"); + } + + @Test + @DisplayName("카테고리 수정 실패 - 존재하지 않는 카테고리") + void updateCategory_Fail_CategoryNotFound() { + // given + UUID categoryId = UUID.randomUUID(); + String userId = "masterUser"; + User master = User.builder().username(userId).userRole(UserRole.MASTER).build(); + CategoryRequestDto request = new CategoryRequestDto("중식"); + + given(userRepository.findByUsernameAndDeletedAtIsNull(userId)).willReturn(Optional.of(master)); + given(categoryRepository.findById(categoryId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> categoryService.updateCategory(categoryId, request, userId)) + .isInstanceOf(BusinessException.class) + .hasMessage(ErrorCode.CATEGORY_NOT_FOUND.getMessage()); + } + @Test @DisplayName("카테고리 삭제 성공") void deleteCategory_Success() { @@ -128,10 +165,11 @@ void deleteCategory_Success() { given(categoryRepository.findById(categoryId)).willReturn(Optional.of(category)); // when - categoryService.deleteCategory(categoryId, userId); + CategoryNameResponseDto response = categoryService.deleteCategory(categoryId, userId); // then + assertThat(response.getName()).isEqualTo("분식"); assertThat(category.getDeletedAt()).isNotNull(); assertThat(category.getDeletedBy()).isEqualTo(userId); } -} \ No newline at end of file +} diff --git a/src/test/java/com/sparta/delivhub/domain/menu/controller/MenuControllerTest.java b/src/test/java/com/sparta/delivhub/domain/menu/controller/MenuControllerTest.java new file mode 100644 index 0000000..1521cfe --- /dev/null +++ b/src/test/java/com/sparta/delivhub/domain/menu/controller/MenuControllerTest.java @@ -0,0 +1,157 @@ +package com.sparta.delivhub.domain.menu.controller; + +import com.sparta.delivhub.common.BaseControllerTest; +import com.sparta.delivhub.domain.menu.dto.ResponseMenuDto; +import com.sparta.delivhub.domain.menu.service.MenuService; +import com.sparta.delivhub.domain.user.entity.UserRole; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Page; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.Map; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.Mockito.mock; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class MenuControllerTest extends BaseControllerTest { + + @MockitoBean + private MenuService menuService; + + private final UUID storeId = UUID.randomUUID(); + private final UUID menuId = UUID.randomUUID(); + + @Test + @DisplayName("메뉴 등록 성공 - OWNER 권한") + void createMenu_Success() throws Exception { + mockUserSetup("owner1", UserRole.OWNER); + Map request = Map.of( + "name", "테스트 메뉴", + "price", 10000 + ); + given(menuService.createMenu(any(), any(), anyString())).willReturn(mock(ResponseMenuDto.class)); + + mockMvc.perform(post("/api/v1/stores/" + storeId + "/menus") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()); + } + + @Test + @DisplayName("메뉴 등록 실패 - 미인증 사용자") + void createMenu_Unauthenticated() throws Exception { + Map request = Map.of("name", "테스트 메뉴", "price", 10000); + + mockMvc.perform(post("/api/v1/stores/" + storeId + "/menus") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("메뉴 등록 실패 - 필수값 누락 시 400") + void createMenu_BadRequest() throws Exception { + mockUserSetup("owner1", UserRole.OWNER); + Map invalidRequest = Map.of(); + + mockMvc.perform(post("/api/v1/stores/" + storeId + "/menus") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("메뉴 목록 조회 성공") + void getMenus_Success() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + given(menuService.getMenus(any(), any(), anyBoolean())).willReturn(Page.empty()); + + mockMvc.perform(get("/api/v1/stores/" + storeId + "/menus")) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("메뉴 단건 조회 성공") + void getMenu_Success() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + given(menuService.getMenu(any())).willReturn(mock(ResponseMenuDto.class)); + + mockMvc.perform(get("/api/v1/menus/" + menuId)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("메뉴 수정 성공 - OWNER 권한") + void updateMenu_Success() throws Exception { + mockUserSetup("owner1", UserRole.OWNER); + Map request = Map.of("name", "수정된 메뉴", "price", 12000); + given(menuService.updateMenu(any(), any(), anyString())).willReturn(mock(ResponseMenuDto.class)); + + mockMvc.perform(patch("/api/v1/menus/" + menuId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("메뉴 수정 실패 - 미인증 사용자") + void updateMenu_Unauthenticated() throws Exception { + Map request = Map.of("name", "수정된 메뉴"); + + mockMvc.perform(patch("/api/v1/menus/" + menuId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("메뉴 숨김 처리 성공 - OWNER 권한") + void updateMenuHidden_Success() throws Exception { + mockUserSetup("owner1", UserRole.OWNER); + Map request = Map.of("isHidden", true); + willDoNothing().given(menuService).updateMenuHidden(any(), any(), anyString()); + + mockMvc.perform(patch("/api/v1/menus/" + menuId + "/hidden") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("메뉴 숨김 처리 실패 - 필수값 누락 시 400") + void updateMenuHidden_BadRequest() throws Exception { + mockUserSetup("owner1", UserRole.OWNER); + Map invalidRequest = Map.of(); + + mockMvc.perform(patch("/api/v1/menus/" + menuId + "/hidden") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("메뉴 삭제 성공 - OWNER 권한") + void deleteMenu_Success() throws Exception { + mockUserSetup("owner1", UserRole.OWNER); + willDoNothing().given(menuService).deleteMenu(any(), anyString()); + + mockMvc.perform(delete("/api/v1/menus/" + menuId)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("메뉴 삭제 실패 - 미인증 사용자") + void deleteMenu_Unauthenticated() throws Exception { + mockMvc.perform(delete("/api/v1/menus/" + menuId)) + .andExpect(status().is4xxClientError()); + } +} diff --git a/src/test/java/com/sparta/delivhub/domain/menu/service/MenuServiceTest.java b/src/test/java/com/sparta/delivhub/domain/menu/service/MenuServiceTest.java index 19f4f3a..638a03a 100644 --- a/src/test/java/com/sparta/delivhub/domain/menu/service/MenuServiceTest.java +++ b/src/test/java/com/sparta/delivhub/domain/menu/service/MenuServiceTest.java @@ -1,5 +1,7 @@ package com.sparta.delivhub.domain.menu.service; +import com.sparta.delivhub.common.dto.BusinessException; +import com.sparta.delivhub.common.dto.ErrorCode; import com.sparta.delivhub.domain.ai.service.AiService; import com.sparta.delivhub.domain.menu.dto.CreateMenuDto; import com.sparta.delivhub.domain.menu.dto.HiddenMenuDto; @@ -8,6 +10,8 @@ import com.sparta.delivhub.domain.menu.dto.UpdateMenuDto; import com.sparta.delivhub.domain.menu.entity.Menu; import com.sparta.delivhub.domain.menu.repository.MenuRepository; +import com.sparta.delivhub.domain.option.dto.CreateOptionDto; +import com.sparta.delivhub.domain.option.entity.OptionType; import com.sparta.delivhub.domain.option.repository.OptionRepository; import com.sparta.delivhub.domain.store.entity.Store; import com.sparta.delivhub.domain.store.repository.StoreRepository; @@ -25,6 +29,9 @@ import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.multipart.MultipartFile; import java.time.LocalDateTime; import java.util.List; @@ -56,6 +63,7 @@ class MenuServiceTest { private UUID menuId; private Store store; private Menu menu; + private User admin; @BeforeEach void setUp() { @@ -79,97 +87,165 @@ void setUp() { lenient().when(menu.getCreatedAt()).thenReturn(LocalDateTime.now()); lenient().when(menu.getCreatedBy()).thenReturn("owner1"); - User mockUser = mock(User.class); - lenient().when(mockUser.getUserRole()).thenReturn(UserRole.MASTER); - lenient().when(userRepository.findByUsernameAndDeletedAtIsNull(any())).thenReturn(Optional.of(mockUser)); + admin = mock(User.class); + lenient().when(admin.getUsername()).thenReturn("admin"); + lenient().when(admin.getUserRole()).thenReturn(UserRole.MASTER); + lenient().when(userRepository.findByUsernameAndDeletedAtIsNull(any())).thenReturn(Optional.of(admin)); } @Test @DisplayName("메뉴 등록 성공") void createMenu() { - // given CreateMenuDto request = new CreateMenuDto(); + ReflectionTestUtils.setField(request, "name", "김치찌개"); + ReflectionTestUtils.setField(request, "price", 9000); + when(storeRepository.findById(storeId)).thenReturn(Optional.of(store)); when(menuRepository.save(any(Menu.class))).thenReturn(menu); - // when - ResponseMenuDto response = menuService.createMenu(storeId, request, null); + ResponseMenuDto response = menuService.createMenu(storeId, request, "admin"); - // then assertThat(response).isNotNull(); verify(menuRepository, times(1)).save(any(Menu.class)); } @Test - @DisplayName("메뉴 목록 조회 성공") - void getMenus() { - // given + @DisplayName("메뉴 등록 성공 - 옵션 포함") + void createMenu_WithOptions() { + CreateMenuDto request = new CreateMenuDto(); + ReflectionTestUtils.setField(request, "name", "치킨"); + ReflectionTestUtils.setField(request, "price", 20000); + + CreateOptionDto.Item item = new CreateOptionDto.Item("양념", 1000L); + CreateOptionDto optionDto = new CreateOptionDto("소스선택", OptionType.SINGLE, List.of(item)); + + ReflectionTestUtils.setField(request, "options", List.of(optionDto)); + + when(storeRepository.findById(storeId)).thenReturn(Optional.of(store)); + when(menuRepository.save(any(Menu.class))).thenReturn(menu); + + menuService.createMenu(storeId, request, "admin"); + + verify(optionRepository, times(1)).saveAll(any()); + } + + @Test + @DisplayName("메뉴 등록 실패 - AI 프롬프트 누락") + void createMenu_Fail_AiPromptRequired() { + CreateMenuDto request = new CreateMenuDto(); + ReflectionTestUtils.setField(request, "aiDescription", true); + ReflectionTestUtils.setField(request, "aiPrompt", ""); + + when(storeRepository.findById(storeId)).thenReturn(Optional.of(store)); + + assertThatThrownBy(() -> menuService.createMenu(storeId, request, "admin")) + .isInstanceOf(BusinessException.class) + .hasMessageContaining(ErrorCode.AI_PROMPT_REQUIRED.getMessage()); + } + + @Test + @DisplayName("메뉴 목록 조회 - 관리자 권한 (숨김 메뉴 포함)") + void getMenus_Admin() { Pageable pageable = PageRequest.of(0, 10); - Page menuPage = new PageImpl<>(List.of(menu)); when(storeRepository.findById(storeId)).thenReturn(Optional.of(store)); - when(menuRepository.findByStoreIdAndIsHiddenFalseAndDeletedAtIsNull(storeId, pageable)).thenReturn(menuPage); + when(menuRepository.findByStoreIdAndDeletedAtIsNull(eq(storeId), any())).thenReturn(new PageImpl<>(List.of(menu))); - // when - Page response = menuService.getMenus(storeId, pageable, false); + Page response = menuService.getMenus(storeId, pageable, true); - // then assertThat(response.getContent()).hasSize(1); + verify(menuRepository).findByStoreIdAndDeletedAtIsNull(any(), any()); } @Test - @DisplayName("메뉴 단건 조회 성공") - void getMenu() { - // given + @DisplayName("메뉴 목록 조회 실패 - 가게 없음") + void getMenus_Fail_StoreNotFound() { + when(storeRepository.findById(storeId)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> menuService.getMenus(storeId, PageRequest.of(0, 10), false)) + .isInstanceOf(BusinessException.class) + .hasMessageContaining(ErrorCode.STORE_NOT_FOUND.getMessage()); + } + + @Test + @DisplayName("메뉴 숨김 상태 수정 성공") + void updateMenuHidden() { + HiddenMenuDto request = new HiddenMenuDto(); + ReflectionTestUtils.setField(request, "isHidden", true); + when(menuRepository.findByIdAndDeletedAtIsNull(menuId)).thenReturn(Optional.of(menu)); - when(optionRepository.findAllByMenuIdWithItems(menuId)).thenReturn(List.of()); // 수정 - // when - ResponseMenuDto response = menuService.getMenu(menuId); + menuService.updateMenuHidden(menuId, request, "admin"); - // then - assertThat(response).isNotNull(); + verify(menu).updateHidden(true); + } + + @Test + @DisplayName("이미지 기반 AI 설명 생성 성공") + void generateDescriptionFromImage_Success() { + MockMultipartFile image = new MockMultipartFile("image", "test.jpg", "image/jpeg", "test data".getBytes()); + when(menuRepository.findByIdAndDeletedAtIsNull(menuId)).thenReturn(Optional.of(menu)); + when(aiService.generateDescriptionFromImage(anyString(), any(MultipartFile.class))).thenReturn("이미지 분석 결과"); + + menuService.generateDescriptionFromImage(menuId, image, "admin"); + + verify(aiService).generateDescriptionFromImage(eq("admin"), any()); + verify(menu).update(any(), any(), eq("이미지 분석 결과")); + } + + @Test + @DisplayName("이미지 기반 AI 설명 생성 실패 - 이미지 누락") + void generateDescriptionFromImage_Fail_EmptyImage() { + MockMultipartFile image = new MockMultipartFile("image", "", "image/jpeg", new byte[0]); + when(menuRepository.findByIdAndDeletedAtIsNull(menuId)).thenReturn(Optional.of(menu)); + + assertThatThrownBy(() -> menuService.generateDescriptionFromImage(menuId, image, "admin")) + .isInstanceOf(BusinessException.class) + .hasMessageContaining(ErrorCode.AI_IMAGE_REQUIRED.getMessage()); + } + + @Test + @DisplayName("메뉴 조회 실패 - 유저 없음") + void getMenuAndCheckPermission_Fail_UserNotFound() { + when(menuRepository.findByIdAndDeletedAtIsNull(menuId)).thenReturn(Optional.of(menu)); + when(userRepository.findByUsernameAndDeletedAtIsNull("none")).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> menuService.deleteMenu(menuId, "none")) + .isInstanceOf(BusinessException.class) + .hasMessageContaining(ErrorCode.USER_NOT_FOUND.getMessage()); } @Test @DisplayName("메뉴 수정 성공") void updateMenu() { - // given UpdateMenuDto request = new UpdateMenuDto(); + ReflectionTestUtils.setField(request, "name", "수정된 김치찌개"); + when(menuRepository.findByIdAndDeletedAtIsNull(menuId)).thenReturn(Optional.of(menu)); when(optionRepository.findAllByMenuIdWithItems(menuId)).thenReturn(List.of()); - // when - ResponseMenuDto response = menuService.updateMenu(menuId, request, null); + menuService.updateMenu(menuId, request, "admin"); - // then - assertThat(response).isNotNull(); - verify(menu, times(1)).update(request.getName(), request.getPrice(), request.getDescription()); + verify(menu, times(1)).update(eq("수정된 김치찌개"), any(), any()); } @Test - @DisplayName("메뉴 숨김 처리 성공") - void updateMenuHidden() { - // given - HiddenMenuDto request = new HiddenMenuDto(true); + @DisplayName("메뉴 삭제 성공") + void deleteMenu() { when(menuRepository.findByIdAndDeletedAtIsNull(menuId)).thenReturn(Optional.of(menu)); - // when - menuService.updateMenuHidden(menuId, request, null); + menuService.deleteMenu(menuId, "owner1"); - // then - verify(menu, times(1)).updateHidden(request.getIsHidden()); + verify(menu, times(1)).softDelete("owner1"); } @Test - @DisplayName("메뉴 삭제 성공") - void deleteMenu() { - // given + @DisplayName("메뉴 상세 조회 성공") + void getMenu() { when(menuRepository.findByIdAndDeletedAtIsNull(menuId)).thenReturn(Optional.of(menu)); + when(optionRepository.findAllByMenuIdWithItems(menuId)).thenReturn(List.of()); - // when - menuService.deleteMenu(menuId, null); + ResponseMenuDto response = menuService.getMenu(menuId); - // then - verify(menu, times(1)).softDelete(null); + assertThat(response).isNotNull(); } -} \ No newline at end of file +} diff --git a/src/test/java/com/sparta/delivhub/domain/option/controller/OptionControllerTest.java b/src/test/java/com/sparta/delivhub/domain/option/controller/OptionControllerTest.java new file mode 100644 index 0000000..a05fdf7 --- /dev/null +++ b/src/test/java/com/sparta/delivhub/domain/option/controller/OptionControllerTest.java @@ -0,0 +1,131 @@ +package com.sparta.delivhub.domain.option.controller; + +import com.sparta.delivhub.common.BaseControllerTest; +import com.sparta.delivhub.domain.option.dto.ResponseOptionDto; +import com.sparta.delivhub.domain.option.entity.OptionType; +import com.sparta.delivhub.domain.option.service.OptionService; +import com.sparta.delivhub.domain.user.entity.UserRole; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.Mockito.mock; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class OptionControllerTest extends BaseControllerTest { + + @MockitoBean + private OptionService optionService; + + private final UUID menuId = UUID.randomUUID(); + private final UUID optionId = UUID.randomUUID(); + + private String baseUrl() { + return "/api/v1/menus/" + menuId + "/options"; + } + + @Test + @DisplayName("옵션 등록 성공 - OWNER 권한") + void createOption_Success() throws Exception { + mockUserSetup("owner1", UserRole.OWNER); + Map request = Map.of( + "name", "사이즈", + "type", OptionType.SINGLE.name(), + "items", List.of(Map.of("name", "Large", "extraPrice", 1000L)) + ); + given(optionService.createOption(any(), any(), anyString())).willReturn(mock(ResponseOptionDto.class)); + + mockMvc.perform(post(baseUrl()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()); + } + + @Test + @DisplayName("옵션 등록 실패 - 미인증 사용자") + void createOption_Unauthenticated() throws Exception { + Map request = Map.of( + "name", "사이즈", + "type", OptionType.SINGLE.name(), + "items", List.of(Map.of("name", "Large", "extraPrice", 1000L)) + ); + + mockMvc.perform(post(baseUrl()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("옵션 등록 실패 - 필수값 누락 시 400") + void createOption_BadRequest() throws Exception { + mockUserSetup("owner1", UserRole.OWNER); + Map invalidRequest = Map.of("name", "사이즈"); + + mockMvc.perform(post(baseUrl()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("옵션 목록 조회 성공") + void getOptions_Success() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + given(optionService.getOptions(any())).willReturn(List.of()); + + mockMvc.perform(get(baseUrl())) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("옵션 수정 성공 - OWNER 권한") + void updateOption_Success() throws Exception { + mockUserSetup("owner1", UserRole.OWNER); + Map request = Map.of("name", "변경된 사이즈"); + given(optionService.updateOption(any(), any(), any(), anyString())).willReturn(mock(ResponseOptionDto.class)); + + mockMvc.perform(patch(baseUrl() + "/" + optionId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("옵션 수정 실패 - 미인증 사용자") + void updateOption_Unauthenticated() throws Exception { + Map request = Map.of("name", "변경된 사이즈"); + + mockMvc.perform(patch(baseUrl() + "/" + optionId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("옵션 삭제 성공 - OWNER 권한") + void deleteOption_Success() throws Exception { + mockUserSetup("owner1", UserRole.OWNER); + willDoNothing().given(optionService).deleteOption(any(), any(), anyString()); + + mockMvc.perform(delete(baseUrl() + "/" + optionId)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("옵션 삭제 실패 - 미인증 사용자") + void deleteOption_Unauthenticated() throws Exception { + mockMvc.perform(delete(baseUrl() + "/" + optionId)) + .andExpect(status().is4xxClientError()); + } +} diff --git a/src/test/java/com/sparta/delivhub/domain/order/service/OrderServiceTest.java b/src/test/java/com/sparta/delivhub/domain/order/service/OrderServiceTest.java index 83e8f6d..2ee4e83 100644 --- a/src/test/java/com/sparta/delivhub/domain/order/service/OrderServiceTest.java +++ b/src/test/java/com/sparta/delivhub/domain/order/service/OrderServiceTest.java @@ -226,6 +226,23 @@ void cancelOrder_Fail_TimeExceeded() { ); } + @Test + @DisplayName("주문 취소 테스트 - MASTER 권한은 5분 경과 후에도 취소 성공") + void cancelOrder_Master_Success_AfterTimeExceeded() { + // given + UUID orderId = UUID.randomUUID(); + Order order = Order.builder().userId("user01").build(); + ReflectionTestUtils.setField(order, "createdAt", LocalDateTime.now().minusMinutes(10)); + + given(orderRepository.findById(orderId)).willReturn(Optional.of(order)); + + // when + orderService.cancelOrder(orderId, "admin", "MASTER"); + + // then + assertEquals(OrderStatus.CANCELED, order.getStatus()); + } + @Test @DisplayName("페이지네이션 테스트 - 허용되지 않는 사이즈 입력 시 10으로 고정") void validatePageSize_Fallback() { @@ -238,4 +255,19 @@ void validatePageSize_Fallback() { // then verify(orderRepository).findAll(argThat((Pageable p) -> p.getPageSize() == 10)); } + + @Test + @DisplayName("주문 삭제 테스트 - 성공") + void deleteOrder_Success() { + // given + UUID orderId = UUID.randomUUID(); + Order order = Order.builder().userId("user01").build(); + given(orderRepository.findById(orderId)).willReturn(Optional.of(order)); + + // when + orderService.deleteOrder(orderId, "admin"); + + // then + assertTrue(order.isDeleted()); + } } diff --git a/src/test/java/com/sparta/delivhub/domain/order/service/controller/OrderControllerTest.java b/src/test/java/com/sparta/delivhub/domain/order/service/controller/OrderControllerTest.java new file mode 100644 index 0000000..a502b05 --- /dev/null +++ b/src/test/java/com/sparta/delivhub/domain/order/service/controller/OrderControllerTest.java @@ -0,0 +1,161 @@ +package com.sparta.delivhub.domain.order.service.controller; + +import com.sparta.delivhub.common.BaseControllerTest; +import com.sparta.delivhub.domain.order.dto.OrderRequestDto; +import com.sparta.delivhub.domain.order.dto.OrderResponseDto; +import com.sparta.delivhub.domain.order.entity.OrderType; +import com.sparta.delivhub.domain.user.entity.UserRole; +import com.sparta.delivhub.domain.order.service.OrderService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.http.MediaType; + +import java.util.List; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.Mockito.mock; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class OrderControllerTest extends BaseControllerTest { + + @MockitoBean + private OrderService orderService; + + private final String BASE_URL = "/api/v1/orders"; + + @Test + @DisplayName("성공 테스트: 정상적인 주문 생성 시 200 OK를 반환해야 함") + void createOrder_Success() throws Exception { + // Given + mockUserSetup("tester", UserRole.CUSTOMER); + OrderRequestDto requestDto = OrderRequestDto.builder() + .storeId(UUID.randomUUID()) + .addressId(UUID.randomUUID()) + .items(List.of(OrderRequestDto.OrderItemRequestDto.builder().menuId(UUID.randomUUID()).quantity(1).build())) + .orderType(OrderType.ONLINE) + .build(); + + given(orderService.createOrder(any(), anyString())).willReturn(mock(OrderResponseDto.class)); + + // When & Then: URL을 /api/v1/orders 로 정확히 요청 + mockMvc.perform(post(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto))) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("보안 테스트: 인증되지 않은 사용자가 주문 시 4xx 에러를 반환해야 함") + void createOrder_Unauthenticated() throws Exception { + OrderRequestDto requestDto = OrderRequestDto.builder().build(); + + mockMvc.perform(post(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto))) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("실패 테스트: 필수 값이 누락된 주문 요청 시 400 Bad Request를 반환해야 함") + void createOrder_BadRequest() throws Exception { + mockUserSetup("tester", UserRole.CUSTOMER); + OrderRequestDto invalidRequest = OrderRequestDto.builder() + .orderType(OrderType.ONLINE) + .build(); // storeId, addressId, items 모두 누락 + + mockMvc.perform(post(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("조회 테스트: 주문 목록 조회 API가 정상 작동해야 함") + void getOrders_Success() throws Exception { + mockUserSetup("admin", UserRole.MASTER); + + mockMvc.perform(get(BASE_URL) + .param("page", "0") + .param("size", "10")) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("주문 단건 조회 성공") + void getOrder_Success() throws Exception { + mockUserSetup("tester", UserRole.CUSTOMER); + UUID orderId = UUID.randomUUID(); + given(orderService.getOrder(any(), anyString(), anyString())).willReturn(mock(OrderResponseDto.class)); + + mockMvc.perform(get(BASE_URL + "/" + orderId)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("주문 단건 조회 실패 - 미인증 사용자") + void getOrder_Unauthenticated() throws Exception { + mockMvc.perform(get(BASE_URL + "/" + UUID.randomUUID())) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("주문 요청사항 수정 성공") + void updateRequest_Success() throws Exception { + mockUserSetup("tester", UserRole.CUSTOMER); + UUID orderId = UUID.randomUUID(); + given(orderService.updateRequest(any(), anyString(), anyString())).willReturn(mock(OrderResponseDto.class)); + + mockMvc.perform(put(BASE_URL + "/" + orderId) + .contentType(MediaType.APPLICATION_JSON) + .content("\"문 앞에 두세요\"")) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("주문 상태 변경 성공") + void updateStatus_Success() throws Exception { + mockUserSetup("owner1", UserRole.OWNER); + UUID orderId = UUID.randomUUID(); + given(orderService.updateStatus(any(), any(), anyString(), anyString())).willReturn(mock(OrderResponseDto.class)); + + mockMvc.perform(patch(BASE_URL + "/" + orderId + "/status") + .contentType(MediaType.APPLICATION_JSON) + .content("\"DELIVERING\"")) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("주문 취소 성공") + void cancelOrder_Success() throws Exception { + mockUserSetup("tester", UserRole.CUSTOMER); + UUID orderId = UUID.randomUUID(); + given(orderService.cancelOrder(any(), anyString(), anyString())).willReturn(mock(OrderResponseDto.class)); + + mockMvc.perform(patch(BASE_URL + "/" + orderId + "/cancel")) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("주문 삭제 성공 - MASTER 권한") + void deleteOrder_Success() throws Exception { + mockUserSetup("master1", UserRole.MASTER); + UUID orderId = UUID.randomUUID(); + willDoNothing().given(orderService).deleteOrder(any(), anyString()); + + mockMvc.perform(delete(BASE_URL + "/" + orderId)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("주문 삭제 실패 - 미인증 사용자") + void deleteOrder_Unauthenticated() throws Exception { + mockMvc.perform(delete(BASE_URL + "/" + UUID.randomUUID())) + .andExpect(status().is4xxClientError()); + } +} diff --git a/src/test/java/com/sparta/delivhub/domain/payment/controller/PaymentControllerTest.java b/src/test/java/com/sparta/delivhub/domain/payment/controller/PaymentControllerTest.java new file mode 100644 index 0000000..428f16a --- /dev/null +++ b/src/test/java/com/sparta/delivhub/domain/payment/controller/PaymentControllerTest.java @@ -0,0 +1,140 @@ +package com.sparta.delivhub.domain.payment.controller; + +import com.sparta.delivhub.common.BaseControllerTest; +import com.sparta.delivhub.domain.payment.dto.RequestPaymentDTO; +import com.sparta.delivhub.domain.payment.dto.RequestUpdatePaymentStatusDTO; +import com.sparta.delivhub.domain.payment.dto.ResponsePaymentDTO; +import com.sparta.delivhub.domain.payment.service.PaymentService; +import com.sparta.delivhub.domain.user.entity.UserRole; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.Mockito.mock; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class PaymentControllerTest extends BaseControllerTest { + + @MockitoBean + private PaymentService paymentService; + + private static final String BASE_URL = "/api/v1/payments"; + + @Test + @DisplayName("결제 생성 성공 - CUSTOMER 인증된 사용자") + void createPayment_Success() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + RequestPaymentDTO request = RequestPaymentDTO.builder() + .orderId(UUID.randomUUID()) + .amount(10000L) + .paymentMethod("CARD") + .build(); + given(paymentService.createPayment(any(), anyString())).willReturn(mock(ResponsePaymentDTO.class)); + + mockMvc.perform(post(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()); + } + + @Test + @DisplayName("결제 생성 실패 - 미인증 사용자 접근 시 401/403") + void createPayment_Unauthenticated() throws Exception { + RequestPaymentDTO request = RequestPaymentDTO.builder() + .orderId(UUID.randomUUID()) + .amount(10000L) + .paymentMethod("CARD") + .build(); + + mockMvc.perform(post(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("결제 생성 실패 - 필수값 누락 시 400") + void createPayment_BadRequest() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + RequestPaymentDTO invalidRequest = RequestPaymentDTO.builder().build(); + + mockMvc.perform(post(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("결제 단건 조회 성공") + void getPayment_Success() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + UUID paymentId = UUID.randomUUID(); + given(paymentService.getPayment(any(), anyString(), anyString())).willReturn(mock(ResponsePaymentDTO.class)); + + mockMvc.perform(get(BASE_URL + "/" + paymentId)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("결제 단건 조회 실패 - 미인증 사용자") + void getPayment_Unauthenticated() throws Exception { + mockMvc.perform(get(BASE_URL + "/" + UUID.randomUUID())) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("결제 상태 수정 성공 - MANAGER 권한") + void updatePaymentStatus_Success() throws Exception { + mockUserSetup("manager1", UserRole.MANAGER); + UUID paymentId = UUID.randomUUID(); + RequestUpdatePaymentStatusDTO request = RequestUpdatePaymentStatusDTO.builder() + .status("CANCELLED") + .build(); + given(paymentService.updatePaymentStatus(any(), anyString(), anyString())) + .willReturn(mock(ResponsePaymentDTO.class)); + + mockMvc.perform(patch(BASE_URL + "/" + paymentId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("결제 상태 수정 실패 - 미인증 사용자") + void updatePaymentStatus_Unauthenticated() throws Exception { + RequestUpdatePaymentStatusDTO request = RequestUpdatePaymentStatusDTO.builder() + .status("CANCELLED") + .build(); + + mockMvc.perform(patch(BASE_URL + "/" + UUID.randomUUID()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("결제 삭제 성공") + void deletePayment_Success() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + UUID paymentId = UUID.randomUUID(); + willDoNothing().given(paymentService).deletePayment(any(), anyString()); + + mockMvc.perform(delete(BASE_URL + "/" + paymentId)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("결제 삭제 실패 - 미인증 사용자") + void deletePayment_Unauthenticated() throws Exception { + mockMvc.perform(delete(BASE_URL + "/" + UUID.randomUUID())) + .andExpect(status().is4xxClientError()); + } +} diff --git a/src/test/java/com/sparta/delivhub/domain/payment/service/PaymentServiceTest.java b/src/test/java/com/sparta/delivhub/domain/payment/service/PaymentServiceTest.java index 1fcda25..8e38a30 100644 --- a/src/test/java/com/sparta/delivhub/domain/payment/service/PaymentServiceTest.java +++ b/src/test/java/com/sparta/delivhub/domain/payment/service/PaymentServiceTest.java @@ -233,5 +233,51 @@ void getMyPayments_Success() { // then assertNotNull(response); assertEquals(1, response.getContent().size()); + } + + // ========================================== + // 4. 관리자 전용 상태 수정 + // ========================================== + + @Test + @DisplayName("결제 상태 수정 성공 - 관리자 권한 (MASTER)") + void updatePaymentStatus_Success_Admin() { + // given + UUID paymentId = UUID.randomUUID(); + User admin = createMockUser("admin", UserRole.MASTER); + when(userRepository.findByUsername("admin")).thenReturn(Optional.of(admin)); + + Order order = Order.builder().userId("user123").build(); + Payment payment = Payment.builder() + .order(order) + .amount(10000L) + .paymentMethod(PaymentMethod.CARD) + .status(PaymentStatus.COMPLETED) + .build(); + ReflectionTestUtils.setField(payment, "id", paymentId); + ReflectionTestUtils.setField(payment, "createdAt", LocalDateTime.now()); + + when(paymentRepository.findById(paymentId)).thenReturn(Optional.of(payment)); + + // when + ResponsePaymentDTO response = paymentService.updatePaymentStatus(paymentId, "CANCELLED", "admin"); + + // then + assertEquals("CANCELLED", response.getStatus()); + assertEquals(PaymentStatus.CANCELLED, payment.getStatus()); } -} + + @Test + @DisplayName("결제 상태 수정 실패 - 일반 고객(CUSTOMER)의 접근 차단") + void updatePaymentStatus_Fail_AccessDenied() { + // given + UUID paymentId = UUID.randomUUID(); + when(userRepository.findByUsername(currentUserId)).thenReturn(Optional.of(customer)); + + // when & then + BusinessException exception = assertThrows(BusinessException.class, + () -> paymentService.updatePaymentStatus(paymentId, "CANCELLED", currentUserId)); + + assertEquals(ErrorCode.ACCESS_DENIED, exception.getErrorCode()); + } + } \ No newline at end of file diff --git a/src/test/java/com/sparta/delivhub/domain/review/controller/ReviewControllerTest.java b/src/test/java/com/sparta/delivhub/domain/review/controller/ReviewControllerTest.java new file mode 100644 index 0000000..95d1eeb --- /dev/null +++ b/src/test/java/com/sparta/delivhub/domain/review/controller/ReviewControllerTest.java @@ -0,0 +1,170 @@ +package com.sparta.delivhub.domain.review.controller; + +import com.sparta.delivhub.common.BaseControllerTest; +import com.sparta.delivhub.domain.review.dto.MyReviewListResponseDto; +import com.sparta.delivhub.domain.review.dto.ReviewRequestDto; +import com.sparta.delivhub.domain.review.dto.ReviewResponseDto; +import com.sparta.delivhub.domain.review.dto.ReviewUpdateRequestDto; +import com.sparta.delivhub.domain.review.dto.StoreReviewListResponseDto; +import com.sparta.delivhub.domain.review.service.ReviewService; +import com.sparta.delivhub.domain.user.entity.UserRole; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.Mockito.mock; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class ReviewControllerTest extends BaseControllerTest { + + @MockitoBean + private ReviewService reviewService; + + private static final String BASE_URL = "/api/v1/reviews"; + private final UUID reviewId = UUID.randomUUID(); + + @Test + @DisplayName("리뷰 등록 성공 - 201 CREATED") + void createReview_Success() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + ReviewRequestDto request = ReviewRequestDto.builder() + .orderId(UUID.randomUUID()) + .storeId(UUID.randomUUID()) + .rating(4) + .content("맛있어요") + .build(); + given(reviewService.createReview(any(), anyString(), anyString())).willReturn(mock(ReviewResponseDto.class)); + + mockMvc.perform(post(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()); + } + + @Test + @DisplayName("리뷰 등록 실패 - 미인증 사용자") + void createReview_Unauthenticated() throws Exception { + ReviewRequestDto request = ReviewRequestDto.builder() + .orderId(UUID.randomUUID()) + .storeId(UUID.randomUUID()) + .rating(4) + .content("맛있어요") + .build(); + + mockMvc.perform(post(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("리뷰 등록 실패 - 별점 범위 초과 시 400") + void createReview_InvalidRating_BadRequest() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + ReviewRequestDto request = ReviewRequestDto.builder() + .orderId(UUID.randomUUID()) + .storeId(UUID.randomUUID()) + .rating(6) + .content("맛있어요") + .build(); + + mockMvc.perform(post(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("리뷰 등록 실패 - 필수값 누락 시 400") + void createReview_MissingFields_BadRequest() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + ReviewRequestDto invalidRequest = ReviewRequestDto.builder().build(); + + mockMvc.perform(post(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("내 리뷰 목록 조회 성공") + void getMyReviews_Success() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + given(reviewService.getMyReviews(anyString(), anyString(), any())).willReturn(mock(MyReviewListResponseDto.class)); + + mockMvc.perform(get(BASE_URL + "/my")) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("내 리뷰 목록 조회 실패 - 미인증 사용자") + void getMyReviews_Unauthenticated() throws Exception { + mockMvc.perform(get(BASE_URL + "/my")) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("전체 리뷰 목록 조회 성공") + void getAllStoreReviews_Success() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + given(reviewService.getAllStoreReviews(any())).willReturn(mock(StoreReviewListResponseDto.class)); + + mockMvc.perform(get(BASE_URL)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("리뷰 수정 성공") + void updateReview_Success() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + ReviewUpdateRequestDto request = ReviewUpdateRequestDto.builder() + .rating(5) + .content("정말 맛있어요") + .build(); + given(reviewService.updateReview(any(), any(), anyString(), anyString())).willReturn(mock(ReviewResponseDto.class)); + + mockMvc.perform(put(BASE_URL + "/" + reviewId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("리뷰 수정 실패 - 미인증 사용자") + void updateReview_Unauthenticated() throws Exception { + ReviewUpdateRequestDto request = ReviewUpdateRequestDto.builder() + .rating(5) + .content("정말 맛있어요") + .build(); + + mockMvc.perform(put(BASE_URL + "/" + reviewId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("리뷰 삭제 성공") + void deleteReview_Success() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + willDoNothing().given(reviewService).deleteReview(any(), anyString(), anyString()); + + mockMvc.perform(delete(BASE_URL + "/" + reviewId)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("리뷰 삭제 실패 - 미인증 사용자") + void deleteReview_Unauthenticated() throws Exception { + mockMvc.perform(delete(BASE_URL + "/" + reviewId)) + .andExpect(status().is4xxClientError()); + } +} diff --git a/src/test/java/com/sparta/delivhub/domain/store/controller/StoreControllerTest.java b/src/test/java/com/sparta/delivhub/domain/store/controller/StoreControllerTest.java new file mode 100644 index 0000000..6770d3b --- /dev/null +++ b/src/test/java/com/sparta/delivhub/domain/store/controller/StoreControllerTest.java @@ -0,0 +1,168 @@ +package com.sparta.delivhub.domain.store.controller; + +import com.sparta.delivhub.common.BaseControllerTest; +import com.sparta.delivhub.domain.payment.dto.StorePaymentListResponseDto; +import com.sparta.delivhub.domain.payment.service.PaymentService; +import com.sparta.delivhub.domain.review.dto.StoreReviewPageResponseDto; +import com.sparta.delivhub.domain.review.service.ReviewService; +import com.sparta.delivhub.domain.store.dto.response.StoreDetailResponseDto; +import com.sparta.delivhub.domain.store.dto.response.StoreNameResponseDto; +import com.sparta.delivhub.domain.store.service.StoreService; +import com.sparta.delivhub.domain.user.entity.UserRole; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class StoreControllerTest extends BaseControllerTest { + + @MockitoBean + private StoreService storeService; + + @MockitoBean + private ReviewService reviewService; + + @MockitoBean + private PaymentService paymentService; + + private static final String BASE_URL = "/api/v1/stores"; + private final UUID storeId = UUID.randomUUID(); + + @Test + @DisplayName("가게 등록 성공 - OWNER 권한") + void createStore_Success() throws Exception { + mockUserSetup("owner1", UserRole.OWNER); + Map request = Map.of( + "name", "테스트 가게", + "categoryId", UUID.randomUUID().toString(), + "areaId", UUID.randomUUID().toString(), + "address", "서울시 강남구", + "number", "02-1234-5678", + "isHidden", false + ); + given(storeService.createStore(any(), anyString())).willReturn(mock(StoreDetailResponseDto.class)); + + mockMvc.perform(post(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("가게 등록 실패 - 미인증 사용자") + void createStore_Unauthenticated() throws Exception { + Map request = Map.of("name", "테스트 가게"); + + mockMvc.perform(post(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("가게 목록 조회 성공") + void getAllStores_Success() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + given(storeService.findAllStores(any())).willReturn(List.of()); + + mockMvc.perform(get(BASE_URL)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("가게 단건 조회 성공") + void getStore_Success() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + given(storeService.findStore(any())).willReturn(mock(StoreDetailResponseDto.class)); + + mockMvc.perform(get(BASE_URL + "/" + storeId)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("가게 수정 성공 - OWNER 권한") + void updateStore_Success() throws Exception { + mockUserSetup("owner1", UserRole.OWNER); + Map request = Map.of( + "name", "수정된 가게", + "categoryId", UUID.randomUUID().toString(), + "areaId", UUID.randomUUID().toString(), + "address", "서울시 서초구", + "number", "02-9999-8888", + "isHidden", false + ); + given(storeService.updateStore(any(), any(), anyString())).willReturn(mock(StoreNameResponseDto.class)); + + mockMvc.perform(put(BASE_URL + "/" + storeId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("가게 수정 실패 - 미인증 사용자") + void updateStore_Unauthenticated() throws Exception { + Map request = Map.of("name", "수정된 가게"); + + mockMvc.perform(put(BASE_URL + "/" + storeId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("가게 삭제 성공 - OWNER 권한") + void deleteStore_Success() throws Exception { + mockUserSetup("owner1", UserRole.OWNER); + given(storeService.deleteStore(any(), anyString())).willReturn(mock(StoreNameResponseDto.class)); + + mockMvc.perform(delete(BASE_URL + "/" + storeId)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("가게 삭제 실패 - 미인증 사용자") + void deleteStore_Unauthenticated() throws Exception { + mockMvc.perform(delete(BASE_URL + "/" + storeId)) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("가게 리뷰 목록 조회 성공") + void getReviewsByStore_Success() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + given(reviewService.getReviewsByStore(any(), any())).willReturn(mock(StoreReviewPageResponseDto.class)); + + mockMvc.perform(get(BASE_URL + "/" + storeId + "/reviews")) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("가게 결제 목록 조회 성공 - OWNER 권한") + void getStorePayments_Success() throws Exception { + mockUserSetup("owner1", UserRole.OWNER); + given(paymentService.getStorePayments(any(), anyString(), any())) + .willReturn(mock(StorePaymentListResponseDto.class)); + + mockMvc.perform(get(BASE_URL + "/" + storeId + "/payments")) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("가게 결제 목록 조회 실패 - 미인증 사용자") + void getStorePayments_Unauthenticated() throws Exception { + mockMvc.perform(get(BASE_URL + "/" + storeId + "/payments")) + .andExpect(status().is4xxClientError()); + } +} diff --git a/src/test/java/com/sparta/delivhub/domain/user/controller/UserControllerTest.java b/src/test/java/com/sparta/delivhub/domain/user/controller/UserControllerTest.java new file mode 100644 index 0000000..fd3368c --- /dev/null +++ b/src/test/java/com/sparta/delivhub/domain/user/controller/UserControllerTest.java @@ -0,0 +1,190 @@ +package com.sparta.delivhub.domain.user.controller; + +import com.sparta.delivhub.common.BaseControllerTest; +import com.sparta.delivhub.common.dto.PageResponse; +import com.sparta.delivhub.domain.payment.dto.MyPaymentListResponseDto; +import com.sparta.delivhub.domain.payment.service.PaymentService; +import com.sparta.delivhub.domain.user.dto.UserResponse; +import com.sparta.delivhub.domain.user.entity.UserRole; +import com.sparta.delivhub.domain.user.service.UserService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.Mockito.mock; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class UserControllerTest extends BaseControllerTest { + + @MockitoBean + private UserService userService; + + @MockitoBean + private PaymentService paymentService; + + private static final String BASE_URL = "/api/v1/users"; + + @Test + @DisplayName("내 결제 목록 조회 성공 - 인증된 사용자") + void getMyPayments_Success() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + given(paymentService.getMyPayments(anyString(), any())).willReturn(mock(MyPaymentListResponseDto.class)); + + mockMvc.perform(get(BASE_URL + "/payments")) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("내 결제 목록 조회 실패 - 미인증 사용자") + void getMyPayments_Unauthenticated() throws Exception { + mockMvc.perform(get(BASE_URL + "/payments")) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("사용자 목록 조회 성공 - MANAGER 권한") + void getUsers_AsManager_Success() throws Exception { + mockUserSetup("manager1", UserRole.MANAGER); + given(userService.getUsers(any(), any(), any())).willReturn(mock(PageResponse.class)); + + mockMvc.perform(get(BASE_URL)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("사용자 목록 조회 성공 - MASTER 권한") + void getUsers_AsMaster_Success() throws Exception { + mockUserSetup("master1", UserRole.MASTER); + given(userService.getUsers(any(), any(), any())).willReturn(mock(PageResponse.class)); + + mockMvc.perform(get(BASE_URL)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("사용자 목록 조회 실패 - CUSTOMER 권한 접근 금지") + void getUsers_AsCustomer_Forbidden() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + + mockMvc.perform(get(BASE_URL)) + .andExpect(status().isForbidden()); + } + + @Test + @DisplayName("사용자 목록 조회 실패 - 미인증 사용자") + void getUsers_Unauthenticated() throws Exception { + mockMvc.perform(get(BASE_URL)) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("사용자 단건 조회 성공 - 본인 조회") + void getUser_Self_Success() throws Exception { + mockUserSetup("user1", UserRole.CUSTOMER); + given(userService.getUser(anyString())).willReturn(mock(UserResponse.class)); + + mockMvc.perform(get(BASE_URL + "/user1")) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("사용자 단건 조회 성공 - MASTER 권한으로 타인 조회") + void getUser_AsManager_Success() throws Exception { + mockUserSetup("master1", UserRole.MASTER); + given(userService.getUser(anyString())).willReturn(mock(UserResponse.class)); + + mockMvc.perform(get(BASE_URL + "/otheruser")) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("사용자 단건 조회 실패 - 미인증 사용자") + void getUser_Unauthenticated() throws Exception { + mockMvc.perform(get(BASE_URL + "/user1")) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("사용자 정보 수정 성공 - 본인 수정") + void updateUser_Self_Success() throws Exception { + mockUserSetup("user1", UserRole.CUSTOMER); + Map request = Map.of("nickname", "newNickname"); + given(userService.updateUser(anyString(), any())).willReturn(mock(UserResponse.class)); + + mockMvc.perform(put(BASE_URL + "/user1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("사용자 정보 수정 실패 - 타인 수정 시도") + void updateUser_OtherUser_Forbidden() throws Exception { + mockUserSetup("user1", UserRole.CUSTOMER); + Map request = Map.of("nickname", "newNickname"); + + mockMvc.perform(put(BASE_URL + "/otheruser") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isForbidden()); + } + + @Test + @DisplayName("권한 변경 성공 - MASTER 권한") + void updateRole_AsMaster_Success() throws Exception { + mockUserSetup("master1", UserRole.MASTER); + Map request = Map.of("role", "OWNER"); + given(userService.updateRole(anyString(), any())).willReturn(mock(UserResponse.class)); + + mockMvc.perform(put(BASE_URL + "/targetuser/role") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("권한 변경 실패 - CUSTOMER가 권한 변경 시도") + void updateRole_AsCustomer_Forbidden() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + Map request = Map.of("role", "OWNER"); + + mockMvc.perform(put(BASE_URL + "/targetuser/role") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isForbidden()); + } + + @Test + @DisplayName("사용자 삭제 성공 - MANAGER 권한") + void deleteUser_AsManager_Success() throws Exception { + mockUserSetup("manager1", UserRole.MANAGER); + willDoNothing().given(userService).deleteUser(anyString(), anyString()); + + mockMvc.perform(delete(BASE_URL + "/targetuser")) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("사용자 삭제 실패 - CUSTOMER가 삭제 시도") + void deleteUser_AsCustomer_Forbidden() throws Exception { + mockUserSetup("customer1", UserRole.CUSTOMER); + + mockMvc.perform(delete(BASE_URL + "/targetuser")) + .andExpect(status().isForbidden()); + } + + @Test + @DisplayName("사용자 삭제 실패 - 미인증 사용자") + void deleteUser_Unauthenticated() throws Exception { + mockMvc.perform(delete(BASE_URL + "/targetuser")) + .andExpect(status().is4xxClientError()); + } +} diff --git a/src/test/java/com/sparta/delivhub/domain/user/service/UserServiceTest.java b/src/test/java/com/sparta/delivhub/domain/user/service/UserServiceTest.java index 011fb42..ead4201 100644 --- a/src/test/java/com/sparta/delivhub/domain/user/service/UserServiceTest.java +++ b/src/test/java/com/sparta/delivhub/domain/user/service/UserServiceTest.java @@ -114,6 +114,22 @@ void getUsers_success_withRole() { assertThat(response.getContent().get(0).getRole()).isEqualTo(UserRole.CUSTOMER); } + @Test + @DisplayName("유저_목록_조회_페이지_사이즈_제한_테스트") + void getUsers_fallback_pageSize() { + // given + Pageable pageable = PageRequest.of(0, 25); // 허용되지 않는 사이즈 25 + Page userPage = new PageImpl<>(List.of(user), PageRequest.of(0, 10), 1); + given(userRepository.findAll(any(Specification.class), any(Pageable.class))).willReturn(userPage); + + // when + userService.getUsers(null, null, pageable); + + // then + // 25를 넣어도 내부적으로 10으로 변환되어 리포지토리에 전달되는지 검증 + verify(userRepository).findAll(any(Specification.class), any(Pageable.class)); + } + @Test @DisplayName("유저_단건_조회_성공") void getUser_success() { diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 5d49ee0..ef2d32c 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -1,27 +1,24 @@ spring: datasource: - url: jdbc:postgresql://localhost:5432/delivhub - username: postgres - password: ${DB_PASSWORD} - driver-class-name: org.postgresql.Driver + url: jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1 + username: sa + password: + driver-class-name: org.h2.Driver jpa: - database-platform: org.hibernate.dialect.PostgreSQLDialect + database-platform: org.hibernate.dialect.H2Dialect hibernate: - ddl-auto: create-drop # 테스트마다 테이블 생성/삭제 + ddl-auto: create-drop show-sql: true properties: hibernate: format_sql: true - default_batch_fetch_size: 100 - jdbc: - time_zone: Asia/Seoul jwt: - secret: ${JWT_SECRET} + secret: j/twyolgALc1/u2Hyii351222fuFAO1vqrVfecc5lxAscSL3rYRI4bayMad3fku1 access-token-validity-ms: 3600000 refresh-token-validity-ms: 604800000 gemini: api: - key: ${GEMINI_API_KEY:test-key} + key: test-key