diff --git a/.bin/e2e.sh b/.bin/e2e.sh deleted file mode 100644 index 4e3f8e5..0000000 --- a/.bin/e2e.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env bash -set -e - - - -echo "[INFO] Starting Go Backend..." -cd drone-black-box && go build -o app main.go -cd .. - -./drone-black-box/app & -GO_PID=$! -echo " Go PID: $GO_PID" - -echo "[INFO] Starting Java Backend..." - -cd ilp-rest-service -./gradlew bootRun & -JAVA_PID=$! -echo " Java PID: $JAVA_PID" -cd .. - -cleanup() { - echo "[INFO] Stopping services..." - kill $GO_PID || true - kill $JAVA_PID || true -} -trap cleanup EXIT - - -echo "[INFO] Waiting for services to be ready..." - -for i in {1..30}; do - if curl -s http://localhost:8080/actuator/health > /dev/null; then - echo "[INFO] Java is UP!" - break - fi - echo "[DEBUG] Waiting for Java..." - sleep 2 -done - - -echo "[INFO] Running Bruno E2E Collection..." - -cd ./ilp-rest-service/ilp-cw-api - -bru run - -echo "[INFO] E2E Tests Passed!" - -rm --force drone_black_box.db -rm --force drone_black_box.db-wal -rm --force drone_black_box.db-shm - -cleanup diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 16ff453..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Polyglot CI - -on: - push: - branches: [ "main" ] - paths: - - '**.java' - - '**.go' - - '.justfile' - - '**.js' - - '**.svelte' - - '**.css' - pull_request: - branches: [ "main" ] - - -jobs: - tests: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: cachix/install-nix-action@v25 - with: - nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v14 - with: - name: mycache - - run: chmod +x ./ilp-rest-service/gradlew - - name: Unit Testing and Integration Testing - run: nix develop .#ci --command just all - - name: E2E Test - run: nix develop .#ci --command just e2e diff --git a/.gitignore b/.gitignore index 55bcae1..e8e3c27 100644 --- a/.gitignore +++ b/.gitignore @@ -47,9 +47,3 @@ drone-black-box/drone-black-box drone-black-box/drone_black_box.db* *.pdf - -*.out -*.html - -drone_black_box.db* -app diff --git a/.justfile b/.justfile deleted file mode 100644 index 15b37ae..0000000 --- a/.justfile +++ /dev/null @@ -1,29 +0,0 @@ -all: - @just format - @just static-analysis - @just test - -format: - cd ./drone-black-box && go fmt - fd --extension java --exec google-java-format --replace --aosp {} - - -static-analysis: - cd ./drone-black-box && go vet ./... - -test: - cd ./drone-black-box && go test -v ./... -coverprofile=coverage.out - cd ./drone-black-box && go tool cover -html=coverage.out -o coverage.html - rm ./drone-black-box/coverage.out - # Java - cd ./ilp-rest-service/ && ./gradlew check - -e2e: - bash .bin/e2e.sh - -benchmark: - echo "Make sure the server is running at http://localhost:3000" - oha -z 10s -c 50 -m POST \ - -H "Content-Type: application/json" \ - -d '{"drone_id": "TEST-DRONE", "latitude": 0.0, "longitude": 0.0, "timestamp": "2025-01-01"}' \ - http://localhost:3000/ingest diff --git a/README.md b/README.md index 54acbad..17b974d 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,5 @@ # Informatics Large Practical Coursework 3 -This is a temporary repository, if you are viewing on GitHub, just ignore the (randomly generated) repository name. - -[![Polyglot CI](https://github.com/js0ny/expert-goggles/actions/workflows/ci.yml/badge.svg)](https://github.com/js0ny/expert-goggles/actions/workflows/ci.yml) - ## Installation ### Docker Compose diff --git a/drone-black-box/main_test.go b/drone-black-box/main_test.go index b6537e9..6fc24b0 100644 --- a/drone-black-box/main_test.go +++ b/drone-black-box/main_test.go @@ -125,7 +125,6 @@ func TestIngestBadJSON(t *testing.T) { // Ensure graceful shutdown path does not hang: start a server and shut it down quickly. func TestGracefulShutdown(t *testing.T) { db := newTestDB(t) - defer db.Close() srv := &Server{db: db} mux := http.NewServeMux() mux.HandleFunc("GET /health", srv.healthHandler) @@ -139,75 +138,3 @@ func TestGracefulShutdown(t *testing.T) { t.Fatalf("shutdown: %v", err) } } - -func TestCORSAllowedOriginAndPreflight(t *testing.T) { - handled := false - next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - handled = true - w.WriteHeader(http.StatusOK) - }) - handler := corsMiddleware(next) - - req := httptest.NewRequest(http.MethodGet, "/health", nil) - req.Header.Set("Origin", "http://localhost:5173") - rec := httptest.NewRecorder() - handler.ServeHTTP(rec, req) - - if rec.Code != http.StatusOK { - t.Fatalf("expected 200, got %d", rec.Code) - } - if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "http://localhost:5173" { - t.Fatalf("expected allow-origin header, got %q", got) - } - if !handled { - t.Fatalf("expected handler to be called") - } - - preflight := httptest.NewRequest(http.MethodOptions, "/health", nil) - preflight.Header.Set("Origin", "http://localhost:5173") - preflightRec := httptest.NewRecorder() - handler.ServeHTTP(preflightRec, preflight) - if preflightRec.Code != http.StatusNoContent { - t.Fatalf("preflight expected 204, got %d", preflightRec.Code) - } -} - -func TestIngestDBError(t *testing.T) { - db := newTestDB(t) - db.Close() - srv := &Server{db: db} - - ev := DroneEvent{DroneID: "d1", Latitude: 1.0, Longitude: 2.0, Timestamp: "2025-12-06T00:00:00Z"} - body, _ := json.Marshal(ev) - req := httptest.NewRequest(http.MethodPost, "/ingest", bytes.NewReader(body)) - rec := httptest.NewRecorder() - srv.ingestHandler(rec, req) - if rec.Code != http.StatusInternalServerError { - t.Fatalf("expected 500, got %d", rec.Code) - } -} - -func TestSnapshotQueryError(t *testing.T) { - db := newTestDB(t) - db.Close() - srv := &Server{db: db} - - req := httptest.NewRequest(http.MethodGet, "/snapshot?time=2025-12-06T00:00:00Z", nil) - rec := httptest.NewRecorder() - srv.snapshotHandler(rec, req) - if rec.Code != http.StatusInternalServerError { - t.Fatalf("expected 500, got %d", rec.Code) - } -} - -func TestHealthFailure(t *testing.T) { - db := newTestDB(t) - db.Close() - srv := &Server{db: db} - req := httptest.NewRequest(http.MethodGet, "/health", nil) - rec := httptest.NewRecorder() - srv.healthHandler(rec, req) - if rec.Code != http.StatusServiceUnavailable { - t.Fatalf("expected 503, got %d", rec.Code) - } -} diff --git a/flake.nix b/flake.nix index 30c3f5d..596f994 100644 --- a/flake.nix +++ b/flake.nix @@ -17,42 +17,29 @@ pkgs = import nixpkgs { inherit system; }; - ciDeps = with pkgs; [ - jdk21 - gradle - google-java-format - go - just - fd - bruno-cli - ]; - devDeps = with pkgs; [ - vscode-langservers-extracted - jdt-language-server - jless - httpie - docker - docker-compose - newman - gron - fx - oha - gopls - bun - svelte-language-server - typescript-language-server - prettier - ]; in { default = pkgs.mkShell { - buildInputs = ciDeps ++ devDeps; - shellHook = '' - export JAVA_HOME=${pkgs.jdk21} - echo "Java: $(java --version | head -n 1)" - ''; - }; - ci = pkgs.mkShell { - buildInputs = ciDeps; + buildInputs = with pkgs; [ + vscode-langservers-extracted + jdt-language-server + jless + jdk21 + gradle + httpie + docker + docker-compose + newman + gron + fx + google-java-format + oha + gopls + go + bun + svelte-language-server + typescript-language-server + prettier + ]; shellHook = '' export JAVA_HOME=${pkgs.jdk21} echo "Java: $(java --version | head -n 1)" diff --git a/ilp-rest-service/build.gradle b/ilp-rest-service/build.gradle index 6cdeedb..81be9e6 100644 --- a/ilp-rest-service/build.gradle +++ b/ilp-rest-service/build.gradle @@ -2,7 +2,6 @@ plugins { id 'java' id 'org.springframework.boot' version '3.5.6' id 'io.spring.dependency-management' version '1.1.7' - id 'jacoco' } group = 'io.github.js0ny' @@ -27,58 +26,6 @@ dependencies { } -jacoco { - toolVersion = "0.8.12" -} - tasks.named('test') { useJUnitPlatform() - finalizedBy jacocoTestReport } - -jacocoTestReport { - dependsOn test - - reports { - xml.required = true - html.required = true - } - - afterEvaluate { - classDirectories.setFrom(files(classDirectories.files.collect { - fileTree(dir: it, exclude: [ - '**/IlpCourseworkApplication.class', - '**/config/*', - '**/data/*', - '**/util/*', - '**/TelemetryService.class' - ]) - })) - } -} - -jacocoTestCoverageVerification { - violationRules { - rule { - element = 'CLASS' - - excludes = [ - 'io.github.js0ny.ilp_coursework.IlpCourseworkApplication', - '**.config.**', - '**.data.**', - '**.util.**', - 'io.github.js0ny.ilp_coursework.service.TelemetryService' - ] - - limit { - counter = 'BRANCH' - value = 'COVEREDRATIO' - - minimum = 0.50 - } - } - } -} - - -check.dependsOn jacocoTestCoverageVerification diff --git a/ilp-rest-service/ilp-cw-api/Front-end Test/2 Drones.bru b/ilp-rest-service/ilp-cw-api/Front-end Test/2 Drones.bru index d31d50a..072c15f 100644 --- a/ilp-rest-service/ilp-cw-api/Front-end Test/2 Drones.bru +++ b/ilp-rest-service/ilp-cw-api/Front-end Test/2 Drones.bru @@ -1,5 +1,5 @@ meta { - name: 2 Drones + name: 2 Drones type: http seq: 9 } @@ -14,7 +14,7 @@ body:json { [ { "id": 123, - "date": "2025-12-25", + "date": "2025-12-22", "time": "14:30", "requirements": { "capacity": 0.75, @@ -28,7 +28,7 @@ body:json { }, { "id": 456, - "date": "2025-12-23", + "date": "2025-12-25", "time": "11:30", "requirements": { "capacity": 0.75, diff --git a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/config/CorsConfig.java b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/config/CorsConfig.java index 63a949d..d8511e5 100644 --- a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/config/CorsConfig.java +++ b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/config/CorsConfig.java @@ -4,17 +4,18 @@ import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -/** Global CORS configuration so the frontend running on a different port can call the REST API. */ +/** + * Global CORS configuration so the frontend running on a different port can call the REST API. + */ @Configuration public class CorsConfig implements WebMvcConfigurer { - private static final String[] ALLOWED_ORIGINS = - new String[] { - "http://localhost:4173", - "http://127.0.0.1:4173", - "http://localhost:5173", - "http://127.0.0.1:5173" - }; + private static final String[] ALLOWED_ORIGINS = new String[] { + "http://localhost:4173", + "http://127.0.0.1:4173", + "http://localhost:5173", + "http://127.0.0.1:5173" + }; @Override public void addCorsMappings(CorsRegistry registry) { diff --git a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/controller/ApiController.java b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/controller/ApiController.java index 2884dd4..dbd2147 100644 --- a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/controller/ApiController.java +++ b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/controller/ApiController.java @@ -8,8 +8,6 @@ import io.github.js0ny.ilp_coursework.data.request.MovementRequest; import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest; import io.github.js0ny.ilp_coursework.service.GpsCalculationService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -27,8 +25,6 @@ import org.springframework.web.bind.annotation.RestController; @RequestMapping("/api/v1") public class ApiController { - private static final Logger log = LoggerFactory.getLogger(ApiController.class); - private final GpsCalculationService gpsService; /** @@ -49,7 +45,6 @@ public class ApiController { */ @GetMapping("/uid") public String getUid() { - log.info("GET /api/v1/uid"); return "s2522255"; } @@ -63,7 +58,6 @@ public class ApiController { public double getDistance(@RequestBody DistanceRequest request) { LngLat position1 = request.position1(); LngLat position2 = request.position2(); - log.info("POST /api/v1/distanceTo position1={} position2={}", position1, position2); return gpsService.calculateDistance(position1, position2); } @@ -78,7 +72,6 @@ public class ApiController { public boolean getIsCloseTo(@RequestBody DistanceRequest request) { LngLat position1 = request.position1(); LngLat position2 = request.position2(); - log.info("POST /api/v1/isCloseTo position1={} position2={}", position1, position2); return gpsService.isCloseTo(position1, position2); } @@ -93,7 +86,6 @@ public class ApiController { public LngLat getNextPosition(@RequestBody MovementRequest request) { LngLat start = request.start(); Angle angle = new Angle(request.angle()); - log.info("POST /api/v1/nextPosition start={} angle={}", start, angle); return gpsService.nextPosition(start, angle); } @@ -107,7 +99,6 @@ public class ApiController { public boolean getIsInRegion(@RequestBody RegionCheckRequest request) { LngLat position = request.position(); Region region = request.region(); - log.info("POST /api/v1/isInRegion position={} region={}", position, region); return gpsService.checkIsInRegion(position, region); } } diff --git a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/controller/DroneController.java b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/controller/DroneController.java index cd6d3f4..712fa8d 100644 --- a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/controller/DroneController.java +++ b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/controller/DroneController.java @@ -8,8 +8,6 @@ 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 org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -25,8 +23,6 @@ import java.util.List; @RequestMapping("/api/v1") public class DroneController { - private static final Logger log = LoggerFactory.getLogger(DroneController.class); - private final DroneInfoService droneInfoService; private final DroneAttrComparatorService droneAttrComparatorService; private final PathFinderService pathFinderService; @@ -59,7 +55,6 @@ public class DroneController { */ @GetMapping("/dronesWithCooling/{state}") public List getDronesWithCoolingCapability(@PathVariable boolean state) { - log.info("GET /api/v1/dronesWithCooling/{}", state); return droneInfoService.dronesWithCooling(state); } @@ -73,11 +68,9 @@ public class DroneController { @GetMapping("/droneDetails/{id}") public ResponseEntity getDroneDetail(@PathVariable String id) { try { - log.info("GET /api/v1/droneDetails/{}", id); Drone drone = droneInfoService.droneDetail(id); return ResponseEntity.ok(drone); } catch (IllegalArgumentException ex) { - log.warn("GET /api/v1/droneDetails/{} not found", id); return ResponseEntity.notFound().build(); } } @@ -93,35 +86,26 @@ public class DroneController { @GetMapping("/queryAsPath/{attrName}/{attrVal}") public List getIdByAttrMap( @PathVariable String attrName, @PathVariable String attrVal) { - log.info("GET /api/v1/queryAsPath/{}/{}", attrName, attrVal); return droneAttrComparatorService.dronesWithAttribute(attrName, attrVal); } @PostMapping("/query") public List getIdByAttrMapPost(@RequestBody AttrQueryRequest[] attrComparators) { - int count = attrComparators == null ? 0 : attrComparators.length; - log.info("POST /api/v1/query comparators={}", count); return droneAttrComparatorService.dronesSatisfyingAttributes(attrComparators); } @PostMapping("/queryAvailableDrones") public List queryAvailableDrones(@RequestBody MedDispatchRecRequest[] records) { - int count = records == null ? 0 : records.length; - log.info("POST /api/v1/queryAvailableDrones records={}", count); return droneInfoService.dronesMatchesRequirements(records); } @PostMapping("/calcDeliveryPath") public DeliveryPathResponse calculateDeliveryPath(@RequestBody MedDispatchRecRequest[] record) { - int count = record == null ? 0 : record.length; - log.info("POST /api/v1/calcDeliveryPath records={}", count); return pathFinderService.calculateDeliveryPath(record); } @PostMapping("/calcDeliveryPathAsGeoJson") public String calculateDeliveryPathAsGeoJson(@RequestBody MedDispatchRecRequest[] record) { - int count = record == null ? 0 : record.length; - log.info("POST /api/v1/calcDeliveryPathAsGeoJson records={}", count); return pathFinderService.calculateDeliveryPathAsGeoJson(record); } } diff --git a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/controller/MapMetaController.java b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/controller/MapMetaController.java index be33787..d4c81b9 100644 --- a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/controller/MapMetaController.java +++ b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/controller/MapMetaController.java @@ -1,23 +1,19 @@ package io.github.js0ny.ilp_coursework.controller; +import java.util.List; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + 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.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.List; - @RestController @RequestMapping("/api/v1") public class MapMetaController { - private static final Logger log = LoggerFactory.getLogger(MapMetaController.class); - private final DroneInfoService droneInfoService; public MapMetaController(DroneInfoService droneInfoService) { @@ -26,13 +22,11 @@ public class MapMetaController { @GetMapping("/restrictedAreas") public List getRestrictedAreas() { - log.info("GET /api/v1/restrictedAreas"); return droneInfoService.fetchRestrictedAreas(); } @GetMapping("/servicePoints") public List getServicePoints() { - log.info("GET /api/v1/servicePoints"); return droneInfoService.fetchServicePoints(); } } diff --git a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/data/common/DroneEvent.java b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/data/common/DroneEvent.java index 79ca2f6..a056863 100644 --- a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/data/common/DroneEvent.java +++ b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/data/common/DroneEvent.java @@ -1,11 +1,11 @@ package io.github.js0ny.ilp_coursework.data.common; -import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse; - import java.time.LocalDateTime; import java.util.List; import java.util.Map; +import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse; + // Corresponding in Go // @@ -18,10 +18,13 @@ import java.util.Map; * } */ -public record DroneEvent(String droneId, double latitude, double longitude, String timestamp) { - - static final int STEP = 1; // seconds between events +public record DroneEvent( + String droneId, + double latitude, + double longitude, + String timestamp) { + final static int STEP = 1; // seconds between events // Helper method that converts from DeliveryPathResponse to List public static List fromPathResponse(DeliveryPathResponse resp) { @@ -31,7 +34,11 @@ public record DroneEvent(String droneId, double latitude, double longitude, Stri for (var d : p.deliveries()) { for (var coord : d.flightPath()) { String timestamp = java.time.Instant.now().toString(); - events.add(new DroneEvent(id, coord.lat(), coord.lng(), timestamp)); + events.add(new DroneEvent( + id, + coord.lat(), + coord.lng(), + timestamp)); } } } @@ -40,15 +47,19 @@ public record DroneEvent(String droneId, double latitude, double longitude, Stri // Helper method that converts from DeliveryPathResponse to List // with base timestamp - public static List fromPathResponseWithTimestamp( - DeliveryPathResponse resp, LocalDateTime baseTimestamp) { + public static List fromPathResponseWithTimestamp(DeliveryPathResponse resp, + LocalDateTime baseTimestamp) { List events = new java.util.ArrayList<>(); java.time.LocalDateTime timestamp = baseTimestamp; for (var p : resp.dronePaths()) { String id = String.valueOf(p.droneId()); for (var d : p.deliveries()) { for (var coord : d.flightPath()) { - events.add(new DroneEvent(id, coord.lat(), coord.lng(), timestamp.toString())); + events.add(new DroneEvent( + id, + coord.lat(), + coord.lng(), + timestamp.toString())); timestamp = timestamp.plusSeconds(STEP); // Increment timestamp for each event } } @@ -64,11 +75,14 @@ public record DroneEvent(String droneId, double latitude, double longitude, Stri for (var d : p.deliveries()) { LocalDateTime timestamp = deliveryTimestamps.get(d.deliveryId()); // Fallback to current time if the delivery does not carry a timestamp. - System.out.println( - "Generated event for drone " + id + " at " + timestamp.toString()); + System.out.println("Generated event for drone " + id + " at " + timestamp.toString()); LocalDateTime current = timestamp != null ? timestamp : LocalDateTime.now(); for (var coord : d.flightPath()) { - events.add(new DroneEvent(id, coord.lat(), coord.lng(), current.toString())); + events.add(new DroneEvent( + id, + coord.lat(), + coord.lng(), + current.toString())); current = current.plusSeconds(STEP); } } diff --git a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/service/DroneInfoService.java b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/service/DroneInfoService.java index cff9ba3..35ed1e0 100644 --- a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/service/DroneInfoService.java +++ b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/service/DroneInfoService.java @@ -1,6 +1,10 @@ package io.github.js0ny.ilp_coursework.service; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + import io.github.js0ny.ilp_coursework.data.common.LngLat; +import io.github.js0ny.ilp_coursework.data.common.Region; 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; @@ -50,13 +54,15 @@ public class DroneInfoService { /** * Return an array of ids of drones with/without cooling capability * - *

