fix(coverage): detailed coverage test filtering

This commit is contained in:
js0ny 2026-01-22 10:53:09 +00:00
parent 326c31c149
commit fb48c58a30
5 changed files with 415 additions and 4 deletions

View file

@ -50,7 +50,8 @@ jacocoTestReport {
'**/IlpCourseworkApplication.class', '**/IlpCourseworkApplication.class',
'**/config/*', '**/config/*',
'**/data/*', '**/data/*',
'**/util/*' '**/util/*',
'**/TelemetryService.class'
]) ])
})) }))
} }
@ -62,17 +63,18 @@ jacocoTestCoverageVerification {
element = 'CLASS' element = 'CLASS'
excludes = [ excludes = [
'io.github.js0ny.IlpCourseworkApplication', 'io.github.js0ny.ilp_coursework.IlpCourseworkApplication',
'**.config.**', '**.config.**',
'**.data.**', '**.data.**',
'**.util.**' '**.util.**',
'io.github.js0ny.ilp_coursework.service.TelemetryService'
] ]
limit { limit {
counter = 'BRANCH' counter = 'BRANCH'
value = 'COVEREDRATIO' value = 'COVEREDRATIO'
// minimum = 0.50 minimum = 0.50
} }
} }
} }

View file

@ -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.external.Drone;
import io.github.js0ny.ilp_coursework.data.request.AttrQueryRequest; 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.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.DroneAttrComparatorService;
import io.github.js0ny.ilp_coursework.service.DroneInfoService; import io.github.js0ny.ilp_coursework.service.DroneInfoService;
import io.github.js0ny.ilp_coursework.service.PathFinderService; import io.github.js0ny.ilp_coursework.service.PathFinderService;
@ -409,4 +410,79 @@ public class DroneControllerTest {
.andExpect(content().json(objectMapper.writeValueAsString(expected))); .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));
}
}
} }

View file

@ -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<RestrictedArea> 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<ServicePoint> 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)));
}
}
}

View file

@ -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();
}
}
}

View file

@ -4,11 +4,15 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.when; 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.DroneAvailability;
import io.github.js0ny.ilp_coursework.data.common.DroneCapability; 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.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.common.TimeWindow;
import io.github.js0ny.ilp_coursework.data.external.Drone; 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.external.ServicePointDrones;
import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest; import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest;
@ -230,4 +234,145 @@ public class DroneInfoServiceTest {
assertThat(resultEmpty).containsExactly("1", "2", "3"); 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<Drone> 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<Drone> 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<RestrictedArea> 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<ServicePoint> 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<ServicePointDrones> result = droneInfoService.fetchDronesForServicePoints();
assertThat(result).hasSize(1);
assertThat(result.get(0).servicePointId()).isEqualTo(1);
}
}
} }