diff --git a/src/main/java/inu/codin/codinticketingapi/domain/ticketing/controller/EventController.java b/src/main/java/inu/codin/codinticketingapi/domain/ticketing/controller/EventController.java index 0bc06d6..c8ccb2c 100644 --- a/src/main/java/inu/codin/codinticketingapi/domain/ticketing/controller/EventController.java +++ b/src/main/java/inu/codin/codinticketingapi/domain/ticketing/controller/EventController.java @@ -7,7 +7,7 @@ import inu.codin.codinticketingapi.domain.ticketing.dto.stream.EventStockStream; import inu.codin.codinticketingapi.domain.ticketing.entity.Campus; import inu.codin.codinticketingapi.domain.ticketing.entity.ParticipationStatus; -import inu.codin.codinticketingapi.domain.ticketing.service.EventService; +import inu.codin.codinticketingapi.domain.ticketing.service.EventQueryService; import inu.codin.codinticketingapi.domain.ticketing.service.EventStockProducerService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -25,7 +25,7 @@ @Tag(name = "Event API", description = "티켓팅 이벤트 API") public class EventController { - private final EventService eventService; + private final EventQueryService eventQueryService; private final EventStockProducerService eventStockProducerService; @@ -38,7 +38,7 @@ public ResponseEntity> getEventList( @Parameter(description = "페이지", example = "0") @RequestParam("page") @NotNull int pageNumber ) { return ResponseEntity.ok(new SingleResponse<>(200, "티켓팅 이벤트 게시물 리스트 조회 성공", - eventService.getEventList(campus, pageNumber))); + eventQueryService.getEventList(campus, pageNumber))); } /** 티켓팅 이벤트 상세 정보 조회 */ @@ -49,7 +49,7 @@ public ResponseEntity> getEventDetail( @Parameter(description = "이벤트 ID", example = "1111") @PathVariable Long eventId ) { return ResponseEntity.ok(new SingleResponse<>(200, "티켓팅 이벤트 상세 정보 조회 성공", - eventService.getEventDetail(eventId))); + eventQueryService.getEventDetail(eventId))); } /** 유저 마이페이지 티켓팅 참여 전체 이력 조회 */ @@ -60,7 +60,7 @@ public ResponseEntity> get @Parameter(description = "페이지", example = "0") @RequestParam("page") @NotNull int pageNumber ) { return ResponseEntity.ok(new SingleResponse<>(200, "티켓팅 유저 티켓팅 참여 전체 이력 조회", - eventService.getUserEventList(pageNumber))); + eventQueryService.getUserEventList(pageNumber))); } /** 유저 마이페이지 티켓팅 참여 완료, 미수령, 취소 이력 조회 */ @@ -72,7 +72,7 @@ public ResponseEntity> get @Parameter(description = "티켓팀 참여 상태", example = "COMPLETED") @RequestParam("status") ParticipationStatus status ) { return ResponseEntity.ok(new SingleResponse<>(200, "유저 티켓팅 참여 (완료, 취소) 이력 조회", - eventService.getUserEventListByStatus(pageNumber, status))); + eventQueryService.getUserEventListByStatus(pageNumber, status))); } /** [테스트] 재고상태 구독자들에게 SSE 전송 */ diff --git a/src/main/java/inu/codin/codinticketingapi/domain/ticketing/dto/response/EventParticipationHistoryDto.java b/src/main/java/inu/codin/codinticketingapi/domain/ticketing/dto/response/EventParticipationHistoryDto.java index 3cf72f6..f15013f 100644 --- a/src/main/java/inu/codin/codinticketingapi/domain/ticketing/dto/response/EventParticipationHistoryDto.java +++ b/src/main/java/inu/codin/codinticketingapi/domain/ticketing/dto/response/EventParticipationHistoryDto.java @@ -4,12 +4,14 @@ import inu.codin.codinticketingapi.domain.ticketing.entity.ParticipationStatus; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import java.time.LocalDateTime; @Getter @AllArgsConstructor +@Builder @Schema(description = "티켓팅 이벤트 참여 기록 응답 DTO") public class EventParticipationHistoryDto { diff --git a/src/main/java/inu/codin/codinticketingapi/domain/ticketing/exception/TicketingErrorCode.java b/src/main/java/inu/codin/codinticketingapi/domain/ticketing/exception/TicketingErrorCode.java index 6e4d715..efef18b 100644 --- a/src/main/java/inu/codin/codinticketingapi/domain/ticketing/exception/TicketingErrorCode.java +++ b/src/main/java/inu/codin/codinticketingapi/domain/ticketing/exception/TicketingErrorCode.java @@ -19,7 +19,8 @@ public enum TicketingErrorCode implements GlobalErrorCode { PASSWORD_INVALID(HttpStatus.BAD_REQUEST, "관리자 비밀번호가 맞지 않습니다."), UNAUTHORIZED_EVENT_UPDATE(HttpStatus.UNAUTHORIZED, "인증되지 않은 이벤트 업데이트 입니다."), EVENT_ALREADY_STARTED(HttpStatus.BAD_REQUEST, "이미 이벤트가 시작했습니다."), - INSUFFICIENT_TOTAL_STOCK(HttpStatus.BAD_REQUEST, "변경하려는 재고는 남은 재고보다 적을 수 없습니다."); + INSUFFICIENT_TOTAL_STOCK(HttpStatus.BAD_REQUEST, "변경하려는 재고는 남은 재고보다 적을 수 없습니다."), + PAGE_ILLEGAL_ARGUMENT(HttpStatus.BAD_REQUEST, "페이지 번호는 1 이상이어야 합니다."); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/inu/codin/codinticketingapi/domain/ticketing/redis/RedisHealthCheckService.java b/src/main/java/inu/codin/codinticketingapi/domain/ticketing/redis/RedisHealthCheckService.java index 2ee5ff7..d1f27c0 100644 --- a/src/main/java/inu/codin/codinticketingapi/domain/ticketing/redis/RedisHealthCheckService.java +++ b/src/main/java/inu/codin/codinticketingapi/domain/ticketing/redis/RedisHealthCheckService.java @@ -1,6 +1,6 @@ package inu.codin.codinticketingapi.domain.ticketing.redis; -import inu.codin.codinticketingapi.domain.ticketing.service.EventService; +import inu.codin.codinticketingapi.domain.ticketing.service.EventCommandService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.RedisConnectionFailureException; @@ -10,11 +10,12 @@ import java.util.concurrent.atomic.AtomicInteger; -@Slf4j @Service +@Slf4j @RequiredArgsConstructor public class RedisHealthCheckService { - private final EventService eventService; + + private final EventCommandService eventCommandService; private final RedisTemplate pingRedisTemplate; // 연속 실패 횟수를 저장 (동시성 문제를 위해 AtomicInteger 사용) @@ -39,10 +40,9 @@ public void checkRedisHealth() { private void handleSuccess() { int currentFailures = consecutiveFailures.get(); - if (currentFailures >= FAILURE_THRESHOLD) { log.info("Redis 연결이 복구되었습니다. 이벤트 상태를 ACTIVE로 복원합니다."); - eventService.restoreUpcomingEventsToActive(); + eventCommandService.restoreUpcomingEventsToActive(); } else if (currentFailures > 0) { log.info("Redis 연결이 복구되었습니다."); } @@ -53,11 +53,10 @@ private void handleSuccess() { private void handleFailure() { int failures = consecutiveFailures.incrementAndGet(); log.warn("Redis 연결 실패. 연속 실패 횟수: {}회", failures); - // 정확히 임계값에 도달했을 때 딱 한 번만 실행 if (failures == FAILURE_THRESHOLD) { log.error("Redis 장애가 {}초 이상 지속되어 모든 활성 이벤트를 UPCOMING 상태로 변경합니다.", 10 * FAILURE_THRESHOLD); - eventService.changeAllActiveEventsToUpcoming(); + eventCommandService.changeAllActiveEventsToUpcoming(); } } } diff --git a/src/main/java/inu/codin/codinticketingapi/domain/ticketing/scheduler/StockCheckJob.java b/src/main/java/inu/codin/codinticketingapi/domain/ticketing/scheduler/StockCheckJob.java index c094532..0f2433d 100644 --- a/src/main/java/inu/codin/codinticketingapi/domain/ticketing/scheduler/StockCheckJob.java +++ b/src/main/java/inu/codin/codinticketingapi/domain/ticketing/scheduler/StockCheckJob.java @@ -40,9 +40,10 @@ public void execute(JobExecutionContext context) throws JobExecutionException { lastStockMap.compute(eventId, (id, prev) -> { // 최초 등록 시에는 prev == null - if (prev == null) { - return current; - } + lastStockMap.putIfAbsent(id, current); +// if (prev == null) { +// return current; +// } // 재고 변화가 감지되면 이벤트 발행 및 값 갱신 if (prev != current) { producerService.publishEventStock(new EventStockStream(eventId, (long) current)); diff --git a/src/main/java/inu/codin/codinticketingapi/domain/ticketing/service/EventCommandService.java b/src/main/java/inu/codin/codinticketingapi/domain/ticketing/service/EventCommandService.java new file mode 100644 index 0000000..002694e --- /dev/null +++ b/src/main/java/inu/codin/codinticketingapi/domain/ticketing/service/EventCommandService.java @@ -0,0 +1,47 @@ +package inu.codin.codinticketingapi.domain.ticketing.service; + +import inu.codin.codinticketingapi.domain.admin.entity.Event; +import inu.codin.codinticketingapi.domain.admin.entity.EventStatus; +import inu.codin.codinticketingapi.domain.ticketing.repository.EventRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@Slf4j +@RequiredArgsConstructor +public class EventCommandService { + + private final EventRepository eventRepository; + + @Transactional + public void changeAllActiveEventsToUpcoming() { + List activeEvents = eventRepository.findByEventStatus(EventStatus.ACTIVE); + if (activeEvents.isEmpty()) { + log.info("상태를 변경할 활성 이벤트가 없습니다."); + return; + } + // 각 이벤트의 상태를 UPCOMING으로 변경 + activeEvents.forEach(event -> { + log.info("이벤트 ID: {}, '{}'의 상태를 ACTIVE에서 UPCOMING으로 변경합니다.", event.getId(), event.getTitle()); + event.updateStatus(EventStatus.UPCOMING); + }); + } + + @Transactional + public void restoreUpcomingEventsToActive() { + List upcomingEvents = eventRepository.findAllLiveEvent(EventStatus.UPCOMING, LocalDateTime.now()); + if (upcomingEvents.isEmpty()) { + log.info("ACTIVE로 복구할 UPCOMING 상태의 이벤트가 없습니다."); + return; + } + upcomingEvents.forEach(event -> { + log.info("Redis 복구로 인해 이벤트 ID: {}의 상태를 ACTIVE로 복구합니다.", event.getId()); + event.updateStatus(EventStatus.ACTIVE); + }); + } +} diff --git a/src/main/java/inu/codin/codinticketingapi/domain/ticketing/service/EventService.java b/src/main/java/inu/codin/codinticketingapi/domain/ticketing/service/EventQueryService.java similarity index 76% rename from src/main/java/inu/codin/codinticketingapi/domain/ticketing/service/EventService.java rename to src/main/java/inu/codin/codinticketingapi/domain/ticketing/service/EventQueryService.java index 9b5ff4f..a7e8b34 100644 --- a/src/main/java/inu/codin/codinticketingapi/domain/ticketing/service/EventService.java +++ b/src/main/java/inu/codin/codinticketingapi/domain/ticketing/service/EventQueryService.java @@ -26,10 +26,10 @@ import java.time.LocalDateTime; import java.util.List; -@Slf4j @Service +@Slf4j @RequiredArgsConstructor -public class EventService { +public class EventQueryService { private final EventRepository eventRepository; private final ParticipationRepository participationRepository; @@ -63,6 +63,9 @@ public EventDetailResponse getEventDetail(Long eventId) { @Transactional(readOnly = true) public EventParticipationHistoryPageResponse getUserEventList(int pageNumber) { + if (pageNumber < 1) { + throw new TicketingException(TicketingErrorCode.PAGE_ILLEGAL_ARGUMENT); + } String userId = userClientService.fetchUser().getUserId(); Pageable pageable = PageRequest.of(pageNumber, 10, Sort.by("createdAt").descending()); return EventParticipationHistoryPageResponse.of(participationRepository.findHistoryByUserId(userId, pageable)); @@ -70,41 +73,11 @@ public EventParticipationHistoryPageResponse getUserEventList(int pageNumber) { @Transactional(readOnly = true) public EventParticipationHistoryPageResponse getUserEventListByStatus(int pageNumber, @NotNull ParticipationStatus status) { + if (pageNumber < 1) { + throw new TicketingException(TicketingErrorCode.PAGE_ILLEGAL_ARGUMENT); + } String userId = userClientService.fetchUser().getUserId(); Pageable pageable = PageRequest.of(pageNumber, 10, Sort.by("createdAt").descending()); return EventParticipationHistoryPageResponse.of(participationRepository.findHistoryByUserIdAndCanceled(userId, status, pageable)); } - - @Transactional - public void changeAllActiveEventsToUpcoming() { - List activeEvents = eventRepository.findByEventStatus(EventStatus.ACTIVE); - - if (activeEvents.isEmpty()) { - log.info("상태를 변경할 활성 이벤트가 없습니다."); - - return; - } - - // 각 이벤트의 상태를 UPCOMING으로 변경 - activeEvents.forEach(event -> { - log.info("이벤트 ID: {}, '{}'의 상태를 ACTIVE에서 UPCOMING으로 변경합니다.", event.getId(), event.getTitle()); - event.updateStatus(EventStatus.UPCOMING); - }); - } - - @Transactional - public void restoreUpcomingEventsToActive() { - List upcomingEvents = eventRepository.findAllLiveEvent(EventStatus.UPCOMING, LocalDateTime.now()); - - if (upcomingEvents.isEmpty()) { - log.info("ACTIVE로 복구할 UPCOMING 상태의 이벤트가 없습니다."); - - return; - } - - upcomingEvents.forEach(event -> { - log.info("Redis 복구로 인해 이벤트 ID: {}의 상태를 ACTIVE로 복구합니다.", event.getId()); - event.updateStatus(EventStatus.ACTIVE); - }); - } } \ No newline at end of file diff --git a/src/main/java/inu/codin/codinticketingapi/domain/ticketing/service/EventStockProducerService.java b/src/main/java/inu/codin/codinticketingapi/domain/ticketing/service/EventStockProducerService.java index f956bb4..364cf8b 100644 --- a/src/main/java/inu/codin/codinticketingapi/domain/ticketing/service/EventStockProducerService.java +++ b/src/main/java/inu/codin/codinticketingapi/domain/ticketing/service/EventStockProducerService.java @@ -24,15 +24,11 @@ public class EventStockProducerService { * EventStockStream DTO를 Redis Stream에 전송 */ public void publishEventStock(EventStockStream eventStockStream) { - // stock_event_log 테이블 추가해 로그 관리 ObjectRecord record = StreamRecords .newRecord() .in(STREAM_KEY) .ofObject(eventStockStream); - - RecordId recordId = redisTemplate.opsForStream().add(record); - + // RecordId recordId = redisTemplate.opsForStream().add(record); log.info("[Producer] Published EventStockStream: {}", eventStockStream); -// return recordId; } } diff --git a/src/main/java/inu/codin/codinticketingapi/domain/ticketing/service/ParticipationService.java b/src/main/java/inu/codin/codinticketingapi/domain/ticketing/service/ParticipationService.java index 9a6e45f..6bf1abf 100644 --- a/src/main/java/inu/codin/codinticketingapi/domain/ticketing/service/ParticipationService.java +++ b/src/main/java/inu/codin/codinticketingapi/domain/ticketing/service/ParticipationService.java @@ -53,7 +53,6 @@ public ParticipationResponse saveParticipation(Long eventId) { // 이벤트 상태 검증 if (findEvent.getEventStatus() != EventStatus.ACTIVE) { - throw new TicketingException(TicketingErrorCode.EVENT_NOT_ACTIVE); } diff --git a/src/test/java/inu/codin/codinticketingapi/domain/ticketing/controller/EventControllerTest.java b/src/test/java/inu/codin/codinticketingapi/domain/ticketing/controller/EventControllerTest.java index a92156d..327d174 100644 --- a/src/test/java/inu/codin/codinticketingapi/domain/ticketing/controller/EventControllerTest.java +++ b/src/test/java/inu/codin/codinticketingapi/domain/ticketing/controller/EventControllerTest.java @@ -1,10 +1,9 @@ package inu.codin.codinticketingapi.domain.ticketing.controller; -import com.fasterxml.jackson.databind.ObjectMapper; import inu.codin.codinticketingapi.domain.ticketing.dto.response.*; import inu.codin.codinticketingapi.domain.ticketing.entity.Campus; import inu.codin.codinticketingapi.domain.ticketing.entity.ParticipationStatus; -import inu.codin.codinticketingapi.domain.ticketing.service.EventService; +import inu.codin.codinticketingapi.domain.ticketing.service.EventQueryService; import inu.codin.codinticketingapi.domain.ticketing.service.EventStockProducerService; import inu.codin.codinticketingapi.security.jwt.TokenUserDetails; import org.junit.jupiter.api.BeforeEach; @@ -27,6 +26,7 @@ import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -34,26 +34,33 @@ @AutoConfigureMockMvc(addFilters = false) class EventControllerTest { - @Autowired - private MockMvc mockMvc; + private static final Long EVENT_ID = 1000L; + private static final String TEST_TITLE = "test-title"; + private static final String TEST_LOCATION = "test-location"; + private static final String TEST_IMAGE_URL = "test-image-url"; + private static final String TEST_TARGET = "test-target"; + private static final String TEST_DESCRIPTION = "test-description"; + private static final String TEST_USER_ID = "test-user-id"; + private static final String TEST_EMAIL = "testuser@inu.ac.kr"; + private static final String TEST_TOKEN = "test-token"; + private static final int QUANTITY = 100; + private static final int CURRENT_QUANTITY = 80; + private static final int PAGE = 0; + private static final int LAST_PAGE = -1; + private static final String CAMPUS_PARAM = "SONGDO_CAMPUS"; @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; @MockitoBean - private EventService eventService; + private EventQueryService eventQueryService; @MockitoBean private EventStockProducerService eventStockProducerService; @BeforeEach void setUp() { - TokenUserDetails userDetails = TokenUserDetails.builder() - .userId("TEST_USER_ID") - .email("testuser@inu.ac.kr") - .token("TEST_TOKEN") - .role("USER") - .build(); + TokenUserDetails userDetails = getTokenUserDetails("USER"); SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()) ); @@ -64,31 +71,18 @@ void setUp() { @DisplayName("이벤트 목록 조회 - 성공") void getEventList_Success() throws Exception { // given - EventPageDetailResponse eventDetail = EventPageDetailResponse.builder() - .eventId(1L) - .eventTitle("테스트 이벤트") - .eventImageUrl("test-image-url") - .eventTime(LocalDateTime.now().plusHours(1)) - .eventEndTime(LocalDateTime.now().plusHours(2)) - .locationInfo("테스트 위치") - .quantity(100) - .currentQuantity(80) - .build(); - - EventPageResponse eventPageResponse = EventPageResponse.of( - List.of(eventDetail), 0, -1 - ); - - when(eventService.getEventList(any(Campus.class), anyInt())) + EventPageDetailResponse eventDetail = getEventPageDetailResponse(); + EventPageResponse eventPageResponse = EventPageResponse.of(List.of(eventDetail), PAGE, LAST_PAGE); + // when + when(eventQueryService.getEventList(any(Campus.class), anyInt())) .thenReturn(eventPageResponse); - - // when & then + // then mockMvc.perform(get("/event") - .param("campus", "SONGDO_CAMPUS") - .param("page", "0")) + .param("campus", CAMPUS_PARAM) + .param("page", String.valueOf(PAGE))) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.eventList").isArray()) - .andExpect(jsonPath("$.data.eventList[0].eventId").value(1)); + .andExpect(jsonPath("$.data.eventList[0].eventId").value(EVENT_ID)); } @Test @@ -96,27 +90,14 @@ void getEventList_Success() throws Exception { @DisplayName("이벤트 상세 조회 - 성공") void getEventDetail_Success() throws Exception { // given - EventDetailResponse eventDetailResponse = EventDetailResponse.builder() - .eventId(1L) - .eventTime(LocalDateTime.now()) - .eventEndTime(LocalDateTime.now().plusHours(2)) - .eventImageUrls("test-image-url") - .eventTitle("테스트 이벤트") - .locationInfo("테스트 위치") - .quantity(100) - .currentQuantity(80) - .target("테스트 대상") - .description("테스트 설명") - .build(); - - when(eventService.getEventDetail(1L)) - .thenReturn(eventDetailResponse); - - // when & then - mockMvc.perform(get("/event/{id}", 1L)) + EventDetailResponse eventDetailResponse = getEventDetailResponse(); + // when + when(eventQueryService.getEventDetail(EVENT_ID)).thenReturn(eventDetailResponse); + // then + mockMvc.perform(get("/event/{id}", EVENT_ID)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.eventId").value(1)) - .andExpect(jsonPath("$.data.eventTitle").value("테스트 이벤트")); + .andExpect(jsonPath("$.data.eventId").value(EVENT_ID)) + .andExpect(jsonPath("$.data.eventTitle").value(TEST_TITLE)); } @Test @@ -124,51 +105,18 @@ void getEventDetail_Success() throws Exception { @DisplayName("유저 이벤트 참여 전체 이력 조회 - 성공") void getUserEventList_Success() throws Exception { // given - EventParticipationHistoryDto historyDto = new EventParticipationHistoryDto( - 1L, "테스트 이벤트", "test-image-url", "테스트 위치", - LocalDateTime.now(), LocalDateTime.now().plusHours(2), - ParticipationStatus.COMPLETED - ); - - EventParticipationHistoryPageResponse response = EventParticipationHistoryPageResponse.of( - List.of(historyDto), 0, -1 - ); - - when(eventService.getUserEventList(0)) + EventParticipationHistoryDto historyDto = getEventParticipationHistoryDto(ParticipationStatus.COMPLETED); + EventParticipationHistoryPageResponse response = EventParticipationHistoryPageResponse.of(List.of(historyDto), PAGE, LAST_PAGE); + // when + when(eventQueryService.getUserEventList(PAGE)) .thenReturn(response); - - // when & then + // then mockMvc.perform(get("/event/user") - .param("page", "0")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.eventList").isArray()) - .andExpect(jsonPath("$.data.eventList[0].eventId").value(1)); - } - - @Test - @WithMockUser(roles = "USER") - @DisplayName("유저 이벤트 참여 상태별 이력 조회 - 성공") - void getUserEventListByStatus_Success() throws Exception { - // given - EventParticipationHistoryDto historyDto = new EventParticipationHistoryDto( - 1L, "테스트 이벤트", "test-image-url", "테스트 위치", - LocalDateTime.now(), LocalDateTime.now().plusHours(2), - ParticipationStatus.COMPLETED - ); - - EventParticipationHistoryPageResponse response = EventParticipationHistoryPageResponse.of( - List.of(historyDto), 0, -1 - ); - - when(eventService.getUserEventListByStatus(anyInt(), any(ParticipationStatus.class))) - .thenReturn(response); - - // when & then - mockMvc.perform(get("/event/user/status") - .param("page", "0") - .param("status", "COMPLETED")) + .param("page", String.valueOf(PAGE))) + .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.eventList").isArray()) + .andExpect(jsonPath("$.data.eventList[0].eventId").value(EVENT_ID)) .andExpect(jsonPath("$.data.eventList[0].status").value("COMPLETED")); } @@ -176,12 +124,7 @@ void getUserEventListByStatus_Success() throws Exception { @ValueSource(strings = {"ADMIN", "MANAGER"}) @DisplayName("SSE 전송 - 권한별 성공 테스트") void sendSse_AsManager(String role) throws Exception { - TokenUserDetails userDetails = TokenUserDetails.builder() - .userId("TEST_USER_ID") - .email("testuser@inu.ac.kr") - .token("TEST_TOKEN") - .role(role) - .build(); + TokenUserDetails userDetails = getTokenUserDetails(role); SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()) ); @@ -190,11 +133,60 @@ void sendSse_AsManager(String role) throws Exception { doNothing().when(eventStockProducerService).publishEventStock(any()); // when & then - mockMvc.perform(post("/event/sse/{id}", 1L) - .param("quantity", "100")) + mockMvc.perform(post("/event/sse/{id}", EVENT_ID) + .param("quantity", String.valueOf(QUANTITY))) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.message").value("SSE 전송 성공")); verify(eventStockProducerService).publishEventStock(any()); } + + private static EventPageDetailResponse getEventPageDetailResponse() { + return EventPageDetailResponse.builder() + .eventId(EVENT_ID) + .eventTitle(TEST_TITLE) + .eventImageUrl(TEST_IMAGE_URL) + .eventTime(LocalDateTime.now().plusHours(1)) + .eventEndTime(LocalDateTime.now().plusHours(2)) + .locationInfo(TEST_LOCATION) + .quantity(QUANTITY) + .currentQuantity(CURRENT_QUANTITY) + .build(); + } + + private static TokenUserDetails getTokenUserDetails(String role) { + return TokenUserDetails.builder() + .userId(TEST_USER_ID) + .email(TEST_EMAIL) + .token(TEST_TOKEN) + .role(role) + .build(); + } + + private static EventDetailResponse getEventDetailResponse() { + return EventDetailResponse.builder() + .eventId(EVENT_ID) + .eventTime(LocalDateTime.now()) + .eventEndTime(LocalDateTime.now().plusHours(2)) + .eventImageUrls(TEST_IMAGE_URL) + .eventTitle(TEST_TITLE) + .locationInfo(TEST_LOCATION) + .quantity(QUANTITY) + .currentQuantity(CURRENT_QUANTITY) + .target(TEST_TARGET) + .description(TEST_DESCRIPTION) + .build(); + } + + private static EventParticipationHistoryDto getEventParticipationHistoryDto(ParticipationStatus status) { + return EventParticipationHistoryDto.builder() + .eventId(EVENT_ID) + .title(TEST_TITLE) + .eventImageUrl(TEST_IMAGE_URL) + .locationInfo(TEST_LOCATION) + .eventTime(LocalDateTime.now()) + .eventEndTime(LocalDateTime.now().plusHours(2)) + .status(status) + .build(); + } } diff --git a/src/test/java/inu/codin/codinticketingapi/domain/ticketing/service/ParticipationServiceTest.java b/src/test/java/inu/codin/codinticketingapi/domain/ticketing/service/ParticipationServiceTest.java index 1b1a307..5939c26 100644 --- a/src/test/java/inu/codin/codinticketingapi/domain/ticketing/service/ParticipationServiceTest.java +++ b/src/test/java/inu/codin/codinticketingapi/domain/ticketing/service/ParticipationServiceTest.java @@ -7,6 +7,7 @@ import inu.codin.codinticketingapi.domain.ticketing.entity.*; import inu.codin.codinticketingapi.domain.ticketing.exception.TicketingErrorCode; import inu.codin.codinticketingapi.domain.ticketing.exception.TicketingException; +import inu.codin.codinticketingapi.domain.ticketing.redis.RedisEventService; import inu.codin.codinticketingapi.domain.ticketing.redis.RedisParticipationService; import inu.codin.codinticketingapi.domain.ticketing.repository.EventRepository; import inu.codin.codinticketingapi.domain.ticketing.repository.ParticipationRepository; @@ -51,6 +52,8 @@ class ParticipationServiceTest { private ApplicationEventPublisher eventPublisher; @Mock private RedisParticipationService redisParticipationService; + @Mock + private RedisEventService redisEventService; private static final Long EVENT_ID = 1L; private static final String USER_ID = "testUser"; @@ -73,18 +76,12 @@ class ParticipationServiceTest { .id(EVENT_ID) .title("테스트 이벤트") .campus(Campus.SONGDO_CAMPUS) - .eventTime(LocalDateTime.now().minusDays(1)) - .eventEndTime(LocalDateTime.now().plusDays(1).plusHours(2)) + .eventTime(LocalDateTime.now().minusHours(1)) + .eventEndTime(LocalDateTime.now().plusHours(2)) .build(); event.updateStatus(EventStatus.ACTIVE); - Stock stock = Stock.builder() - .event(event) - .initialStock(100) - .build(); - stock.updateStock(99); - Participation participation = Participation.builder() .event(event) .ticketNumber(TICKET_NUMBER) @@ -94,7 +91,8 @@ class ParticipationServiceTest { given(userClientService.fetchUser()).willReturn(userInfo); given(eventRepository.findById(EVENT_ID)).willReturn(Optional.of(event)); given(redisParticipationService.getCachedParticipation(USER_ID, EVENT_ID)).willReturn(Optional.empty()); - given(participationRepository.findByUserIdAndEvent(USER_ID, event)).willReturn(Optional.empty()); + given(participationRepository.findByUserIdAndEventIdAndNotCanceled(USER_ID, EVENT_ID)).willReturn(Optional.empty()); + given(redisEventService.getTicket(EVENT_ID)).willReturn(TICKET_NUMBER); given(participationRepository.save(any(Participation.class))).willReturn(participation); // when @@ -114,7 +112,18 @@ class ParticipationServiceTest { .name(USER_NAME) .build(); // department, studentId 없음 + Event event = Event.builder() + .id(EVENT_ID) + .title("테스트 이벤트") + .campus(Campus.SONGDO_CAMPUS) + .eventTime(LocalDateTime.now().minusHours(1)) + .eventEndTime(LocalDateTime.now().plusHours(2)) + .build(); + + event.updateStatus(EventStatus.ACTIVE); + given(userClientService.fetchUser()).willReturn(userInfo); + given(eventRepository.findById(EVENT_ID)).willReturn(Optional.of(event)); // when & then assertThatThrownBy(() -> participationService.saveParticipation(EVENT_ID)) @@ -160,10 +169,12 @@ class ParticipationServiceTest { .id(EVENT_ID) .title("테스트 이벤트") .campus(Campus.SONGDO_CAMPUS) - .eventTime(LocalDateTime.now().plusDays(1)) - .eventEndTime(LocalDateTime.now().plusDays(1).plusHours(2)) + .eventTime(LocalDateTime.now().minusHours(1)) + .eventEndTime(LocalDateTime.now().plusHours(2)) .build(); + event.updateStatus(EventStatus.ACTIVE); + given(userClientService.fetchUser()).willReturn(userInfo); given(eventRepository.findById(EVENT_ID)).willReturn(Optional.of(event)); given(redisParticipationService.getCachedParticipation(USER_ID, EVENT_ID)).willReturn(Optional.of(cachedResponse)); @@ -190,7 +201,15 @@ class ParticipationServiceTest { .studentId(STUDENT_ID) .build(); - Event event = Event.builder().id(EVENT_ID).build(); + Event event = Event.builder() + .id(EVENT_ID) + .title("테스트 이벤트") + .campus(Campus.SONGDO_CAMPUS) + .eventTime(LocalDateTime.now().minusHours(1)) + .eventEndTime(LocalDateTime.now().plusHours(2)) + .build(); + + event.updateStatus(EventStatus.ACTIVE); Participation existingParticipation = Participation.builder() .event(event) @@ -201,7 +220,8 @@ class ParticipationServiceTest { given(userClientService.fetchUser()).willReturn(userInfo); given(eventRepository.findById(EVENT_ID)).willReturn(Optional.of(event)); given(redisParticipationService.getCachedParticipation(USER_ID, EVENT_ID)).willReturn(Optional.empty()); - given(participationRepository.findByUserIdAndEvent(USER_ID, event)).willReturn(Optional.of(existingParticipation)); + given(participationRepository.findByUserIdAndEventIdAndNotCanceled(USER_ID, EVENT_ID)) + .willReturn(Optional.of(existingParticipation)); // when ParticipationResponse result = participationService.saveParticipation(EVENT_ID); diff --git a/src/test/java/inu/codin/codinticketingapi/domain/ticketing/service/TicketingServiceTest.java b/src/test/java/inu/codin/codinticketingapi/domain/ticketing/service/TicketingServiceTest.java index a3c7d9e..fbe2390 100644 --- a/src/test/java/inu/codin/codinticketingapi/domain/ticketing/service/TicketingServiceTest.java +++ b/src/test/java/inu/codin/codinticketingapi/domain/ticketing/service/TicketingServiceTest.java @@ -3,13 +3,14 @@ import inu.codin.codinticketingapi.domain.admin.entity.Event; import inu.codin.codinticketingapi.domain.admin.entity.EventStatus; import inu.codin.codinticketingapi.domain.image.service.ImageService; -import inu.codin.codinticketingapi.domain.ticketing.dto.event.ParticipationStatusChangedEvent; import inu.codin.codinticketingapi.domain.ticketing.entity.Campus; import inu.codin.codinticketingapi.domain.ticketing.entity.Participation; import inu.codin.codinticketingapi.domain.ticketing.entity.ParticipationStatus; import inu.codin.codinticketingapi.domain.ticketing.entity.Stock; import inu.codin.codinticketingapi.domain.ticketing.exception.TicketingErrorCode; import inu.codin.codinticketingapi.domain.ticketing.exception.TicketingException; +import inu.codin.codinticketingapi.domain.ticketing.redis.RedisEventService; +import inu.codin.codinticketingapi.domain.ticketing.redis.RedisParticipationService; import inu.codin.codinticketingapi.domain.ticketing.repository.EventRepository; import inu.codin.codinticketingapi.domain.ticketing.repository.ParticipationRepository; import inu.codin.codinticketingapi.domain.ticketing.repository.StockRepository; @@ -52,6 +53,10 @@ class TicketingServiceTest { private ImageService imageService; @Mock private ApplicationEventPublisher eventPublisher; + @Mock + private RedisEventService redisEventService; + @Mock + private RedisParticipationService redisParticipationService; @Mock private MultipartFile signatureImage; @@ -74,63 +79,34 @@ class TicketingServiceTest { private static final int CURRENT_STOCK_50 = 50; private static final int ZERO_STOCK = 0; -// 현재 동작하지 않는 테스트라 비활성화 - -// @Test -// @DisplayName("재고 감소 - 정상") -// void decrement_성공() { -// // given -// Event mockEvent = createMockEvent(TEST_EVENT_ID, TEST_EVENT_TITLE, EventStatus.ACTIVE); -// Stock mockStock = Stock.builder() -// .event(mockEvent) -// .initialStock(INITIAL_STOCK) -// .build(); -// mockStock.updateStock(CURRENT_STOCK_50); -// -// given(stockRepository.findByEvent_Id(TEST_EVENT_ID)).willReturn(Optional.of(mockStock)); -// -// // when -// Stock result = ticketingService.decrement(TEST_EVENT_ID); -// -// // then -// assertThat(result).isNotNull(); -// assertThat(result.getRemainingStock()).isEqualTo(CURRENT_STOCK_50 - 1); -// verify(stockRepository).findByEvent_Id(TEST_EVENT_ID); -// } - @Test @DisplayName("재고 감소 - 재고 없음") void decrement_재고없음() { // given - given(stockRepository.findByEvent_Id(NON_EXISTENT_EVENT_ID)).willReturn(Optional.empty()); + doThrow(new TicketingException(TicketingErrorCode.STOCK_NOT_FOUND)) + .when(stockRepository).decrementStockByEventId(NON_EXISTENT_EVENT_ID); // when & then assertThatThrownBy(() -> ticketingService.decrement(NON_EXISTENT_EVENT_ID)) .isInstanceOf(TicketingException.class) .hasFieldOrPropertyWithValue("errorCode", TicketingErrorCode.STOCK_NOT_FOUND); - verify(stockRepository).findByEvent_Id(NON_EXISTENT_EVENT_ID); + verify(stockRepository).decrementStockByEventId(NON_EXISTENT_EVENT_ID); } @Test @DisplayName("재고 감소 - 품절") void decrement_품절() { // given - Event mockEvent = createMockEvent(TEST_EVENT_ID, TEST_EVENT_TITLE, EventStatus.ACTIVE); - Stock mockStock = Stock.builder() - .event(mockEvent) - .initialStock(INITIAL_STOCK) - .build(); - mockStock.updateStock(ZERO_STOCK); - - given(stockRepository.findByEvent_Id(TEST_EVENT_ID)).willReturn(Optional.of(mockStock)); + doThrow(new TicketingException(TicketingErrorCode.SOLD_OUT)) + .when(stockRepository).decrementStockByEventId(TEST_EVENT_ID); // when & then assertThatThrownBy(() -> ticketingService.decrement(TEST_EVENT_ID)) .isInstanceOf(TicketingException.class) .hasFieldOrPropertyWithValue("errorCode", TicketingErrorCode.SOLD_OUT); - verify(stockRepository).findByEvent_Id(TEST_EVENT_ID); + verify(stockRepository).decrementStockByEventId(TEST_EVENT_ID); } @Test @@ -277,6 +253,12 @@ class TicketingServiceTest { Event mockEvent = createMockEvent(TEST_EVENT_ID, TEST_EVENT_TITLE, EventStatus.ACTIVE); Participation mockParticipation = createMockParticipation(mockEvent, userInfo, ParticipationStatus.WAITING); + Long participationId = 123L; + int ticketNumber = 5; + + given(mockParticipation.getId()).willReturn(participationId); + given(mockParticipation.getTicketNumber()).willReturn(ticketNumber); + Stock mockStock = createMockStock(mockEvent, 10); given(userClientService.fetchUser()).willReturn(userInfo); @@ -292,10 +274,10 @@ class TicketingServiceTest { verify(eventRepository).findById(TEST_EVENT_ID); verify(participationRepository).findByEventAndUserId(mockEvent, TEST_USER_ID); verify(stockRepository).findByEvent(mockEvent); - verify(mockParticipation).changeStatusCanceled(); verify(mockStock).increase(); - - verify(eventPublisher).publishEvent(any(ParticipationStatusChangedEvent.class)); + verify(redisEventService).returnTicket(TEST_EVENT_ID, ticketNumber); + verify(participationRepository).deleteById(participationId); + verify(redisParticipationService).evictParticipation(TEST_USER_ID, TEST_EVENT_ID); } @Test diff --git a/src/test/java/inu/codin/codinticketingapi/domain/ticketing/service/event/EventCommandServiceTest.java b/src/test/java/inu/codin/codinticketingapi/domain/ticketing/service/event/EventCommandServiceTest.java new file mode 100644 index 0000000..4832303 --- /dev/null +++ b/src/test/java/inu/codin/codinticketingapi/domain/ticketing/service/event/EventCommandServiceTest.java @@ -0,0 +1,191 @@ +package inu.codin.codinticketingapi.domain.ticketing.service.event; + +import inu.codin.codinticketingapi.domain.admin.entity.Event; +import inu.codin.codinticketingapi.domain.admin.entity.EventStatus; +import inu.codin.codinticketingapi.domain.ticketing.entity.Campus; +import inu.codin.codinticketingapi.domain.ticketing.entity.Stock; +import inu.codin.codinticketingapi.domain.ticketing.repository.EventRepository; +import inu.codin.codinticketingapi.domain.ticketing.service.EventCommandService; +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 java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class EventCommandServiceTest { + + @InjectMocks + private EventCommandService eventCommandService; + + @Mock + private EventRepository eventRepository; + + private static final Long TEST_EVENT_ID_1 = 1L; + private static final Long TEST_EVENT_ID_2 = 2L; + private static final String TEST_TITLE_1 = "테스트 이벤트 1"; + private static final String TEST_TITLE_2 = "테스트 이벤트 2"; + private static final String TEST_IMAGE_URL = "http://test-image.com"; + private static final String TEST_LOCATION = "테스트 장소"; + private static final String TEST_DESCRIPTION = "테스트 설명"; + private static final String TEST_TARGET = "테스트 대상"; + private static final int TEST_INITIAL_STOCK = 100; + + @Test + @DisplayName("모든 ACTIVE 이벤트를 UPCOMING으로 변경 - 정상") + void changeAllActiveEventsToUpcoming_성공() { + // given + Event event1 = createMockEvent(TEST_EVENT_ID_1, TEST_TITLE_1, EventStatus.ACTIVE); + Event event2 = createMockEvent(TEST_EVENT_ID_2, TEST_TITLE_2, EventStatus.ACTIVE); + List activeEvents = List.of(event1, event2); + + given(eventRepository.findByEventStatus(EventStatus.ACTIVE)).willReturn(activeEvents); + + // when + eventCommandService.changeAllActiveEventsToUpcoming(); + + // then + verify(eventRepository).findByEventStatus(EventStatus.ACTIVE); + verify(event1).updateStatus(EventStatus.UPCOMING); + verify(event2).updateStatus(EventStatus.UPCOMING); + } + + @Test + @DisplayName("모든 ACTIVE 이벤트를 UPCOMING으로 변경 - 이벤트 없음") + void changeAllActiveEventsToUpcoming_이벤트없음() { + // given + given(eventRepository.findByEventStatus(EventStatus.ACTIVE)).willReturn(Collections.emptyList()); + + // when + eventCommandService.changeAllActiveEventsToUpcoming(); + + // then + verify(eventRepository).findByEventStatus(EventStatus.ACTIVE); + verify(eventRepository, never()).save(any()); + } + + @Test + @DisplayName("모든 ACTIVE 이벤트를 UPCOMING으로 변경 - 단일 이벤트") + void changeAllActiveEventsToUpcoming_단일이벤트() { + // given + Event event = createMockEvent(TEST_EVENT_ID_1, TEST_TITLE_1, EventStatus.ACTIVE); + List activeEvents = List.of(event); + + given(eventRepository.findByEventStatus(EventStatus.ACTIVE)).willReturn(activeEvents); + + // when + eventCommandService.changeAllActiveEventsToUpcoming(); + + // then + verify(eventRepository).findByEventStatus(EventStatus.ACTIVE); + verify(event).updateStatus(EventStatus.UPCOMING); + } + + @Test + @DisplayName("UPCOMING 이벤트를 ACTIVE로 복구 - 정상") + void restoreUpcomingEventsToActive_성공() { + // given + LocalDateTime now = LocalDateTime.now(); + Event event1 = createMockEvent(TEST_EVENT_ID_1, TEST_TITLE_1, EventStatus.UPCOMING); + Event event2 = createMockEvent(TEST_EVENT_ID_2, TEST_TITLE_2, EventStatus.UPCOMING); + List upcomingEvents = List.of(event1, event2); + + given(eventRepository.findAllLiveEvent(eq(EventStatus.UPCOMING), any(LocalDateTime.class))) + .willReturn(upcomingEvents); + + // when + eventCommandService.restoreUpcomingEventsToActive(); + + // then + verify(eventRepository).findAllLiveEvent(eq(EventStatus.UPCOMING), any(LocalDateTime.class)); + verify(event1).updateStatus(EventStatus.ACTIVE); + verify(event2).updateStatus(EventStatus.ACTIVE); + } + + @Test + @DisplayName("UPCOMING 이벤트를 ACTIVE로 복구 - 이벤트 없음") + void restoreUpcomingEventsToActive_이벤트없음() { + // given + given(eventRepository.findAllLiveEvent(eq(EventStatus.UPCOMING), any(LocalDateTime.class))) + .willReturn(Collections.emptyList()); + + // when + eventCommandService.restoreUpcomingEventsToActive(); + + // then + verify(eventRepository).findAllLiveEvent(eq(EventStatus.UPCOMING), any(LocalDateTime.class)); + verify(eventRepository, never()).save(any()); + } + + @Test + @DisplayName("UPCOMING 이벤트를 ACTIVE로 복구 - 단일 이벤트") + void restoreUpcomingEventsToActive_단일이벤트() { + // given + Event event = createMockEvent(TEST_EVENT_ID_1, TEST_TITLE_1, EventStatus.UPCOMING); + List upcomingEvents = List.of(event); + + given(eventRepository.findAllLiveEvent(eq(EventStatus.UPCOMING), any(LocalDateTime.class))) + .willReturn(upcomingEvents); + + // when + eventCommandService.restoreUpcomingEventsToActive(); + + // then + verify(eventRepository).findAllLiveEvent(eq(EventStatus.UPCOMING), any(LocalDateTime.class)); + verify(event).updateStatus(EventStatus.ACTIVE); + } + + @Test + @DisplayName("UPCOMING 이벤트를 ACTIVE로 복구 - 여러 이벤트") + void restoreUpcomingEventsToActive_여러이벤트() { + // given + Event event1 = createMockEvent(TEST_EVENT_ID_1, TEST_TITLE_1, EventStatus.UPCOMING); + Event event2 = createMockEvent(TEST_EVENT_ID_2, TEST_TITLE_2, EventStatus.UPCOMING); + Event event3 = createMockEvent(3L, "테스트 이벤트 3", EventStatus.UPCOMING); + List upcomingEvents = List.of(event1, event2, event3); + + given(eventRepository.findAllLiveEvent(eq(EventStatus.UPCOMING), any(LocalDateTime.class))) + .willReturn(upcomingEvents); + + // when + eventCommandService.restoreUpcomingEventsToActive(); + + // then + verify(eventRepository).findAllLiveEvent(eq(EventStatus.UPCOMING), any(LocalDateTime.class)); + verify(event1).updateStatus(EventStatus.ACTIVE); + verify(event2).updateStatus(EventStatus.ACTIVE); + verify(event3).updateStatus(EventStatus.ACTIVE); + } + + private Event createMockEvent(Long eventId, String title, EventStatus status) { + Stock stock = Stock.builder() + .initialStock(TEST_INITIAL_STOCK) + .build(); + + Event event = spy(Event.builder() + .id(eventId) + .title(title) + .campus(Campus.SONGDO_CAMPUS) + .eventTime(LocalDateTime.now().plusHours(1)) + .eventEndTime(LocalDateTime.now().plusHours(3)) + .description(TEST_DESCRIPTION) + .locationInfo(TEST_LOCATION) + .target(TEST_TARGET) + .eventImageUrl(TEST_IMAGE_URL) + .stock(stock) + .build()); + + event.updateStatus(status); + return event; + } +} diff --git a/src/test/java/inu/codin/codinticketingapi/domain/ticketing/service/EventServiceTest.java b/src/test/java/inu/codin/codinticketingapi/domain/ticketing/service/event/EventQueryServiceTest.java similarity index 62% rename from src/test/java/inu/codin/codinticketingapi/domain/ticketing/service/EventServiceTest.java rename to src/test/java/inu/codin/codinticketingapi/domain/ticketing/service/event/EventQueryServiceTest.java index a68f74b..992704b 100644 --- a/src/test/java/inu/codin/codinticketingapi/domain/ticketing/service/EventServiceTest.java +++ b/src/test/java/inu/codin/codinticketingapi/domain/ticketing/service/event/EventQueryServiceTest.java @@ -1,4 +1,4 @@ -package inu.codin.codinticketingapi.domain.ticketing.service; +package inu.codin.codinticketingapi.domain.ticketing.service.event; import inu.codin.codinticketingapi.domain.admin.entity.Event; import inu.codin.codinticketingapi.domain.ticketing.dto.response.EventDetailResponse; @@ -13,6 +13,8 @@ import inu.codin.codinticketingapi.domain.ticketing.exception.TicketingException; import inu.codin.codinticketingapi.domain.ticketing.repository.EventRepository; import inu.codin.codinticketingapi.domain.ticketing.repository.ParticipationRepository; +import inu.codin.codinticketingapi.domain.ticketing.service.EventQueryService; +import inu.codin.codinticketingapi.domain.ticketing.service.ParticipationService; import inu.codin.codinticketingapi.domain.user.dto.UserInfoResponse; import inu.codin.codinticketingapi.domain.user.service.UserClientService; import org.junit.jupiter.api.DisplayName; @@ -35,10 +37,30 @@ import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) -class EventServiceTest { +class EventQueryServiceTest { + + private static final String TEST_USER_ID_1 = "user123"; + private static final String TEST_USER_ID_2 = "user456"; + private static final Long TEST_EVENT_ID = 999L; + private static final String TEST_TITLE = "test-title"; + private static final String TEST_IMAGE_URL = "http://test-image.com"; + private static final String TEST_LOCATION = "test-location"; + private static final String TEST_DESCRIPTION = "test-description"; + private static final int TEST_INITIAL_STOCK = 100; + private static final LocalDateTime START_TIME = LocalDateTime.now().plusDays(1); + private static final LocalDateTime END_TIME = LocalDateTime.now().plusDays(1).plusHours(2); + private static final String TEST_USER_NAME = "test-user-name"; + private static final String TEST_STUDENT_ID = "201901536"; + private static final String TEST_TARGET = "test-target"; + private static final int TEST_DEFAULT_PAGE_NUM = 1; + private static final int PAGE_SIZE = 10; + private static final Sort UNSORT = Sort.unsorted(); + private static final Sort DESC_SORT = Sort.by("createdAt").descending(); + private static final Campus SONGDO = Campus.SONGDO_CAMPUS; + private static final Campus MICHUHOL = Campus.MICHUHOL_CAMPUS; @InjectMocks - private EventService eventService; + private EventQueryService eventQueryService; @Mock private EventRepository eventRepository; @@ -46,36 +68,21 @@ class EventServiceTest { private ParticipationRepository participationRepository; @Mock private UserClientService userClientService; - - private static final Campus SONGDO = Campus.SONGDO_CAMPUS; - private static final Campus MICHUHOL = Campus.MICHUHOL_CAMPUS; - private static final int PAGE_SIZE = 10; - private static final Sort DESC_SORT = Sort.by("createdAt").descending(); - private static final String TEST_EVENT_TITLE = "TEST_EVENT_TITLE"; - private static final String TEST_USER_ID_1 = "user123"; - private static final String USER_456 = "user456"; - private static final String TEST_IMAGE_URL = "http://test-image.com"; - private static final String TEST_LOCATION = "테스트 장소"; - private static final String TEST_DESCRIPTION = "테스트 이벤트 설명"; - private static final int INITIAL_STOCK = 100; - private static final LocalDateTime START_TIME = LocalDateTime.now().plusDays(1); - private static final LocalDateTime END_TIME = LocalDateTime.now().plusDays(1).plusHours(2); + @Mock + private ParticipationService participationService; @Test @DisplayName("이벤트 리스트 반환 - 정상, 페이징 메타 정보 검증") void getEventList_성공() { // given int page = 0; - Pageable pageable = PageRequest.of(page, PAGE_SIZE, DESC_SORT); - Event mockEvent = createMockEvent(1L, TEST_EVENT_TITLE, SONGDO); + Pageable pageable = PageRequest.of(page, PAGE_SIZE, UNSORT); + Event mockEvent = getMockEvent(); Page mockPage = new PageImpl<>(List.of(mockEvent), pageable, 1); - - given(eventRepository.findByCampus(SONGDO, pageable)).willReturn(mockPage); - // when - EventPageResponse result = eventService.getEventList(SONGDO, page); - + when(eventRepository.findByCampus(SONGDO, pageable)).thenReturn(mockPage); // then + EventPageResponse result = eventQueryService.getEventList(SONGDO, page); assertThat(result.getEventList()).hasSize(1); assertThat(result.getLastPage()).isEqualTo(0); assertThat(result.getNextPage()).isEqualTo(-1); @@ -87,14 +94,11 @@ class EventServiceTest { void getEventList_빈리스트() { // given int page = 0; - Pageable pageable = PageRequest.of(page, PAGE_SIZE, DESC_SORT); + Pageable pageable = PageRequest.of(page, PAGE_SIZE, UNSORT); Page emptyPage = new PageImpl<>(Collections.emptyList(), pageable, 0); - given(eventRepository.findByCampus(MICHUHOL, pageable)).willReturn(emptyPage); - // when - EventPageResponse result = eventService.getEventList(MICHUHOL, page); - + EventPageResponse result = eventQueryService.getEventList(MICHUHOL, page); // then assertThat(result.getEventList()).isEmpty(); assertThat(result.getLastPage()).isEqualTo(-1); @@ -106,16 +110,15 @@ class EventServiceTest { @DisplayName("이벤트 리스트 반환 - Campus NULL 일때") void getEventList_campusNull() { // when & then - assertThatThrownBy(() -> eventService.getEventList(null, 0)) + assertThatThrownBy(() -> eventQueryService.getEventList(null, 0)) .isInstanceOf(NullPointerException.class); } - @Test @DisplayName("이벤트 리스트 반환 - 페이지 음수 예외") void getEventList_페이지음수예외처리() { // when & then - assertThatThrownBy(() -> eventService.getEventList(SONGDO, -1)) + assertThatThrownBy(() -> eventQueryService.getEventList(SONGDO, -1)) .isInstanceOf(IllegalArgumentException.class); } @@ -124,57 +127,48 @@ void getEventList_campusNull() { void getEventList_기본페이지크기() { // given int page = 2; - Pageable expected = PageRequest.of(page, 10, Sort.by("createdAt").descending()); + Pageable expected = PageRequest.of(page, PAGE_SIZE, UNSORT); given(eventRepository.findByCampus(eq(SONGDO), any(Pageable.class))) .willReturn(new PageImpl<>(List.of(), expected, 0)); - // when - eventService.getEventList(SONGDO, page); - + eventQueryService.getEventList(SONGDO, page); // then verify(eventRepository, times(1)) - .findByCampus(eq(SONGDO), argThat(p -> p.getPageSize() == 10 && p.getPageNumber() == page)); + .findByCampus(eq(SONGDO), argThat(p -> p.getPageSize() == PAGE_SIZE && p.getPageNumber() == page)); } - @Test @DisplayName("이벤트 상세 조회 - 성공") void getEventDetail_성공() { // given - Long eventId = 999L; - UserInfoResponse userInfo = createUserInfo("testUser", "TEST_USER"); - Event mockEvent = createMockEvent(eventId, TEST_EVENT_TITLE, SONGDO); - + UserInfoResponse userInfo = createUserInfo(); + Event mockEvent = getMockEvent(); given(userClientService.fetchUser()).willReturn(userInfo); - given(eventRepository.findById(eventId)).willReturn(Optional.of(mockEvent)); - + given(participationService.isUserParticipatedInEvent(TEST_EVENT_ID)).willReturn(false); + given(eventRepository.findById(TEST_EVENT_ID)).willReturn(Optional.of(mockEvent)); // when - EventDetailResponse result = eventService.getEventDetail(eventId); - + EventDetailResponse result = eventQueryService.getEventDetail(TEST_EVENT_ID); // then - assertThat(result.getEventId()).isEqualTo(eventId); - assertThat(result.getEventTitle()).isEqualTo(TEST_EVENT_TITLE); + assertThat(result.getEventId()).isEqualTo(TEST_EVENT_ID); + assertThat(result.getEventTitle()).isEqualTo(TEST_TITLE); verify(userClientService).fetchUser(); - verify(eventRepository).findById(eventId); + verify(participationService).isUserParticipatedInEvent(TEST_EVENT_ID); + verify(eventRepository).findById(TEST_EVENT_ID); } @Test @DisplayName("이벤트 상세 조회 - 이벤트 없음 예외") void getEventDetail_이벤트존재X() { // given - Long eventId = 999L; - UserInfoResponse userInfo = createUserInfo("testUser", "TEST_USER"); - + UserInfoResponse userInfo = createUserInfo(); given(userClientService.fetchUser()).willReturn(userInfo); - given(eventRepository.findById(eventId)).willReturn(Optional.empty()); - + given(eventRepository.findById(TEST_EVENT_ID)).willReturn(Optional.empty()); // when & then - assertThatThrownBy(() -> eventService.getEventDetail(eventId)) + assertThatThrownBy(() -> eventQueryService.getEventDetail(TEST_EVENT_ID)) .isInstanceOf(TicketingException.class) .hasFieldOrPropertyWithValue("errorCode", TicketingErrorCode.EVENT_NOT_FOUND); - verify(userClientService).fetchUser(); - verify(eventRepository).findById(eventId); + verify(eventRepository).findById(TEST_EVENT_ID); } @Test @@ -182,73 +176,65 @@ void getEventList_campusNull() { void getUserEventList_성공() { // given int pageNumber = 1; - Pageable pageable = PageRequest.of(0, PAGE_SIZE, DESC_SORT); - UserInfoResponse userInfo = UserInfoResponse.builder().userId(TEST_USER_ID_1).build(); - EventParticipationHistoryDto historyDto = createMockHistoryDto(1L, "TEST HISTORY TITLE", ParticipationStatus.WAITING); + Pageable pageable = PageRequest.of(pageNumber, PAGE_SIZE, DESC_SORT); + UserInfoResponse userInfo = getUserInfoResponse(); + EventParticipationHistoryDto historyDto = getEventParticipationHistoryDto(); Page mockPage = new PageImpl<>(List.of(historyDto), pageable, 1); - given(userClientService.fetchUser()).willReturn(userInfo); given(participationRepository.findHistoryByUserId(TEST_USER_ID_1, pageable)).willReturn(mockPage); - - EventParticipationHistoryPageResponse result = eventService.getUserEventList(pageNumber); - + // when + EventParticipationHistoryPageResponse result = eventQueryService.getUserEventList(pageNumber); + // then assertThat(result.getEventList()).hasSize(1); - assertThat(result.getLastPage()).isEqualTo(0); + assertThat(result.getLastPage()).isEqualTo(pageNumber); assertThat(result.getNextPage()).isEqualTo(-1); verify(userClientService).fetchUser(); verify(participationRepository).findHistoryByUserId(TEST_USER_ID_1, pageable); } @Test - @DisplayName("사용자 이벤트 참여 내역 조회 - 페이지 음수 예외") + @DisplayName("사용자 이벤트 참여 내역 조회 - 페이지 예외") void getUserEventList_페이지음수() { - UserInfoResponse mockUser = UserInfoResponse.builder().userId("testUser").build(); - given(userClientService.fetchUser()).willReturn(mockUser); - - assertThatThrownBy(() -> eventService.getUserEventList(0)) - .isInstanceOf(IllegalArgumentException.class); + // when & then + assertThatThrownBy(() -> eventQueryService.getUserEventList(0)) + .isInstanceOf(TicketingException.class); } @Test @DisplayName("사용자 이벤트 참여 내역 조회 – 기본 페이지 크기(10)로 호출되는지 검증") void getUserEventList_기본페이지크기() { // given - given(userClientService.fetchUser()).willReturn(UserInfoResponse.builder().userId(TEST_USER_ID_1).build()); - Pageable expected = PageRequest.of(0, 10, Sort.by("createdAt").descending()); - given(participationRepository.findHistoryByUserId(eq(TEST_USER_ID_1), any(Pageable.class))) + given(userClientService.fetchUser()).willReturn(getUserInfoResponse()); + Pageable expected = PageRequest.of(TEST_DEFAULT_PAGE_NUM, PAGE_SIZE, DESC_SORT); + given(participationRepository.findHistoryByUserId(eq(TEST_USER_ID_1), eq(expected))) .willReturn(new PageImpl<>(List.of(), expected, 0)); - // when - eventService.getUserEventList(1); - + eventQueryService.getUserEventList(TEST_DEFAULT_PAGE_NUM); // then verify(participationRepository) - .findHistoryByUserId(eq(TEST_USER_ID_1), argThat(p -> p.getPageSize() == 10 && p.getPageNumber() == 0)); + .findHistoryByUserId(eq(TEST_USER_ID_1), eq(expected)); } @Test @DisplayName("상태별 사용자 이벤트 참여 내역 조회 - 완료, 취소, 대기") void getUserEventListByStatus_상태별() { - int pageNumber = 1; - Pageable pageable = PageRequest.of(0, PAGE_SIZE, DESC_SORT); - UserInfoResponse userInfo = UserInfoResponse.builder().userId(USER_456).build(); + Pageable pageable = PageRequest.of(TEST_DEFAULT_PAGE_NUM, PAGE_SIZE, DESC_SORT); + UserInfoResponse userInfo = UserInfoResponse.builder().userId(TEST_USER_ID_2).build(); for (ParticipationStatus status : ParticipationStatus.values()) { // given Page mockPage = new PageImpl<>(Collections.emptyList(), pageable, 0); given(userClientService.fetchUser()).willReturn(userInfo); - given(participationRepository.findHistoryByUserIdAndCanceled(USER_456, status, pageable)).willReturn(mockPage); - + given(participationRepository.findHistoryByUserIdAndCanceled(TEST_USER_ID_2, status, pageable)).willReturn(mockPage); // when - EventParticipationHistoryPageResponse result = eventService.getUserEventListByStatus(pageNumber, status); - + EventParticipationHistoryPageResponse result = eventQueryService.getUserEventListByStatus(TEST_DEFAULT_PAGE_NUM, status); // then assertThat(result.getEventList()).isEmpty(); assertThat(result.getLastPage()).isEqualTo(0); assertThat(result.getNextPage()).isEqualTo(-1); verify(userClientService).fetchUser(); - verify(participationRepository).findHistoryByUserIdAndCanceled(USER_456, status, pageable); - + verify(participationRepository).findHistoryByUserIdAndCanceled(TEST_USER_ID_2, status, pageable); + // reset reset(userClientService, participationRepository); } } @@ -256,10 +242,11 @@ void getEventList_campusNull() { @Test @DisplayName("상태별 사용자 이벤트 참여 내역 조회 – Status Null") void getUserEventListByStatus_statusNull() { - UserInfoResponse mockUser = UserInfoResponse.builder().userId(TEST_USER_ID_1).build(); + // given + UserInfoResponse mockUser = getUserInfoResponse(); given(userClientService.fetchUser()).willReturn(mockUser); - - assertThatThrownBy(() -> eventService.getUserEventListByStatus(1, null)) + // when & then + assertThatThrownBy(() -> eventQueryService.getUserEventListByStatus(TEST_DEFAULT_PAGE_NUM, null)) .isInstanceOf(NullPointerException.class); } @@ -268,50 +255,58 @@ void getUserEventListByStatus_statusNull() { void getUserEventListByStatus_기본페이지크기() { // given ParticipationStatus status = ParticipationStatus.COMPLETED; - given(userClientService.fetchUser()).willReturn(UserInfoResponse.builder().userId(TEST_USER_ID_1).build()); + given(userClientService.fetchUser()).willReturn(getUserInfoResponse()); - Pageable expected = PageRequest.of(1, 10, Sort.by("createdAt").descending()); - given(participationRepository.findHistoryByUserIdAndCanceled(eq(TEST_USER_ID_1), eq(status), any(Pageable.class))) + Pageable expected = PageRequest.of(TEST_DEFAULT_PAGE_NUM, PAGE_SIZE, DESC_SORT); + given(participationRepository.findHistoryByUserIdAndCanceled(eq(TEST_USER_ID_1), eq(status), eq(expected))) .willReturn(new PageImpl<>(List.of(), expected, 0)); - // when - eventService.getUserEventListByStatus(2, status); - + eventQueryService.getUserEventListByStatus(TEST_DEFAULT_PAGE_NUM, status); // then verify(participationRepository).findHistoryByUserIdAndCanceled( eq(TEST_USER_ID_1), eq(status), - argThat(p -> p.getPageSize() == 10 && p.getPageNumber() == 1) + eq(expected) ); } - private Event createMockEvent(Long id, String title, Campus campus) { - Stock stock = Stock.builder().initialStock(INITIAL_STOCK).build(); + private static UserInfoResponse getUserInfoResponse() { + return UserInfoResponse.builder().userId(TEST_USER_ID_1).build(); + } + + private Event getMockEvent() { + Stock stock = Stock.builder().initialStock(TEST_INITIAL_STOCK).build(); return Event.builder() - .id(id) - .title(title) - .campus(campus) + .id(TEST_EVENT_ID) + .title(TEST_TITLE) + .campus(SONGDO) .eventTime(START_TIME) .eventEndTime(END_TIME) .description(TEST_DESCRIPTION) .locationInfo(TEST_LOCATION) - .target("테스트 대상") + .target(TEST_TARGET) .eventImageUrl(TEST_IMAGE_URL) .stock(stock) .build(); } - private EventParticipationHistoryDto createMockHistoryDto(Long eventId, String title, ParticipationStatus status) { - return new EventParticipationHistoryDto( - eventId, title, TEST_IMAGE_URL, TEST_LOCATION, START_TIME, END_TIME, status - ); + private EventParticipationHistoryDto getEventParticipationHistoryDto() { + return EventParticipationHistoryDto.builder() + .eventId(TEST_EVENT_ID) + .title(TEST_TITLE) + .eventImageUrl(TEST_IMAGE_URL) + .locationInfo(TEST_LOCATION) + .eventTime(START_TIME) + .eventEndTime(END_TIME) + .status(ParticipationStatus.WAITING) + .build(); } - private UserInfoResponse createUserInfo(String userId, String name) { + private UserInfoResponse createUserInfo() { return UserInfoResponse.builder() - .userId(userId) - .name(name) - .studentId("2020123456") + .userId(TEST_USER_ID_1) + .name(TEST_USER_NAME) + .studentId(TEST_STUDENT_ID) .department(Department.COMPUTER_SCI) .build(); }