Associated service method with {@code /dronesWithCooling/{state}} + *

+ * Associated service method with {@code /dronesWithCooling/{state}} * * @param state determines the capability filtering - * @return if {@code state} is true, return ids of drones with cooling capability, else without - * cooling + * @return if {@code state} is true, return ids of drones with cooling + * capability, else without + * cooling * @see - * io.github.js0ny.ilp_coursework.controller.DroneController#getDronesWithCoolingCapability(boolean) + * io.github.js0ny.ilp_coursework.controller.DroneController#getDronesWithCoolingCapability(boolean) */ public List dronesWithCooling(boolean state) { // URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint); @@ -76,13 +82,16 @@ public class DroneInfoService { /** * Return a {@link Drone}-style json data structure with the given {@code id} * - *

Associated service method with {@code /droneDetails/{id}} + *

+ * Associated service method with {@code /droneDetails/{id}} * * @param id The id of the drone * @return drone json body of given id - * @throws NullPointerException when cannot fetch available drones from remote - * @throws IllegalArgumentException when drone with given {@code id} cannot be found this should - * lead to a 404 + * @throws NullPointerException when cannot fetch available drones from + * remote + * @throws IllegalArgumentException when drone with given {@code id} cannot be + * found this should + * lead to a 404 * @see io.github.js0ny.ilp_coursework.controller.DroneController#getDroneDetail(String) */ public Drone droneDetail(String id) { @@ -99,10 +108,12 @@ public class DroneInfoService { } /** - * Return an array of ids of drones that match all the requirements in the medical dispatch + * Return an array of ids of drones that match all the requirements in the + * medical dispatch * records * - *

