From 88a316c0f0ade316b183260afdf2de6c8e721708 Mon Sep 17 00:00:00 2001 From: js0ny Date: Thu, 27 Nov 2025 13:56:41 +0000 Subject: [PATCH] feat(cw2): Drone related test --- .gitignore | 1 + .../service/DroneInfoService.java | 12 +- .../controller/DroneControllerTest.java | 392 ++++++++++++++++++ .../service/DroneInfoServiceTest.java | 204 +++++++++ 4 files changed, 606 insertions(+), 3 deletions(-) create mode 100644 src/test/java/io/github/js0ny/ilp_coursework/controller/DroneControllerTest.java create mode 100644 src/test/java/io/github/js0ny/ilp_coursework/service/DroneInfoServiceTest.java diff --git a/.gitignore b/.gitignore index e502041..06eb083 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ out/ .direnv/ .envrc localjson +ilp-cw-api/results.json diff --git a/src/main/java/io/github/js0ny/ilp_coursework/service/DroneInfoService.java b/src/main/java/io/github/js0ny/ilp_coursework/service/DroneInfoService.java index 829fff6..29f101a 100644 --- a/src/main/java/io/github/js0ny/ilp_coursework/service/DroneInfoService.java +++ b/src/main/java/io/github/js0ny/ilp_coursework/service/DroneInfoService.java @@ -28,12 +28,17 @@ public class DroneInfoService { public static final String servicePointsEndpoint = "service-points"; public static final String restrictedAreasEndpoint = "restricted-areas"; - private final RestTemplate restTemplate = new RestTemplate(); + private final RestTemplate restTemplate; /** * Constructor, handles the base url here. */ public DroneInfoService() { + this(new RestTemplate()); + } + + public DroneInfoService(RestTemplate restTemplate) { + this.restTemplate = restTemplate; String baseUrl = System.getenv("ILP_ENDPOINT"); if (baseUrl == null || baseUrl.isBlank()) { this.baseUrl = @@ -268,11 +273,12 @@ public class DroneInfoService { } public List fetchAllDrones() { + System.out.println("fetchAllDrones called"); String dronesEndpoint = "drones"; URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint); + System.out.println("Fetching from URL: " + droneUrl); Drone[] drones = restTemplate.getForObject(droneUrl, Drone[].class); - assert drones != null; - return Arrays.asList(drones); + return drones == null ? new ArrayList<>() : Arrays.asList(drones); } public List fetchRestrictedAreas() { diff --git a/src/test/java/io/github/js0ny/ilp_coursework/controller/DroneControllerTest.java b/src/test/java/io/github/js0ny/ilp_coursework/controller/DroneControllerTest.java new file mode 100644 index 0000000..a9ae4f8 --- /dev/null +++ b/src/test/java/io/github/js0ny/ilp_coursework/controller/DroneControllerTest.java @@ -0,0 +1,392 @@ +package io.github.js0ny.ilp_coursework.controller; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; +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.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import io.github.js0ny.ilp_coursework.data.common.DroneCapability; +import io.github.js0ny.ilp_coursework.data.common.LngLat; +import io.github.js0ny.ilp_coursework.data.external.Drone; +import io.github.js0ny.ilp_coursework.data.request.AttrQueryRequest; +import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest; +import io.github.js0ny.ilp_coursework.service.DroneAttrComparatorService; +import io.github.js0ny.ilp_coursework.service.DroneInfoService; +import io.github.js0ny.ilp_coursework.service.PathFinderService; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +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.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(DroneController.class) +public class DroneControllerTest { + + @Autowired + private MockMvc mockMvc; + + private ObjectMapper objectMapper; + + @MockitoBean + private DroneInfoService droneInfoService; + + @MockitoBean + private DroneAttrComparatorService droneAttrComparatorService; + + @MockitoBean + private PathFinderService pathFinderService; + + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + } + + @Nested + @DisplayName("GET /dronesWithCooling/{state}") + class GetDronesWithCoolingTest { + + final String API_ENDPOINT_BASE = "/api/v1/dronesWithCooling/"; + + @Test + @DisplayName("true -> 200 OK") + void getDronesWithCooling_shouldReturn200AndArrayOfString_whenStateIsTrue() + throws Exception { + String endpoint = API_ENDPOINT_BASE + "true"; + List expected = List.of("1", "5", "8", "9"); + when( + droneInfoService.dronesWithCooling(anyBoolean())).thenReturn(expected); + var mock = mockMvc.perform( + get(endpoint).contentType(MediaType.APPLICATION_JSON)); + + mock.andExpect(status().isOk()); + mock.andExpect( + content().json(objectMapper.writeValueAsString(expected))); + } + + @Test + @DisplayName("false -> 200 OK") + void getDronesWithCooling_shouldReturn200AndArrayOfString_whenStateIsFalse() + throws Exception { + String endpoint = API_ENDPOINT_BASE + "false"; + List expected = List.of("2", "3", "4", "6", "7", "10"); + when( + droneInfoService.dronesWithCooling(anyBoolean())).thenReturn(expected); + var mock = mockMvc.perform( + get(endpoint).contentType(MediaType.APPLICATION_JSON)); + + mock.andExpect(status().isOk()); + mock.andExpect( + content().json(objectMapper.writeValueAsString(expected))); + } + + @Test + @DisplayName("-> 400 Bad Request") + void getDronesWithCooling_shouldReturn400_whenStateIsInvalid() + throws Exception { + String endpoint = API_ENDPOINT_BASE + "invalid"; + mockMvc.perform( + get(endpoint).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + } + + @Nested + @DisplayName("GET /droneDetails/{droneId}") + class DroneDetailsTest { + + static final String API_ENDPOINT_BASE = "/api/v1/droneDetails/"; + + @Test + @DisplayName("-> 200 OK") + void getDroneDetails_shouldReturn200AndJson_whenCorrectInput() + throws Exception { + Drone expected = new Drone("Drone 1", "1", + new DroneCapability(true, true, 4.0f, 2000, 0.01f, 4.3f, 6.5f)); + String endpoint = API_ENDPOINT_BASE + "1"; + when( + droneInfoService.droneDetail(anyString())).thenReturn(expected); + var mock = mockMvc.perform( + get(endpoint).contentType(MediaType.APPLICATION_JSON)); + + mock.andExpect(status().isOk()); + mock.andExpect( + content().json(objectMapper.writeValueAsString(expected))); + } + + @Test + @DisplayName("-> 404 Not Found") + void getDroneDetails_shouldReturn404_whenDroneNotFound() + throws Exception { + String endpoint = API_ENDPOINT_BASE + "invalidDroneId"; + when( + droneInfoService.droneDetail(anyString())).thenThrow(new IllegalArgumentException()); + mockMvc.perform( + get(endpoint).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + } + + @Nested + @DisplayName("GET /queryAsPath/{attrName}/{attrVal}") + class GetQueryAsPathTests { + + final String API_ENDPOINT_BASE = "/api/v1/queryAsPath/"; + + @Test + @DisplayName("capacity = 8 -> 200 OK") + void getQueryAsPath_shouldReturn200AndArrayOfString_whenCapacityIs8() + throws Exception { + String attrName = "capacity"; + String attrVal = "8"; + List expected = List.of("2", "4", "7", "9"); + when(droneAttrComparatorService.dronesWithAttribute(attrName, attrVal)) + .thenReturn(expected); + + mockMvc.perform(get(API_ENDPOINT_BASE + attrName + "/" + attrVal) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(expected))); + } + + @Test + @DisplayName("heating = true -> 200 OK") + void getQueryAsPath_shouldReturn200AndArrayOfString_whenHeatingIsTrue() + throws Exception { + String attrName = "heating"; + String attrVal = "true"; + List expected = List.of("1", "2", "4", "5", "6", "7", "9"); + when(droneAttrComparatorService.dronesWithAttribute(attrName, attrVal)) + .thenReturn(expected); + + mockMvc.perform(get(API_ENDPOINT_BASE + attrName + "/" + attrVal) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(expected))); + } + + @Test + @DisplayName("cooling = false -> 200 OK") + void getQueryAsPath_shouldReturn200AndArrayOfString_whenCoolingIsFalse() + throws Exception { + String attrName = "cooling"; + String attrVal = "false"; + List expected = List.of("2", "3", "4", "6", "7", "10"); + when(droneAttrComparatorService.dronesWithAttribute(attrName, attrVal)) + .thenReturn(expected); + + mockMvc.perform(get(API_ENDPOINT_BASE + attrName + "/" + attrVal) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(expected))); + } + + @Test + @DisplayName("maxMoves = 1000 -> 200 OK") + void getQueryAsPath_shouldReturn200AndArrayOfString_whenMaxMovesIs1000() + throws Exception { + String attrName = "maxMoves"; + String attrVal = "1000"; + List expected = List.of("2", "4", "7", "9"); + when(droneAttrComparatorService.dronesWithAttribute(attrName, attrVal)) + .thenReturn(expected); + + mockMvc.perform(get(API_ENDPOINT_BASE + attrName + "/" + attrVal) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(expected))); + } + + @Test + @DisplayName("invalid = null -> 200 OK (empty list)") + void getQueryAsPath_shouldReturn200AndEmptyArrayOfString_whenInvalidAttribute() + throws Exception { + String attrName = "invalid"; + String attrVal = "null"; + List expected = List.of(); + when(droneAttrComparatorService.dronesWithAttribute(attrName, attrVal)) + .thenReturn(expected); + + mockMvc.perform(get(API_ENDPOINT_BASE + attrName + "/" + attrVal) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(expected))); + } + } + + @Nested + @DisplayName("POST /query") + class PostQueryTests { + + final String API_ENDPOINT = "/api/v1/query"; + + @Test + @DisplayName("three eqs -> 200 OK") + void postQuery_shouldReturn200AndArrayOfString_whenThreeEqualsConditions() + throws Exception { + AttrQueryRequest req1 = new AttrQueryRequest("capacity", "=", "20"); + AttrQueryRequest req2 = new AttrQueryRequest("heating", "=", "false"); + AttrQueryRequest req3 = new AttrQueryRequest("cooling", "=", "true"); + AttrQueryRequest[] requestBody = {req1, req2, req3}; + List expected = List.of("8"); + + when(droneAttrComparatorService.dronesSatisfyingAttributes(any(AttrQueryRequest[].class))) + .thenReturn(expected); + + mockMvc.perform(post(API_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestBody))) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(expected))); + } + + @Test + @DisplayName("GT LT -> 200 OK") + void postQuery_shouldReturn200AndArrayOfString_whenGtLtConditions() + throws Exception { + AttrQueryRequest req1 = new AttrQueryRequest("capacity", ">", "8"); + AttrQueryRequest req2 = new AttrQueryRequest("maxMoves", "<", "2000"); + AttrQueryRequest[] requestBody = {req1, req2}; + List expected = List.of("5", "10"); + + when(droneAttrComparatorService.dronesSatisfyingAttributes(any(AttrQueryRequest[].class))) + .thenReturn(expected); + + mockMvc.perform(post(API_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestBody))) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(expected))); + } + + @Test + @DisplayName("GT LT -contradict -> 200 OK (empty list)") + void postQuery_shouldReturn200AndEmptyArrayOfString_whenContradictoryConditions() + throws Exception { + AttrQueryRequest req1 = new AttrQueryRequest("capacity", ">", "8"); + AttrQueryRequest req2 = new AttrQueryRequest("capacity", "<", "8"); + AttrQueryRequest[] requestBody = {req1, req2}; + List expected = List.of(); + + when(droneAttrComparatorService.dronesSatisfyingAttributes(any(AttrQueryRequest[].class))) + .thenReturn(expected); + + mockMvc.perform(post(API_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestBody))) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(expected))); + } + + @Test + @DisplayName("GT LT EQ -> 200 OK") + void postQuery_shouldReturn200AndArrayOfString_whenGtLtEqConditions() + throws Exception { + AttrQueryRequest req1 = new AttrQueryRequest("capacity", ">", "8"); + AttrQueryRequest req2 = new AttrQueryRequest("maxMoves", "<", "2000"); + AttrQueryRequest req3 = new AttrQueryRequest("cooling", "=", "true"); + AttrQueryRequest[] requestBody = {req1, req2, req3}; + List expected = List.of("5"); + + when(droneAttrComparatorService.dronesSatisfyingAttributes(any(AttrQueryRequest[].class))) + .thenReturn(expected); + + mockMvc.perform(post(API_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestBody))) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(expected))); + } + } + + @Nested + @DisplayName("POST /queryAvailableDrones") + class PostQueryAvailableDronesTests { + + final String API_ENDPOINT = "/api/v1/queryAvailableDrones"; + + @Test + @DisplayName("Example -> 200 OK") + void postQueryAvailableDrones_shouldReturn200AndArrayOfString_whenExampleRequest() + throws Exception { + var reqs = new MedDispatchRecRequest.MedRequirement(0.75f, false, true, 13.5f); + var delivery = new LngLat(-3.00, 55.121); + var record = new MedDispatchRecRequest(123, LocalDate.parse("2025-12-22"), LocalTime.parse("14:30"), reqs, delivery); + MedDispatchRecRequest[] requestBody = {record}; + List expected = List.of("1", "2", "6", "7", "9"); + + when(droneInfoService.dronesMatchesRequirements(any(MedDispatchRecRequest[].class))) + .thenReturn(expected); + + mockMvc.perform(post(API_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestBody))) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(expected))); + } + + @Test + @DisplayName("Treat Null as False (Cooling) -> 200 OK") + void postQueryAvailableDrones_shouldReturn200AndArrayOfString_whenCoolingIsNull() + throws Exception { + var requestMap = Map.of( + "id", 123, + "date", "2025-12-22", + "time", "14:30", + "requirements", Map.of( + "capacity", 0.75, + "heating", true, + "maxCost", 13.5 + ) + ); + List expected = List.of("1", "2", "6", "7", "9"); + + when(droneInfoService.dronesMatchesRequirements(any(MedDispatchRecRequest[].class))) + .thenReturn(expected); + + mockMvc.perform(post(API_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new Object[]{requestMap}))) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(expected))); + } + + @Test + @DisplayName("Complex -> 200 OK") + void postQueryAvailableDrones_shouldReturn200AndArrayOfString_whenComplexRequest() + throws Exception { + var reqs1 = new MedDispatchRecRequest.MedRequirement(0.75f, false, true, 13.5f); + var delivery1 = new LngLat(-3.00, 55.121); + var record1 = new MedDispatchRecRequest(123, LocalDate.parse("2025-12-22"), LocalTime.parse("14:30"), reqs1, delivery1); + + var reqs2 = new MedDispatchRecRequest.MedRequirement(0.75f, false, true, 13.5f); + var delivery2 = new LngLat(-3.00, 55.121); + var record2 = new MedDispatchRecRequest(456, LocalDate.parse("2025-12-25"), LocalTime.parse("11:30"), reqs2, delivery2); + + MedDispatchRecRequest[] requestBody = {record1, record2}; + List expected = List.of("2", "7", "9"); + + when(droneInfoService.dronesMatchesRequirements(any(MedDispatchRecRequest[].class))) + .thenReturn(expected); + + mockMvc.perform(post(API_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestBody))) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(expected))); + } + } +} \ No newline at end of file diff --git a/src/test/java/io/github/js0ny/ilp_coursework/service/DroneInfoServiceTest.java b/src/test/java/io/github/js0ny/ilp_coursework/service/DroneInfoServiceTest.java new file mode 100644 index 0000000..16a39cd --- /dev/null +++ b/src/test/java/io/github/js0ny/ilp_coursework/service/DroneInfoServiceTest.java @@ -0,0 +1,204 @@ +package io.github.js0ny.ilp_coursework.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.when; + +import io.github.js0ny.ilp_coursework.data.common.DroneAvailability; +import io.github.js0ny.ilp_coursework.data.common.DroneCapability; +import io.github.js0ny.ilp_coursework.data.common.LngLat; +import io.github.js0ny.ilp_coursework.data.common.TimeWindow; +import io.github.js0ny.ilp_coursework.data.external.Drone; +import io.github.js0ny.ilp_coursework.data.external.ServicePointDrones; +import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest; +import java.net.URI; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.client.RestTemplate; + +@ExtendWith(MockitoExtension.class) +public class DroneInfoServiceTest { + + @Mock + private RestTemplate restTemplate; + + private DroneInfoService droneInfoService; + + private final String baseUrl = "http://localhost:8080/"; + + @BeforeEach + void setUp() { + droneInfoService = new DroneInfoService(restTemplate); + ReflectionTestUtils.setField(droneInfoService, "baseUrl", baseUrl); + } + + private Drone[] getMockDrones() { + return new Drone[] { + new Drone("Drone 1", "1", new DroneCapability(true, true, 10, 1000, 1, 1, 1)), + new Drone("Drone 2", "2", new DroneCapability(false, true, 20, 2000, 2, 2, 2)), + new Drone("Drone 3", "3", new DroneCapability(false, false, 30, 3000, 3, 3, 3)) + }; + } + + @Nested + @DisplayName("dronesWithCooling(boolean) tests") + class DronesWithCoolingTests { + + @Test + @DisplayName("Should return drones with cooling") + void dronesWithCooling_shouldReturnDronesWithCooling() { + // Arrange + when(restTemplate.getForObject(URI.create(baseUrl + "drones"), Drone[].class)) + .thenReturn(getMockDrones()); + + // Act + List result = droneInfoService.dronesWithCooling(true); + + // Assert + assertThat(result).containsExactly("1"); + } + + @Test + @DisplayName("Should return drones without cooling") + void dronesWithCooling_shouldReturnDronesWithoutCooling() { + // Arrange + when(restTemplate.getForObject(URI.create(baseUrl + "drones"), Drone[].class)) + .thenReturn(getMockDrones()); + + // Act + List result = droneInfoService.dronesWithCooling(false); + + // Assert + assertThat(result).containsExactly("2", "3"); + } + + @Test + @DisplayName("Should return empty list when API returns null") + void dronesWithCooling_shouldReturnEmptyList_whenApiReturnsNull() { + // Arrange + when(restTemplate.getForObject(URI.create(baseUrl + "drones"), Drone[].class)) + .thenReturn(null); + + // Act + List result = droneInfoService.dronesWithCooling(true); + + // Assert + assertThat(result).isEmpty(); + } + } + + @Nested + @DisplayName("droneDetail(String) tests") + class DroneDetailTests { + + @Test + @DisplayName("Should return correct drone details") + void droneDetail_shouldReturnCorrectDrone() { + // Arrange + when(restTemplate.getForObject(URI.create(baseUrl + "drones"), Drone[].class)) + .thenReturn(getMockDrones()); + + // Act + Drone result = droneInfoService.droneDetail("2"); + + // Assert + assertThat(result.id()).isEqualTo("2"); + assertThat(result.name()).isEqualTo("Drone 2"); + } + + @Test + @DisplayName("Should throw exception for non-existent drone") + void droneDetail_shouldThrowException_forNonExistentDrone() { + // Arrange + when(restTemplate.getForObject(URI.create(baseUrl + "drones"), Drone[].class)) + .thenReturn(getMockDrones()); + + // Act & Assert + assertThatThrownBy(() -> droneInfoService.droneDetail("4")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("drone with that ID cannot be found"); + } + } + + @Nested + @DisplayName("dronesMatchesRequirements(MedDispatchRecRequest[]) tests") + class DronesMatchesRequirementsTests { + private ServicePointDrones[] getMockServicePointDrones() { + TimeWindow[] timeWindows = { new TimeWindow(DayOfWeek.MONDAY, LocalTime.of(9, 0), LocalTime.of(17, 0)) }; + DroneAvailability drone1Avail = new DroneAvailability("1", timeWindows); + ServicePointDrones spd = new ServicePointDrones(1, new DroneAvailability[] { drone1Avail }); + return new ServicePointDrones[] { spd }; + } + + @Test + @DisplayName("Should return drones matching a single requirement") + void dronesMatchesRequirements_shouldReturnMatchingDrones_forSingleRequirement() { + // Arrange + var drones = new Drone[] { + new Drone("Drone 1", "1", new DroneCapability(true, true, 10, 1000, 1, 1, 1)), + new Drone("Drone 2", "2", new DroneCapability(false, true, 5, 2000, 2, 2, 2)) + }; + when(restTemplate.getForObject(URI.create(baseUrl + "drones"), Drone[].class)).thenReturn(drones); + when(restTemplate.getForObject(URI.create(baseUrl + "drones-for-service-points"), + ServicePointDrones[].class)).thenReturn(getMockServicePointDrones()); + + var requirement = new MedDispatchRecRequest.MedRequirement(8, true, false, 100); + var record = new MedDispatchRecRequest(1, LocalDate.now().with(DayOfWeek.MONDAY), LocalTime.of(10, 0), + requirement, new LngLat(0, 0)); + + // Act + List result = droneInfoService.dronesMatchesRequirements(new MedDispatchRecRequest[] { record }); + + // Assert + assertThat(result).containsExactly("1"); + } + + @Test + @DisplayName("Should return empty list if no drones match") + void dronesMatchesRequirements_shouldReturnEmptyList_whenNoDronesMatch() { + // Arrange + var drones = new Drone[] { + new Drone("Drone 1", "1", new DroneCapability(true, true, 5, 1000, 1, 1, 1)), + new Drone("Drone 2", "2", new DroneCapability(false, true, 5, 2000, 2, 2, 2)) + }; + when(restTemplate.getForObject(URI.create(baseUrl + "drones"), Drone[].class)).thenReturn(drones); + // No need to mock drones-for-service-points as it won't be called + + var requirement = new MedDispatchRecRequest.MedRequirement(10, true, false, 100); + var record = new MedDispatchRecRequest(1, LocalDate.now().with(DayOfWeek.MONDAY), LocalTime.of(10, 0), + requirement, new LngLat(0, 0)); + + // Act + List result = droneInfoService.dronesMatchesRequirements(new MedDispatchRecRequest[] { record }); + + // Assert + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("Should return all drones if requirements are null or empty") + void dronesMatchesRequirements_shouldReturnAllDrones_forNullOrEmptyRequirements() { + // Arrange + when(restTemplate.getForObject(URI.create(baseUrl + "drones"), Drone[].class)) + .thenReturn(getMockDrones()); + + // Act + List resultNull = droneInfoService.dronesMatchesRequirements(null); + List resultEmpty = droneInfoService.dronesMatchesRequirements(new MedDispatchRecRequest[0]); + + // Assert + assertThat(resultNull).containsExactly("1", "2", "3"); + assertThat(resultEmpty).containsExactly("1", "2", "3"); + } + } +}