From fb48c58a3088a996f3befcc43b9d3d45f735cf4b Mon Sep 17 00:00:00 2001 From: js0ny Date: Thu, 22 Jan 2026 10:53:09 +0000 Subject: [PATCH] fix(coverage): detailed coverage test filtering --- ilp-rest-service/build.gradle | 10 +- .../controller/DroneControllerTest.java | 76 +++++++++ .../controller/MapMetaControllerTest.java | 81 ++++++++++ .../DroneAttrComparatorServiceTest.java | 107 +++++++++++++ .../service/DroneInfoServiceTest.java | 145 ++++++++++++++++++ 5 files changed, 415 insertions(+), 4 deletions(-) create mode 100644 ilp-rest-service/src/test/java/io/github/js0ny/ilp_coursework/controller/MapMetaControllerTest.java create mode 100644 ilp-rest-service/src/test/java/io/github/js0ny/ilp_coursework/service/DroneAttrComparatorServiceTest.java diff --git a/ilp-rest-service/build.gradle b/ilp-rest-service/build.gradle index 94a836e..6cdeedb 100644 --- a/ilp-rest-service/build.gradle +++ b/ilp-rest-service/build.gradle @@ -50,7 +50,8 @@ jacocoTestReport { '**/IlpCourseworkApplication.class', '**/config/*', '**/data/*', - '**/util/*' + '**/util/*', + '**/TelemetryService.class' ]) })) } @@ -62,17 +63,18 @@ jacocoTestCoverageVerification { element = 'CLASS' excludes = [ - 'io.github.js0ny.IlpCourseworkApplication', + 'io.github.js0ny.ilp_coursework.IlpCourseworkApplication', '**.config.**', '**.data.**', - '**.util.**' + '**.util.**', + 'io.github.js0ny.ilp_coursework.service.TelemetryService' ] limit { counter = 'BRANCH' value = 'COVEREDRATIO' - // minimum = 0.50 + minimum = 0.50 } } } diff --git a/ilp-rest-service/src/test/java/io/github/js0ny/ilp_coursework/controller/DroneControllerTest.java b/ilp-rest-service/src/test/java/io/github/js0ny/ilp_coursework/controller/DroneControllerTest.java index a377da7..54a9627 100644 --- a/ilp-rest-service/src/test/java/io/github/js0ny/ilp_coursework/controller/DroneControllerTest.java +++ b/ilp-rest-service/src/test/java/io/github/js0ny/ilp_coursework/controller/DroneControllerTest.java @@ -15,6 +15,7 @@ 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.data.response.DeliveryPathResponse; import io.github.js0ny.ilp_coursework.service.DroneAttrComparatorService; import io.github.js0ny.ilp_coursework.service.DroneInfoService; import io.github.js0ny.ilp_coursework.service.PathFinderService; @@ -409,4 +410,79 @@ public class DroneControllerTest { .andExpect(content().json(objectMapper.writeValueAsString(expected))); } } + + @Nested + @DisplayName("POST /calcDeliveryPath") + class PostCalcDeliveryPathTests { + + final String API_ENDPOINT = "/api/v1/calcDeliveryPath"; + + @Test + @DisplayName("Example -> 200 OK") + void postCalcDeliveryPath_shouldReturn200AndJson_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}; + + var flightPath = List.of(new LngLat(-3.0, 55.12), new LngLat(-3.01, 55.13)); + var deliveryPath = + new DeliveryPathResponse.DronePath.Delivery(123, flightPath); + var dronePath = new DeliveryPathResponse.DronePath(1, List.of(deliveryPath)); + DeliveryPathResponse expected = + new DeliveryPathResponse( + 12.5f, 42, new DeliveryPathResponse.DronePath[] {dronePath}); + + when(pathFinderService.calculateDeliveryPath(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))); + } + } + + @Nested + @DisplayName("POST /calcDeliveryPathAsGeoJson") + class PostCalcDeliveryPathAsGeoJsonTests { + + final String API_ENDPOINT = "/api/v1/calcDeliveryPathAsGeoJson"; + + @Test + @DisplayName("Example -> 200 OK") + void postCalcDeliveryPathAsGeoJson_shouldReturn200AndGeoJson_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}; + String expected = "{\"type\":\"FeatureCollection\",\"features\":[]}"; + + when(pathFinderService.calculateDeliveryPathAsGeoJson( + any(MedDispatchRecRequest[].class))) + .thenReturn(expected); + + mockMvc.perform( + post(API_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestBody))) + .andExpect(status().isOk()) + .andExpect(content().string(expected)); + } + } } diff --git a/ilp-rest-service/src/test/java/io/github/js0ny/ilp_coursework/controller/MapMetaControllerTest.java b/ilp-rest-service/src/test/java/io/github/js0ny/ilp_coursework/controller/MapMetaControllerTest.java new file mode 100644 index 0000000..b1b1535 --- /dev/null +++ b/ilp-rest-service/src/test/java/io/github/js0ny/ilp_coursework/controller/MapMetaControllerTest.java @@ -0,0 +1,81 @@ +package io.github.js0ny.ilp_coursework.controller; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +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 io.github.js0ny.ilp_coursework.data.common.AltitudeRange; +import io.github.js0ny.ilp_coursework.data.common.LngLatAlt; +import io.github.js0ny.ilp_coursework.data.external.RestrictedArea; +import io.github.js0ny.ilp_coursework.data.external.ServicePoint; +import io.github.js0ny.ilp_coursework.service.DroneInfoService; + +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; + +import java.util.List; + +@WebMvcTest(MapMetaController.class) +public class MapMetaControllerTest { + + @Autowired private MockMvc mockMvc; + + @Autowired private ObjectMapper objectMapper; + + @MockitoBean private DroneInfoService droneInfoService; + + @Nested + @DisplayName("GET /restrictedAreas") + class RestrictedAreasTests { + + @Test + @DisplayName("-> 200 OK") + void getRestrictedAreas_shouldReturn200AndJson() throws Exception { + String endpoint = "/api/v1/restrictedAreas"; + RestrictedArea area = + new RestrictedArea( + "Zone A", + 1, + new AltitudeRange(0.0, 120.0), + new LngLatAlt[] { + new LngLatAlt(0.0, 0.0, 0.0), + new LngLatAlt(1.0, 0.0, 0.0), + new LngLatAlt(1.0, 1.0, 0.0), + new LngLatAlt(0.0, 1.0, 0.0) + }); + List expected = List.of(area); + when(droneInfoService.fetchRestrictedAreas()).thenReturn(expected); + + var mock = mockMvc.perform(get(endpoint).contentType(MediaType.APPLICATION_JSON)); + mock.andExpect(status().isOk()); + mock.andExpect(content().json(objectMapper.writeValueAsString(expected))); + } + } + + @Nested + @DisplayName("GET /servicePoints") + class ServicePointsTests { + + @Test + @DisplayName("-> 200 OK") + void getServicePoints_shouldReturn200AndJson() throws Exception { + String endpoint = "/api/v1/servicePoints"; + ServicePoint point = new ServicePoint("Point A", 1, new LngLatAlt(0.1, 0.2, 12.0)); + List expected = List.of(point); + when(droneInfoService.fetchServicePoints()).thenReturn(expected); + + var mock = mockMvc.perform(get(endpoint).contentType(MediaType.APPLICATION_JSON)); + mock.andExpect(status().isOk()); + mock.andExpect(content().json(objectMapper.writeValueAsString(expected))); + } + } +} diff --git a/ilp-rest-service/src/test/java/io/github/js0ny/ilp_coursework/service/DroneAttrComparatorServiceTest.java b/ilp-rest-service/src/test/java/io/github/js0ny/ilp_coursework/service/DroneAttrComparatorServiceTest.java new file mode 100644 index 0000000..9077d92 --- /dev/null +++ b/ilp-rest-service/src/test/java/io/github/js0ny/ilp_coursework/service/DroneAttrComparatorServiceTest.java @@ -0,0 +1,107 @@ +package io.github.js0ny.ilp_coursework.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.github.js0ny.ilp_coursework.data.common.DroneCapability; +import io.github.js0ny.ilp_coursework.data.external.Drone; +import io.github.js0ny.ilp_coursework.data.request.AttrQueryRequest; + +import org.junit.jupiter.api.AfterEach; +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.springframework.http.MediaType; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.web.client.RestTemplate; + +@ExtendWith(SpringExtension.class) +public class DroneAttrComparatorServiceTest { + + private DroneAttrComparatorService service; + private MockRestServiceServer server; + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + service = new DroneAttrComparatorService(); + RestTemplate restTemplate = + (RestTemplate) ReflectionTestUtils.getField(service, "restTemplate"); + server = MockRestServiceServer.createServer(restTemplate); + ReflectionTestUtils.setField(service, "baseUrl", "http://localhost/"); + objectMapper = new ObjectMapper(); + } + + @AfterEach + void tearDown() { + server.verify(); + } + + @Nested + @DisplayName("dronesWithAttribute(String, String) tests") + class DronesWithAttributeTests { + + @Test + @DisplayName("Should return matching ids for boolean attribute") + void dronesWithAttribute_shouldReturnMatchingIds() throws Exception { + Drone[] drones = { + new Drone("Drone 1", "1", new DroneCapability(true, true, 10, 1000, 1, 1, 1)), + new Drone("Drone 2", "2", new DroneCapability(false, true, 5, 500, 1, 1, 1)), + new Drone("Drone 3", "3", new DroneCapability(true, false, 12, 800, 1, 1, 1)) + }; + String responseBody = objectMapper.writeValueAsString(drones); + server.expect(requestTo("http://localhost/drones")) + .andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON)); + + var result = service.dronesWithAttribute("cooling", "true"); + + assertThat(result).containsExactly("1", "3"); + } + } + + @Nested + @DisplayName("dronesSatisfyingAttributes(AttrQueryRequest[]) tests") + class DronesSatisfyingAttributesTests { + + @Test + @DisplayName("Should return intersection of all rules") + void dronesSatisfyingAttributes_shouldReturnIntersection() throws Exception { + Drone[] drones = { + new Drone("Drone 1", "1", new DroneCapability(true, true, 10, 1000, 1, 1, 1)), + new Drone("Drone 2", "2", new DroneCapability(true, true, 3, 1000, 1, 1, 1)), + new Drone("Drone 3", "3", new DroneCapability(true, false, 12, 1000, 1, 1, 1)) + }; + String responseBody = objectMapper.writeValueAsString(drones); + server.expect(requestTo("http://localhost/drones")) + .andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON)); + server.expect(requestTo("http://localhost/drones")) + .andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON)); + + AttrQueryRequest[] comparators = { + new AttrQueryRequest("capacity", ">", "5"), + new AttrQueryRequest("heating", "=", "true") + }; + + var result = service.dronesSatisfyingAttributes(comparators); + + assertThat(result).containsExactly("1"); + } + + @Test + @DisplayName("Should return empty list when no comparators") + void dronesSatisfyingAttributes_shouldReturnEmpty_whenNoComparators() { + AttrQueryRequest[] comparators = {}; + + var result = service.dronesSatisfyingAttributes(comparators); + + assertThat(result).isEmpty(); + } + } +} diff --git a/ilp-rest-service/src/test/java/io/github/js0ny/ilp_coursework/service/DroneInfoServiceTest.java b/ilp-rest-service/src/test/java/io/github/js0ny/ilp_coursework/service/DroneInfoServiceTest.java index 6595e66..b549f52 100644 --- a/ilp-rest-service/src/test/java/io/github/js0ny/ilp_coursework/service/DroneInfoServiceTest.java +++ b/ilp-rest-service/src/test/java/io/github/js0ny/ilp_coursework/service/DroneInfoServiceTest.java @@ -4,11 +4,15 @@ 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.AltitudeRange; 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.LngLatAlt; 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.RestrictedArea; +import io.github.js0ny.ilp_coursework.data.external.ServicePoint; import io.github.js0ny.ilp_coursework.data.external.ServicePointDrones; import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest; @@ -230,4 +234,145 @@ public class DroneInfoServiceTest { assertThat(resultEmpty).containsExactly("1", "2", "3"); } } + + @Nested + @DisplayName("droneMatchesRequirement(Drone, MedDispatchRecRequest) tests") + class DroneMatchesRequirementTests { + + @Test + @DisplayName("Should throw when requirements are null") + void droneMatchesRequirement_shouldThrow_whenRequirementsNull() { + Drone drone = + new Drone("Drone 1", "1", new DroneCapability(true, true, 10, 1000, 1, 1, 1)); + MedDispatchRecRequest record = + new MedDispatchRecRequest( + 1, LocalDate.now(), LocalTime.of(9, 0), null, new LngLat(0, 0)); + + assertThatThrownBy(() -> droneInfoService.droneMatchesRequirement(drone, record)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("requirements cannot be null"); + } + + @Test + @DisplayName("Should throw when drone capability is null") + void droneMatchesRequirement_shouldThrow_whenCapabilityNull() { + Drone drone = new Drone("Drone 1", "1", null); + MedDispatchRecRequest record = + new MedDispatchRecRequest( + 1, + LocalDate.now(), + LocalTime.of(9, 0), + new MedDispatchRecRequest.MedRequirement(1, false, false, 10), + new LngLat(0, 0)); + + assertThatThrownBy(() -> droneInfoService.droneMatchesRequirement(drone, record)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("drone capability cannot be null"); + } + } + + @Nested + @DisplayName("fetchAllDrones() tests") + class FetchAllDronesTests { + + @Test + @DisplayName("Should return list when API returns drones") + void fetchAllDrones_shouldReturnList_whenApiReturnsDrones() { + Drone[] drones = getMockDrones(); + when(restTemplate.getForObject(URI.create(baseUrl + "drones"), Drone[].class)) + .thenReturn(drones); + + List result = droneInfoService.fetchAllDrones(); + + assertThat(result).hasSize(3); + assertThat(result.get(0).id()).isEqualTo("1"); + } + + @Test + @DisplayName("Should return empty list when API returns null") + void fetchAllDrones_shouldReturnEmptyList_whenApiReturnsNull() { + when(restTemplate.getForObject(URI.create(baseUrl + "drones"), Drone[].class)) + .thenReturn(null); + + List result = droneInfoService.fetchAllDrones(); + + assertThat(result).isEmpty(); + } + } + + @Nested + @DisplayName("fetchRestrictedAreas() tests") + class FetchRestrictedAreasTests { + + @Test + @DisplayName("Should return restricted areas") + void fetchRestrictedAreas_shouldReturnList() { + RestrictedArea[] areas = { + new RestrictedArea( + "Zone A", + 1, + new AltitudeRange(0, 100), + new LngLatAlt[] { + new LngLatAlt(0, 0, 0), + new LngLatAlt(1, 0, 0), + new LngLatAlt(1, 1, 0) + }) + }; + when(restTemplate.getForObject( + URI.create(baseUrl + "restricted-areas"), RestrictedArea[].class)) + .thenReturn(areas); + + List result = droneInfoService.fetchRestrictedAreas(); + + assertThat(result).hasSize(1); + assertThat(result.get(0).name()).isEqualTo("Zone A"); + } + } + + @Nested + @DisplayName("fetchServicePoints() tests") + class FetchServicePointsTests { + + @Test + @DisplayName("Should return service points") + void fetchServicePoints_shouldReturnList() { + ServicePoint[] points = { + new ServicePoint("Point A", 1, new LngLatAlt(0.1, 0.2, 3.0)) + }; + when(restTemplate.getForObject( + URI.create(baseUrl + "service-points"), ServicePoint[].class)) + .thenReturn(points); + + List result = droneInfoService.fetchServicePoints(); + + assertThat(result).hasSize(1); + assertThat(result.get(0).name()).isEqualTo("Point A"); + } + } + + @Nested + @DisplayName("fetchDronesForServicePoints() tests") + class FetchDronesForServicePointsTests { + + @Test + @DisplayName("Should return service point drones") + void fetchDronesForServicePoints_shouldReturnList() { + 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}); + ServicePointDrones[] servicePointDrones = {spd}; + when(restTemplate.getForObject( + URI.create(baseUrl + "drones-for-service-points"), + ServicePointDrones[].class)) + .thenReturn(servicePointDrones); + + List result = droneInfoService.fetchDronesForServicePoints(); + + assertThat(result).hasSize(1); + assertThat(result.get(0).servicePointId()).isEqualTo(1); + } + } }