Associated service method with + *

+ * Associated service method with * * @param rec array of medical dispatch records * @return List of drone ids that match all the requirements @@ -124,10 +135,9 @@ public class DroneInfoService { return drones.stream() .filter(d -> d != null && d.capability() != null) .filter( - d -> - Arrays.stream(rec) - .filter(r -> r != null && r.requirements() != null) - .allMatch(r -> droneMatchesRequirement(d, r))) + d -> Arrays.stream(rec) + .filter(r -> r != null && r.requirements() != null) + .allMatch(r -> droneMatchesRequirement(d, r))) .map(Drone::id) .collect(Collectors.toList()); } @@ -135,11 +145,13 @@ public class DroneInfoService { /** * Helper to check if a drone meets the requirement of a medical dispatch. * - * @param drone the drone to be checked + * @param drone the drone to be checked * @param record the medical dispatch record containing the requirement * @return true if the drone meets the requirement, false otherwise - * @throws IllegalArgumentException when record requirements or drone capability is invalid - * (capacity and id cannot be null in {@code MedDispathRecDto}) + * @throws IllegalArgumentException when record requirements or drone capability + * is invalid + * (capacity and id cannot be null in + * {@code MedDispathRecDto}) */ public boolean droneMatchesRequirement(Drone drone, MedDispatchRecRequest record) { var requirements = record.requirements(); @@ -179,13 +191,13 @@ public class DroneInfoService { * Helper to check if a drone is available at the required date and time * * @param droneId the id of the drone to be checked - * @param record the medical dispatch record containing the required date and time + * @param record the medical dispatch record containing the required date and + * time * @return true if the drone is available, false otherwise */ private boolean checkAvailability(String droneId, MedDispatchRecRequest record) { URI droneUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint); - ServicePointDrones[] servicePoints = - restTemplate.getForObject(droneUrl, ServicePointDrones[].class); + ServicePointDrones[] servicePoints = restTemplate.getForObject(droneUrl, ServicePointDrones[].class); LocalDate requiredDate = record.date(); DayOfWeek requiredDay = requiredDate.getDayOfWeek(); @@ -204,8 +216,7 @@ public class DroneInfoService { private LngLat queryServicePointLocationByDroneId(String droneId) { URI droneUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint); - ServicePointDrones[] servicePoints = - restTemplate.getForObject(droneUrl, ServicePointDrones[].class); + ServicePointDrones[] servicePoints = restTemplate.getForObject(droneUrl, ServicePointDrones[].class); assert servicePoints != null; for (var sp : servicePoints) { @@ -222,8 +233,7 @@ public class DroneInfoService { private LngLat queryServicePointLocation(int id) { URI servicePointUrl = URI.create(baseUrl).resolve(servicePointsEndpoint); - ServicePoint[] servicePoints = - restTemplate.getForObject(servicePointUrl, ServicePoint[].class); + ServicePoint[] servicePoints = restTemplate.getForObject(servicePointUrl, ServicePoint[].class); assert servicePoints != null; for (var sp : servicePoints) { @@ -246,24 +256,22 @@ public class DroneInfoService { public List fetchRestrictedAreas() { URI restrictedUrl = URI.create(baseUrl).resolve(restrictedAreasEndpoint); - RestrictedArea[] restrictedAreas = - restTemplate.getForObject(restrictedUrl, RestrictedArea[].class); + RestrictedArea[] restrictedAreas = restTemplate.getForObject(restrictedUrl, RestrictedArea[].class); assert restrictedAreas != null; return Arrays.asList(restrictedAreas); } public List fetchServicePoints() { URI servicePointUrl = URI.create(baseUrl).resolve(servicePointsEndpoint); - ServicePoint[] servicePoints = - restTemplate.getForObject(servicePointUrl, ServicePoint[].class); + ServicePoint[] servicePoints = restTemplate.getForObject(servicePointUrl, ServicePoint[].class); assert servicePoints != null; return Arrays.asList(servicePoints); } public List fetchDronesForServicePoints() { URI servicePointDronesUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint); - ServicePointDrones[] servicePointDrones = - restTemplate.getForObject(servicePointDronesUrl, ServicePointDrones[].class); + ServicePointDrones[] servicePointDrones = restTemplate.getForObject(servicePointDronesUrl, + ServicePointDrones[].class); assert servicePointDrones != null; return Arrays.asList(servicePointDrones); } diff --git a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/service/PathFinderService.java b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/service/PathFinderService.java index b9d1bbc..ab6ef15 100644 --- a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/service/PathFinderService.java +++ b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/service/PathFinderService.java @@ -23,7 +23,6 @@ import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse.DronePa import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; @@ -32,6 +31,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import java.time.LocalDateTime; /** * Class that handles calculations about deliverypath @@ -44,8 +44,10 @@ import java.util.stream.Collectors; public class PathFinderService { /** - * Hard stop on how many pathfinding iterations we attempt for a single segment before bailing, - * useful for preventing infinite loops caused by precision quirks or unexpected map data. + * Hard stop on how many pathfinding iterations we attempt for a single segment + * before bailing, + * useful for preventing infinite loops caused by precision quirks or unexpected + * map data. * * @see #computePath(LngLat, LngLat) */ @@ -65,11 +67,14 @@ public class PathFinderService { private TelemetryService telemetryService; /** - * Constructor for PathFinderService. The dependencies are injected by Spring and the - * constructor pre-computes reference maps used throughout the request lifecycle. + * Constructor for PathFinderService. The dependencies are injected by Spring + * and the + * constructor pre-computes reference maps used throughout the request + * lifecycle. * * @param gpsCalculationService Service handling geometric operations. - * @param droneInfoService Service that exposes drone metadata and capability information. + * @param droneInfoService Service that exposes drone metadata and + * capability information. */ public PathFinderService( GpsCalculationService gpsCalculationService, DroneInfoService droneInfoService) { @@ -82,8 +87,7 @@ public class PathFinderService { this.drones = droneInfoService.fetchAllDrones(); List servicePoints = droneInfoService.fetchServicePoints(); - List servicePointAssignments = - droneInfoService.fetchDronesForServicePoints(); + List servicePointAssignments = droneInfoService.fetchDronesForServicePoints(); List restrictedAreas = droneInfoService.fetchRestrictedAreas(); this.droneById = this.drones.stream().collect(Collectors.toMap(Drone::id, drone -> drone)); @@ -101,17 +105,17 @@ public class PathFinderService { } } - this.servicePointLocations = - servicePoints.stream() - .collect( - Collectors.toMap( - ServicePoint::id, sp -> new LngLat(sp.location()))); + this.servicePointLocations = servicePoints.stream() + .collect( + Collectors.toMap( + ServicePoint::id, sp -> new LngLat(sp.location()))); this.restrictedRegions = restrictedAreas.stream().map(RestrictedArea::toRegion).toList(); } /** - * Produce a delivery plan for the provided dispatch records. Deliveries are grouped per + * Produce a delivery plan for the provided dispatch records. Deliveries are + * grouped per * compatible drone and per trip to satisfy each drone move limit. * * @param records Dispatch records to be fulfilled. @@ -157,17 +161,14 @@ public class PathFinderService { continue; } - List sortedDeliveries = - entry.getValue().stream() - .sorted( - Comparator.comparingDouble( - rec -> - gpsCalculationService.calculateDistance( - servicePointLocation, rec.delivery()))) - .toList(); + List sortedDeliveries = entry.getValue().stream() + .sorted( + Comparator.comparingDouble( + rec -> gpsCalculationService.calculateDistance( + servicePointLocation, rec.delivery()))) + .toList(); - List> trips = - splitTrips(sortedDeliveries, drone, servicePointLocation); + List> trips = splitTrips(sortedDeliveries, drone, servicePointLocation); for (List trip : trips) { TripResult result = buildTrip(drone, servicePointLocation, trip); @@ -194,9 +195,9 @@ public class PathFinderService { * GeoJSON FeatureCollection suitable for mapping visualization. * * @param records Dispatch records to be fulfilled. - * + * * @return GeoJSON payload representing every delivery flight path. - * + * * @throws IllegalStateException When the payload cannot be serialized. */ public String calculateDeliveryPathAsGeoJson(MedDispatchRecRequest[] records) { @@ -245,8 +246,10 @@ public class PathFinderService { } /** - * Group dispatch records by their assigned drone, ensuring every record is routed through - * {@link #findBestDrone(MedDispatchRecRequest)} exactly once and discarding invalid entries. + * Group dispatch records by their assigned drone, ensuring every record is + * routed through + * {@link #findBestDrone(MedDispatchRecRequest)} exactly once and discarding + * invalid entries. * * @param records Dispatch records to be grouped. * @return Map keyed by drone ID with the deliveries it should service. @@ -265,7 +268,8 @@ public class PathFinderService { } /** - * Choose the best drone for the provided record. Currently that equates to picking the closest + * Choose the best drone for the provided record. Currently that equates to + * picking the closest * compatible drone to the delivery location. * * @param record Dispatch record that needs fulfillment. @@ -290,9 +294,8 @@ public class PathFinderService { continue; } - double distance = - gpsCalculationService.calculateDistance( - servicePointLocation, record.delivery()); + double distance = gpsCalculationService.calculateDistance( + servicePointLocation, record.delivery()); if (distance < bestScore) { bestScore = distance; @@ -306,14 +309,16 @@ public class PathFinderService { } /** - * Break a sequence of deliveries into several trips that each respect the drone move limit. The + * Break a sequence of deliveries into several trips that each respect the drone + * move limit. The * deliveries should already be ordered by proximity for sensible grouping. * - * @param deliveries Deliveries assigned to a drone. - * @param drone Drone that will service the deliveries. + * @param deliveries Deliveries assigned to a drone. + * @param drone Drone that will service the deliveries. * @param servicePoint Starting and ending point of every trip. * @return Partitioned trips with at least one delivery each. - * @throws IllegalStateException If a single delivery exceeds the drone's move limit. + * @throws IllegalStateException If a single delivery exceeds the drone's move + * limit. */ private List> splitTrips( List deliveries, Drone drone, LngLat servicePoint) { @@ -349,13 +354,15 @@ public class PathFinderService { } /** - * Build a single trip for the provided drone, including the entire flight path to every - * delivery and back home. The resulting structure contains the {@link DronePath} representation + * Build a single trip for the provided drone, including the entire flight path + * to every + * delivery and back home. The resulting structure contains the + * {@link DronePath} representation * as well as cost and moves consumed. * - * @param drone Drone executing the trip. + * @param drone Drone executing the trip. * @param servicePoint Starting/ending location of the trip. - * @param deliveries Deliveries to include in the trip in execution order. + * @param deliveries Deliveries to include in the trip in execution order. * @return Trip information or {@code null} if no deliveries are provided. * @see DeliveryPathResponse.DronePath */ @@ -395,10 +402,9 @@ public class PathFinderService { flightPlans.add(new Delivery(delivery.id(), flightPath)); } - float cost = - drone.capability().costInitial() - + drone.capability().costFinal() - + (float) (drone.capability().costPerMove() * moves); + float cost = drone.capability().costInitial() + + drone.capability().costFinal() + + (float) (drone.capability().costPerMove() * moves); DronePath path = new DronePath(drone.parseId(), flightPlans); @@ -406,11 +412,12 @@ public class PathFinderService { } /** - * Estimate the number of moves a prospective trip would need by replaying the path calculation + * Estimate the number of moves a prospective trip would need by replaying the + * path calculation * without mutating any persistent state. * * @param servicePoint Trip origin. - * @param deliveries Deliveries that would compose the trip. + * @param deliveries Deliveries that would compose the trip. * @return Total moves required to fly the proposed itinerary. */ private int estimateTripMoves(LngLat servicePoint, List deliveries) { @@ -429,10 +436,11 @@ public class PathFinderService { } /** - * Build a path between {@code start} and {@code target} by repeatedly moving in snapped + * Build a path between {@code start} and {@code target} by repeatedly moving in + * snapped * increments while avoiding restricted zones. * - * @param start Start coordinate. + * @param start Start coordinate. * @param target Destination coordinate. * @return Sequence of visited coordinates and move count. * @see #nextPosition(LngLat, LngLat) @@ -462,18 +470,20 @@ public class PathFinderService { } /** - * Determine the next position on the path from {@code current} toward {@code target}, - * preferring the snapped angle closest to the desired heading that does not infiltrate a + * Determine the next position on the path from {@code current} toward + * {@code target}, + * preferring the snapped angle closest to the desired heading that does not + * infiltrate a * restricted region. * * @param current Current coordinate. - * @param target Destination coordinate. - * @return Next admissible coordinate or the original point if none can be found. + * @param target Destination coordinate. + * @return Next admissible coordinate or the original point if none can be + * found. */ private LngLat nextPosition(LngLat current, LngLat target) { - double desiredAngle = - Math.toDegrees( - Math.atan2(target.lat() - current.lat(), target.lng() - current.lng())); + double desiredAngle = Math.toDegrees( + Math.atan2(target.lat() - current.lat(), target.lng() - current.lng())); List candidateAngles = buildAngleCandidates(desiredAngle); for (Angle angle : candidateAngles) { LngLat next = gpsCalculationService.nextPosition(current, angle); @@ -485,8 +495,10 @@ public class PathFinderService { } /** - * Build a sequence of candidate angles centered on the desired heading, expanding symmetrically - * clockwise and counter-clockwise to explore alternative headings if the primary path is + * Build a sequence of candidate angles centered on the desired heading, + * expanding symmetrically + * clockwise and counter-clockwise to explore alternative headings if the + * primary path is * blocked. * * @param desiredAngle Bearing in degrees between current and target positions. @@ -521,15 +533,17 @@ public class PathFinderService { } /** - * Representation of a computed path segment wrapping the visited positions and the number of + * Representation of a computed path segment wrapping the visited positions and + * the number of * moves taken to traverse them. * * @param positions Ordered coordinates that describe the path. - * @param moves Number of moves consumed by the path. + * @param moves Number of moves consumed by the path. */ private record PathSegment(List positions, int moves) { /** - * Append the positions from this segment to {@code target}, skipping the first coordinate + * Append the positions from this segment to {@code target}, skipping the first + * coordinate * as it is already represented by the last coordinate in the consumer path. * * @param target Mutable list to append to. @@ -542,8 +556,10 @@ public class PathFinderService { } /** - * Bundle containing the calculated {@link DronePath}, total moves and financial cost for a + * Bundle containing the calculated {@link DronePath}, total moves and financial + * cost for a * single trip. */ - private record TripResult(DronePath path, int moves, float cost) {} + private record TripResult(DronePath path, int moves, float cost) { + } } diff --git a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/service/TelemetryService.java b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/service/TelemetryService.java index 00f58a4..33cfc24 100644 --- a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/service/TelemetryService.java +++ b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/service/TelemetryService.java @@ -1,24 +1,20 @@ package io.github.js0ny.ilp_coursework.service; +import java.net.http.HttpClient; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.concurrent.CompletableFuture; +import java.util.Map; + +import org.springframework.stereotype.Service; + import com.fasterxml.jackson.databind.ObjectMapper; import io.github.js0ny.ilp_coursework.data.common.DroneEvent; import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Service; - -import java.net.http.HttpClient; -import java.time.Duration; -import java.time.LocalDateTime; -import java.util.Map; -import java.util.concurrent.CompletableFuture; - @Service public class TelemetryService { - private static final Logger log = LoggerFactory.getLogger(TelemetryService.class); - private final HttpClient client; private final ObjectMapper mapper; @@ -28,8 +24,7 @@ public class TelemetryService { this.client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build(); this.mapper = new ObjectMapper(); - this.BLACKBOX_URL = - System.getenv().getOrDefault("BLACKBOX_ENDPOINT", "http://localhost:3000"); + this.BLACKBOX_URL = System.getenv().getOrDefault("BLACKBOX_ENDPOINT", "http://localhost:3000"); } public void sendEventAsyncByPathResponse(DeliveryPathResponse resp) { @@ -39,16 +34,15 @@ public class TelemetryService { } } - public void sendEventAsyncByPathResponse( - DeliveryPathResponse resp, LocalDateTime baseTimestamp) { + public void sendEventAsyncByPathResponse(DeliveryPathResponse resp, LocalDateTime baseTimestamp) { var events = DroneEvent.fromPathResponseWithTimestamp(resp, baseTimestamp); for (var event : events) { sendEventAsync(event); } } - public void sendEventAsyncByPathResponse( - DeliveryPathResponse resp, Map deliveryTimestamps) { + public void sendEventAsyncByPathResponse(DeliveryPathResponse resp, + Map deliveryTimestamps) { var events = DroneEvent.fromPathResponseWithTimestamps(resp, deliveryTimestamps); for (var event : events) { sendEventAsync(event); @@ -56,24 +50,22 @@ public class TelemetryService { } public void sendEventAsync(DroneEvent event) { - CompletableFuture.runAsync( - () -> { - try { - String json = mapper.writeValueAsString(event); - log.debug("Sending telemetry event: {}", json); - var request = - java.net.http.HttpRequest.newBuilder() - .uri(java.net.URI.create(BLACKBOX_URL + "/ingest")) - .header("Content-Type", "application/json") - .POST( - java.net.http.HttpRequest.BodyPublishers.ofString( - json)) - .build(); + CompletableFuture.runAsync(() -> { + try { + String json = mapper.writeValueAsString(event); + System.out.println("[INFO] Sending telemetry event: " + json); + var request = java.net.http.HttpRequest.newBuilder() + .uri(java.net.URI.create(BLACKBOX_URL + "/ingest")) + .header("Content-Type", "application/json") + .POST(java.net.http.HttpRequest.BodyPublishers.ofString(json)) + .build(); + + client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString()); + } catch (Exception e) { + System.err.println("[ERROR] Failed to send telemetry event: " + e.getMessage()); + } + }); - client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString()); - } catch (Exception e) { - log.error("Failed to send telemetry event: {}", e.getMessage()); - } - }); } + } 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 54a9627..a377da7 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,7 +15,6 @@ 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; @@ -410,79 +409,4 @@ 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 deleted file mode 100644 index b1b1535..0000000 --- a/ilp-rest-service/src/test/java/io/github/js0ny/ilp_coursework/controller/MapMetaControllerTest.java +++ /dev/null @@ -1,81 +0,0 @@ -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 deleted file mode 100644 index 9077d92..0000000 --- a/ilp-rest-service/src/test/java/io/github/js0ny/ilp_coursework/service/DroneAttrComparatorServiceTest.java +++ /dev/null @@ -1,107 +0,0 @@ -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 b549f52..6595e66 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,15 +4,11 @@ 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; @@ -234,145 +230,4 @@ 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); - } - } }