diff --git a/src/main/java/com/mobility/api/domain/office/controller/OfficeV1Controller.java b/src/main/java/com/mobility/api/domain/office/controller/OfficeV1Controller.java index 7097767..b60988d 100644 --- a/src/main/java/com/mobility/api/domain/office/controller/OfficeV1Controller.java +++ b/src/main/java/com/mobility/api/domain/office/controller/OfficeV1Controller.java @@ -96,16 +96,21 @@ public CommonResponse getDispatchSummary() { description = """ 최근 배차 이벤트를 시간순으로 조회합니다. - **피드 타입:** - - `open`: 배차 등록 - - `assigned`: 배차 할당 (기사 배정) + **반환되는 배차 상태 (4가지):** + - `open`: 배차 등록 (대기 중) + - `assigned`: 배차 할당 (기사 배정 완료) - `completed`: 운송 완료 - `canceled`: 배차 취소 + **제외되는 상태:** + - `HOLD`: 자동배차 진행 중 상태는 요구사항에 따라 피드에서 제외됩니다. + (HOLD는 임시 상태로, 최대 50초 이내에 OPEN 또는 ASSIGNED로 전환됨) + **특징:** - - HOLD 상태(자동배차 진행 중)는 제외됩니다. + - 현재 로그인한 사무실의 배차만 조회됩니다 (officeId 필터링) - 최신순 정렬 (createdAt DESC) - Transporter 정보 포함 (N+1 최적화 적용) + - transporterName은 assigned/completed 타입에만 값이 있고, open/canceled는 null **응답 예시:** ```json @@ -120,6 +125,15 @@ public CommonResponse getDispatchSummary() { "transporterName": "김철수", "message": "김철수 기사가 콜 #2024-0001을 배차 받았습니다", "timestamp": "2024-01-15T10:32:00" + }, + { + "id": "feed-02", + "type": "open", + "dispatchId": 124, + "dispatchNumber": "2024-0002", + "transporterName": null, + "message": "배차 #2024-0002가 등록되었습니다", + "timestamp": "2024-01-15T10:30:00" } ] } @@ -139,9 +153,10 @@ public CommonResponse> getDispatchFeed( example = "20", required = false ) - @RequestParam(required = false, defaultValue = "20") Integer limit + @RequestParam(required = false, defaultValue = "20") Integer limit, + @AuthenticationPrincipal PrincipalDetails user ) { - return CommonResponse.success(officeService.getDispatchFeed(limit)); + return CommonResponse.success(officeService.getDispatchFeed(limit, user.getManager())); } /** diff --git a/src/main/java/com/mobility/api/domain/office/service/OfficeService.java b/src/main/java/com/mobility/api/domain/office/service/OfficeService.java index d832b64..f3a6ef4 100644 --- a/src/main/java/com/mobility/api/domain/office/service/OfficeService.java +++ b/src/main/java/com/mobility/api/domain/office/service/OfficeService.java @@ -258,19 +258,23 @@ public void changeTransporterStatus(Long transporterId, TransporterStatus status /** * 대시보드 실시간 피드 조회 * @param limit 조회 개수 (기본: 20) + * @param manager 현재 로그인한 관리자 * @return 최근 배차 이벤트 피드 목록 */ @Transactional(readOnly = true) - public List getDispatchFeed(Integer limit) { + public List getDispatchFeed(Integer limit, Manager manager) { // 최근 배차 조회 (Transporter와 Fetch Join으로 N+1 문제 해결, createdAt 기준 내림차순) List recentDispatches = dispatchRepository.findAllWithTransporter( org.springframework.data.domain.PageRequest.of(0, limit, org.springframework.data.domain.Sort.by(org.springframework.data.domain.Sort.Direction.DESC, "createdAt")) ).getContent(); - // HOLD 상태 제외 후 리스트로 변환 + Long officeId = manager.getOffice().getId(); + + // HOLD 상태 제외 및 현재 사무실 배차만 필터링 List filteredDispatches = recentDispatches.stream() .filter(dispatch -> dispatch.getStatus() != StatusType.HOLD) + .filter(dispatch -> dispatch.getOfficeId().equals(officeId)) .toList(); // 각 배차를 피드 DTO로 변환 (연번 부여) diff --git a/src/test/java/com/mobility/api/domain/office/controller/OfficeV1ControllerTest.java b/src/test/java/com/mobility/api/domain/office/controller/OfficeV1ControllerTest.java new file mode 100644 index 0000000..2bcf2d8 --- /dev/null +++ b/src/test/java/com/mobility/api/domain/office/controller/OfficeV1ControllerTest.java @@ -0,0 +1,274 @@ +package com.mobility.api.domain.office.controller; + +import com.mobility.api.domain.dispatch.enums.StatusType; +import com.mobility.api.domain.office.dto.response.DispatchFeedRes; +import com.mobility.api.domain.office.entity.Manager; +import com.mobility.api.domain.office.entity.Office; +import com.mobility.api.domain.office.enums.ManagerRole; +import com.mobility.api.domain.office.repository.ManagerRepository; +import com.mobility.api.domain.office.service.OfficeService; +import com.mobility.api.domain.transporter.repository.TransporterRepository; +import com.mobility.api.global.jwt.JwtProvider; +import com.mobility.api.global.security.CustomUserDetailsService; +import com.mobility.api.global.security.PrincipalDetails; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +import java.time.LocalDateTime; +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.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +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; + +@WebMvcTest(OfficeV1Controller.class) +class OfficeV1ControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private OfficeService officeService; + + @MockitoBean + private ManagerRepository managerRepository; + + @MockitoBean + private TransporterRepository transporterRepository; + + @MockitoBean + private JwtProvider jwtProvider; + + @MockitoBean + private CustomUserDetailsService customUserDetailsService; + + @MockitoBean + private JpaMetamodelMappingContext jpaMetamodelMappingContext; + + private Manager testManager; + private Office testOffice; + private PrincipalDetails testPrincipalDetails; + + @BeforeEach + void setUp() throws Exception { + // Mock Office 생성 + testOffice = Office.builder() + .officeName("테스트 사무실") + .officeRegistrationNumber("123-45-67890") + .officeAddress("서울시 강남구") + .officeTelNumber("02-1234-5678") + .build(); + + // Reflection으로 id 설정 + java.lang.reflect.Field officeIdField = Office.class.getDeclaredField("id"); + officeIdField.setAccessible(true); + officeIdField.set(testOffice, 1L); + + // Mock Manager 생성 + testManager = Manager.builder() + .loginId("testmanager") + .password("encodedPassword") + .name("테스트 관리자") + .phone("010-1234-5678") + .email("test@example.com") + .role(ManagerRole.OWNER) + .office(testOffice) + .build(); + + // Reflection으로 id 설정 + java.lang.reflect.Field managerIdField = Manager.class.getDeclaredField("id"); + managerIdField.setAccessible(true); + managerIdField.set(testManager, 1L); + + // PrincipalDetails 생성 + testPrincipalDetails = new PrincipalDetails(testManager); + } + + @Test + @DisplayName("[API] 배차 피드 조회 - 성공 (200 OK)") + @WithMockUser + void getDispatchFeed_Success() throws Exception { + // given + Integer limit = 20; + + List mockFeedList = List.of( + DispatchFeedRes.builder() + .id("feed-01") + .type("assigned") + .dispatchId(101L) + .dispatchNumber("2024-0001") + .transporterName("김철수") + .message("김철수 기사가 콜 #2024-0001을 배차 받았습니다") + .timestamp(LocalDateTime.of(2024, 1, 15, 10, 30, 0)) + .build(), + DispatchFeedRes.builder() + .id("feed-02") + .type("open") + .dispatchId(102L) + .dispatchNumber("2024-0002") + .transporterName(null) + .message("배차 #2024-0002가 등록되었습니다") + .timestamp(LocalDateTime.of(2024, 1, 15, 10, 25, 0)) + .build(), + DispatchFeedRes.builder() + .id("feed-03") + .type("completed") + .dispatchId(103L) + .dispatchNumber("2024-0003") + .transporterName("이영희") + .message("이영희 기사가 콜 #2024-0003을 완료했습니다") + .timestamp(LocalDateTime.of(2024, 1, 15, 10, 20, 0)) + .build() + ); + + // Stubbing + given(officeService.getDispatchFeed(eq(limit), any(Manager.class))) + .willReturn(mockFeedList); + + // when & then + mockMvc.perform(get("/api/v1/office/dispatch/feed") + .param("limit", String.valueOf(limit)) + .with(user(testPrincipalDetails)) + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + + // CommonResponse 구조 검증 + .andExpect(jsonPath("$.statusCode").value(0)) + .andExpect(jsonPath("$.message").value("정상 처리 되었습니다.")) + + // 데이터(List) 검증 + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data.length()").value(3)) + + // 첫 번째 피드 검증 (assigned) + .andExpect(jsonPath("$.data[0].id").value("feed-01")) + .andExpect(jsonPath("$.data[0].type").value("assigned")) + .andExpect(jsonPath("$.data[0].dispatchId").value(101L)) + .andExpect(jsonPath("$.data[0].dispatchNumber").value("2024-0001")) + .andExpect(jsonPath("$.data[0].transporterName").value("김철수")) + .andExpect(jsonPath("$.data[0].message").value("김철수 기사가 콜 #2024-0001을 배차 받았습니다")) + + // 두 번째 피드 검증 (open) + .andExpect(jsonPath("$.data[1].id").value("feed-02")) + .andExpect(jsonPath("$.data[1].type").value("open")) + .andExpect(jsonPath("$.data[1].transporterName").doesNotExist()) + + // 세 번째 피드 검증 (completed) + .andExpect(jsonPath("$.data[2].id").value("feed-03")) + .andExpect(jsonPath("$.data[2].type").value("completed")) + .andExpect(jsonPath("$.data[2].transporterName").value("이영희")); + } + + @Test + @DisplayName("[API] 배차 피드 조회 - 기본 limit 값 사용 (limit 미지정 시 20)") + @WithMockUser + void getDispatchFeed_DefaultLimit() throws Exception { + // given + List mockFeedList = List.of( + DispatchFeedRes.builder() + .id("feed-01") + .type("open") + .dispatchId(101L) + .dispatchNumber("2024-0001") + .transporterName(null) + .message("배차 #2024-0001가 등록되었습니다") + .timestamp(LocalDateTime.now()) + .build() + ); + + // limit이 20으로 호출되어야 함 + given(officeService.getDispatchFeed(eq(20), any(Manager.class))) + .willReturn(mockFeedList); + + // when & then + mockMvc.perform(get("/api/v1/office/dispatch/feed") + .with(user(testPrincipalDetails)) + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data.length()").value(1)); + } + + @Test + @DisplayName("[API] 배차 피드 조회 - 빈 결과 (배차가 없는 경우)") + @WithMockUser + void getDispatchFeed_EmptyResult() throws Exception { + // given + Integer limit = 20; + List emptyList = List.of(); + + given(officeService.getDispatchFeed(eq(limit), any(Manager.class))) + .willReturn(emptyList); + + // when & then + mockMvc.perform(get("/api/v1/office/dispatch/feed") + .param("limit", String.valueOf(limit)) + .with(user(testPrincipalDetails)) + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(0)) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data.length()").value(0)); + } + + @Test + @DisplayName("[API] 배차 피드 조회 - 인증 없이 접근 시 실패 (401 Unauthorized)") + void getDispatchFeed_Unauthorized() throws Exception { + // given - 인증 정보 없이 요청 + + // when & then + mockMvc.perform(get("/api/v1/office/dispatch/feed") + .param("limit", "20") + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isUnauthorized()); + } + + @Test + @DisplayName("[API] 배차 피드 조회 - 커스텀 limit 값 사용 (50)") + @WithMockUser + void getDispatchFeed_CustomLimit() throws Exception { + // given + Integer customLimit = 50; + List mockFeedList = List.of( + DispatchFeedRes.builder() + .id("feed-01") + .type("canceled") + .dispatchId(101L) + .dispatchNumber("2024-0001") + .transporterName(null) + .message("배차 #2024-0001이 취소되었습니다") + .timestamp(LocalDateTime.now()) + .build() + ); + + given(officeService.getDispatchFeed(eq(customLimit), any(Manager.class))) + .willReturn(mockFeedList); + + // when & then + mockMvc.perform(get("/api/v1/office/dispatch/feed") + .param("limit", String.valueOf(customLimit)) + .with(user(testPrincipalDetails)) + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.length()").value(1)) + .andExpect(jsonPath("$.data[0].type").value("canceled")); + } +} \ No newline at end of file diff --git a/src/test/java/com/mobility/api/domain/office/service/OfficeServiceTest.java b/src/test/java/com/mobility/api/domain/office/service/OfficeServiceTest.java new file mode 100644 index 0000000..87d29b2 --- /dev/null +++ b/src/test/java/com/mobility/api/domain/office/service/OfficeServiceTest.java @@ -0,0 +1,326 @@ +package com.mobility.api.domain.office.service; + +import com.mobility.api.domain.dispatch.entity.Dispatch; +import com.mobility.api.domain.dispatch.enums.StatusType; +import com.mobility.api.domain.dispatch.repository.DispatchRepository; +import com.mobility.api.domain.office.dto.response.DispatchFeedRes; +import com.mobility.api.domain.office.entity.Manager; +import com.mobility.api.domain.office.entity.Office; +import com.mobility.api.domain.office.enums.ManagerRole; +import com.mobility.api.domain.transporter.entity.Transporter; +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.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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; + +@ExtendWith(MockitoExtension.class) +class OfficeServiceTest { + + @Mock + private DispatchRepository dispatchRepository; + + @InjectMocks + private OfficeService officeService; + + private Manager testManager; + private Office testOffice; + private Office otherOffice; + + @BeforeEach + void setUp() throws Exception { + // 테스트 사무실 1 (ID: 1) + testOffice = Office.builder() + .officeName("테스트 사무실") + .officeRegistrationNumber("123-45-67890") + .officeAddress("서울시 강남구") + .officeTelNumber("02-1234-5678") + .build(); + + // Reflection으로 id 설정 + java.lang.reflect.Field officeIdField = Office.class.getDeclaredField("id"); + officeIdField.setAccessible(true); + officeIdField.set(testOffice, 1L); + + // 테스트 매니저 (사무실 1 소속) + testManager = Manager.builder() + .loginId("testmanager") + .password("encodedPassword") + .name("테스트 관리자") + .phone("010-1234-5678") + .email("test@example.com") + .role(ManagerRole.OWNER) + .office(testOffice) + .build(); + + // Reflection으로 id 설정 + java.lang.reflect.Field managerIdField = Manager.class.getDeclaredField("id"); + managerIdField.setAccessible(true); + managerIdField.set(testManager, 1L); + + // 다른 사무실 2 (ID: 2) + otherOffice = Office.builder() + .officeName("다른 사무실") + .officeRegistrationNumber("987-65-43210") + .officeAddress("서울시 서초구") + .officeTelNumber("02-9876-5432") + .build(); + + // Reflection으로 id 설정 + officeIdField.set(otherOffice, 2L); + } + + @Test + @DisplayName("[Service] 배차 피드 조회 - 현재 사무실의 배차만 필터링") + void getDispatchFeed_FilterByOfficeId() { + // given + Integer limit = 20; + + Transporter transporter1 = Transporter.builder() + .id(1L) + .name("김철수") + .phone("010-1111-2222") + .build(); + + Transporter transporter2 = Transporter.builder() + .id(2L) + .name("이영희") + .phone("010-3333-4444") + .build(); + + // Repository에서 반환될 배차 목록 (여러 사무실 혼재) + List allDispatches = List.of( + // 사무실 1의 배차 (testOffice) - 포함되어야 함 + Dispatch.builder() + .id(101L) + .officeId(1L) // testOffice + .dispatchNumber("2024-0001") + .status(StatusType.ASSIGNED) + .transporter(transporter1) + .assignedAt(LocalDateTime.of(2024, 1, 15, 10, 30, 0)) + .createdAt(LocalDateTime.of(2024, 1, 15, 10, 0, 0)) + .build(), + + // 사무실 2의 배차 (otherOffice) - 필터링되어야 함 + Dispatch.builder() + .id(102L) + .officeId(2L) // otherOffice + .dispatchNumber("2024-0002") + .status(StatusType.OPEN) + .createdAt(LocalDateTime.of(2024, 1, 15, 10, 25, 0)) + .build(), + + // 사무실 1의 배차 (testOffice) - 포함되어야 함 + Dispatch.builder() + .id(103L) + .officeId(1L) // testOffice + .dispatchNumber("2024-0003") + .status(StatusType.COMPLETED) + .transporter(transporter2) + .completedAt(LocalDateTime.of(2024, 1, 15, 10, 20, 0)) + .createdAt(LocalDateTime.of(2024, 1, 15, 9, 50, 0)) + .build(), + + // HOLD 상태 배차 (사무실 1) - 필터링되어야 함 + Dispatch.builder() + .id(104L) + .officeId(1L) // testOffice + .dispatchNumber("2024-0004") + .status(StatusType.HOLD) + .createdAt(LocalDateTime.of(2024, 1, 15, 10, 15, 0)) + .build(), + + // 사무실 1의 배차 (testOffice) - 포함되어야 함 + Dispatch.builder() + .id(105L) + .officeId(1L) // testOffice + .dispatchNumber("2024-0005") + .status(StatusType.CANCELED) + .canceledAt(LocalDateTime.of(2024, 1, 15, 10, 10, 0)) + .createdAt(LocalDateTime.of(2024, 1, 15, 9, 40, 0)) + .build() + ); + + Page dispatchPage = new PageImpl<>(allDispatches); + + given(dispatchRepository.findAllWithTransporter(any(Pageable.class))) + .willReturn(dispatchPage); + + // when + List result = officeService.getDispatchFeed(limit, testManager); + + // then + // 1. 결과 개수 검증: 사무실 1의 배차만 포함되고 HOLD 상태는 제외 + // ID: 101(ASSIGNED), 103(COMPLETED), 105(CANCELED) = 3개 + assertThat(result).hasSize(3); + + // 2. 사무실 1의 배차만 포함되었는지 검증 + assertThat(result) + .extracting(DispatchFeedRes::dispatchId) + .containsExactly(101L, 103L, 105L); + + // 3. 사무실 2의 배차(ID: 102)는 제외되었는지 확인 + assertThat(result) + .extracting(DispatchFeedRes::dispatchId) + .doesNotContain(102L); + + // 4. HOLD 상태 배차(ID: 104)는 제외되었는지 확인 + assertThat(result) + .extracting(DispatchFeedRes::dispatchId) + .doesNotContain(104L); + + // 5. 각 피드의 타입 검증 + assertThat(result.get(0).type()).isEqualTo("assigned"); + assertThat(result.get(1).type()).isEqualTo("completed"); + assertThat(result.get(2).type()).isEqualTo("canceled"); + } + + @Test + @DisplayName("[Service] 배차 피드 조회 - HOLD 상태는 항상 제외") + void getDispatchFeed_ExcludeHoldStatus() { + // given + Integer limit = 20; + + List allDispatches = List.of( + Dispatch.builder() + .id(101L) + .officeId(1L) + .dispatchNumber("2024-0001") + .status(StatusType.HOLD) + .createdAt(LocalDateTime.now()) + .build(), + Dispatch.builder() + .id(102L) + .officeId(1L) + .dispatchNumber("2024-0002") + .status(StatusType.OPEN) + .createdAt(LocalDateTime.now()) + .build() + ); + + Page dispatchPage = new PageImpl<>(allDispatches); + + given(dispatchRepository.findAllWithTransporter(any(Pageable.class))) + .willReturn(dispatchPage); + + // when + List result = officeService.getDispatchFeed(limit, testManager); + + // then + assertThat(result).hasSize(1); + assertThat(result.get(0).dispatchId()).isEqualTo(102L); + assertThat(result.get(0).type()).isEqualTo("open"); + } + + @Test + @DisplayName("[Service] 배차 피드 조회 - 빈 결과 반환") + void getDispatchFeed_EmptyResult() { + // given + Integer limit = 20; + Page emptyPage = new PageImpl<>(List.of()); + + given(dispatchRepository.findAllWithTransporter(any(Pageable.class))) + .willReturn(emptyPage); + + // when + List result = officeService.getDispatchFeed(limit, testManager); + + // then + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("[Service] 배차 피드 조회 - 다른 사무실 배차는 모두 제외") + void getDispatchFeed_FilterOutAllOtherOfficeDispatches() { + // given + Integer limit = 20; + + // 모든 배차가 다른 사무실의 것 + List allDispatches = List.of( + Dispatch.builder() + .id(201L) + .officeId(2L) // otherOffice + .dispatchNumber("2024-0001") + .status(StatusType.OPEN) + .createdAt(LocalDateTime.now()) + .build(), + Dispatch.builder() + .id(202L) + .officeId(3L) // another office + .dispatchNumber("2024-0002") + .status(StatusType.ASSIGNED) + .createdAt(LocalDateTime.now()) + .build() + ); + + Page dispatchPage = new PageImpl<>(allDispatches); + + given(dispatchRepository.findAllWithTransporter(any(Pageable.class))) + .willReturn(dispatchPage); + + // when + List result = officeService.getDispatchFeed(limit, testManager); + + // then + // 모든 배차가 다른 사무실의 것이므로 결과는 비어있어야 함 + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("[Service] 배차 피드 조회 - 피드 ID 연번 생성 검증") + void getDispatchFeed_FeedIdSequence() { + // given + Integer limit = 20; + + List allDispatches = List.of( + Dispatch.builder() + .id(101L) + .officeId(1L) + .dispatchNumber("2024-0001") + .status(StatusType.OPEN) + .createdAt(LocalDateTime.now()) + .build(), + Dispatch.builder() + .id(102L) + .officeId(1L) + .dispatchNumber("2024-0002") + .status(StatusType.ASSIGNED) + .createdAt(LocalDateTime.now()) + .build(), + Dispatch.builder() + .id(103L) + .officeId(1L) + .dispatchNumber("2024-0003") + .status(StatusType.COMPLETED) + .createdAt(LocalDateTime.now()) + .build() + ); + + Page dispatchPage = new PageImpl<>(allDispatches); + + given(dispatchRepository.findAllWithTransporter(any(Pageable.class))) + .willReturn(dispatchPage); + + // when + List result = officeService.getDispatchFeed(limit, testManager); + + // then + assertThat(result).hasSize(3); + assertThat(result.get(0).id()).isEqualTo("feed-01"); + assertThat(result.get(1).id()).isEqualTo("feed-02"); + assertThat(result.get(2).id()).isEqualTo("feed-03"); + } +}