Sfoglia il codice sorgente

Created automated units by building and executing endpoints as a graph

Lukas Cerny 1 anno fa
parent
commit
1db6a8cac0

+ 7 - 6
build.gradle

@@ -54,11 +54,11 @@ dependencies {
     implementation 'org.apache.logging.log4j:log4j-api:2.22.0'
     implementation 'org.apache.logging.log4j:log4j-core:2.22.0'
 
-    implementation 'io.vertx:vertx-core:4.5.1'
-    implementation 'io.vertx:vertx-web:4.5.1'
-    implementation 'io.vertx:vertx-web-openapi:4.5.1'
-    implementation 'io.vertx:vertx-auth-jwt:4.5.1'
-    implementation 'io.vertx:vertx-pg-client:4.5.1'
+    implementation 'io.vertx:vertx-core:4.5.3'
+    implementation 'io.vertx:vertx-web:4.5.3'
+    implementation 'io.vertx:vertx-web-openapi:4.5.3'
+    implementation 'io.vertx:vertx-auth-jwt:4.5.3'
+    implementation 'io.vertx:vertx-pg-client:4.5.3'
     implementation 'org.postgresql:postgresql:42.7.1'
     implementation 'com.ongres.scram:client:2.1'
 
@@ -67,7 +67,8 @@ dependencies {
     testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2'
     testImplementation 'uk.org.webcompere:system-stubs-jupiter:2.1.5'
     testImplementation 'org.mockito:mockito-core:5.3.1'
-    testImplementation 'io.vertx:vertx-junit5:4.4.0'
+    testImplementation 'io.vertx:vertx-junit5:4.5.3'
+    testImplementation 'io.vertx:vertx-unit:4.5.3'
     testImplementation 'org.assertj:assertj-core:3.24.2'
     testImplementation 'org.openapi4j:openapi-parser:1.0.7'
     testImplementation 'org.openapi4j:openapi-schema-validator:1.0.7'

+ 20 - 4
src/main/java/cz/senslog/telemetry/app/PropertyConfig.java

@@ -29,7 +29,7 @@ public final class PropertyConfig {
         this.authConfig = authConfig;
     }
 
-    private static class HttpServer {
+    public static class HttpServer {
 
         public int getPort() {
             String portStr = System.getenv("SERVER_HTTP_PORT");
@@ -37,7 +37,7 @@ public final class PropertyConfig {
         }
     }
 
-    private static class Database {
+    public static class Database {
 
         public int getPort() {
             String portStr = System.getenv("DATABASE_PORT");
@@ -70,7 +70,7 @@ public final class PropertyConfig {
         }
     }
 
-    private static class TCPServer  {
+    public static class TCPServer  {
 
         public int getPort() {
             String portStr = System.getenv("SERVER_TCP_PORT");
@@ -78,7 +78,7 @@ public final class PropertyConfig {
         }
     }
 
-    private static class Auth {
+    public static class Auth {
 
         public boolean getDisabled() {
             return Boolean.parseBoolean(System.getenv("AUTH_DISABLED"));
@@ -99,6 +99,22 @@ public final class PropertyConfig {
         }
     }
 
+    public HttpServer httpServerConfig() {
+        return httpServerConfig;
+    }
+
+    public TCPServer tcpServerConfig() {
+        return tcpServerConfig;
+    }
+
+    public Database dbConfig() {
+        return dbConfig;
+    }
+
+    public Auth authConfig() {
+        return authConfig;
+    }
+
     public JsonObject server() {
         return JsonObject.of(
                 "http.port", httpServerConfig.getPort(),

+ 4 - 4
src/main/java/cz/senslog/telemetry/database/domain/SensorTelemetry.java

@@ -8,13 +8,13 @@ public class SensorTelemetry {
     private final long value;
     private final OffsetDateTime timestamp;
     private final Location location;
-    private final float speed;
+    private final int speed;
 
-    public static SensorTelemetry of(long id, long value, OffsetDateTime timestamp, Location location, float speed) {
+    public static SensorTelemetry of(long id, long value, OffsetDateTime timestamp, Location location, int speed) {
         return new SensorTelemetry(id, value, timestamp, location, speed);
     }
 
-    public SensorTelemetry(long id, long value, OffsetDateTime timestamp, Location location, float speed) {
+    public SensorTelemetry(long id, long value, OffsetDateTime timestamp, Location location, int speed) {
         this.id = id;
         this.value = value;
         this.timestamp = timestamp;
@@ -34,7 +34,7 @@ public class SensorTelemetry {
         return location;
     }
 
-    public float getSpeed() {
+    public int getSpeed() {
         return speed;
     }
 

+ 43 - 0
src/main/java/cz/senslog/telemetry/utils/HttpParamsBuilder.java

@@ -0,0 +1,43 @@
+package cz.senslog.telemetry.utils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class HttpParamsBuilder {
+
+    private final Map<String, String> params;
+
+    public static HttpParamsBuilder create() {
+        return new HttpParamsBuilder(new HashMap<>());
+    }
+
+    public static HttpParamsBuilder create(HttpParamsBuilder builder) {
+        return new HttpParamsBuilder(builder.params);
+    }
+
+    private HttpParamsBuilder(Map<String, String> params) {
+        this.params = params;
+    }
+
+    public HttpParamsBuilder add(String name, String value) {
+        params.put(name, value);
+        return this;
+    }
+
+    public HttpParamsBuilder mergeIn(HttpParamsBuilder builder) {
+        if (builder != null && !builder.params.isEmpty()) {
+            this.params.putAll(builder.params);
+        }
+        return this;
+    }
+
+    public String get() {
+        List<String> p = new ArrayList<>(params.size());
+        for (Map.Entry<String, String> entry : params.entrySet()) {
+            p.add(entry.getKey()+"="+entry.getValue());
+        }
+        return String.join("&", p);
+    }
+}

+ 8 - 11
src/main/resources/openAPISpec.yaml

@@ -1815,7 +1815,7 @@ components:
       type: object
       required:
         - timestamp
-        - value
+        - observedValue
         - speed
         - location
       properties:
@@ -1823,8 +1823,8 @@ components:
           type: string
           format: date-time
         observedValue:
-          type: integer
-          format: int64
+          type: number
+          format: double
         speed:
           type: integer
           format: int64
@@ -2549,11 +2549,6 @@ components:
         - sensorId
         - name
       x-NavigationLinks:
-        Phenomenon@NavigationLink:
-          type: string
-          format: uri
-          x-graph-properties:
-            linkTo: phenomenonIdGET
         Sensor@NavigationLink:
           type: string
           format: uri
@@ -2865,7 +2860,7 @@ components:
           type: string
           format: uri
           x-graph-properties:
-            linkTo: driverIdUnitIdActionIdEventIdGET
+            linkTo: eventIdGET
       properties:
         Event@NavigationLink:
           $ref: '#/components/schemas/EventBasicInfo/x-NavigationLinks/Event@NavigationLink'
@@ -2881,7 +2876,7 @@ components:
           type: string
           format: date-time
       example:
-        Event@NavigationLink: "<domain>/drivers/42/units/105/actions/258/events/999"
+        Event@NavigationLink: "<domain>/events/999"
         id: 999
         fromTime: "2023-01-25 15:35:32Z"
         toTime: "2023-03-20 10:35:32Z"
@@ -2900,7 +2895,7 @@ components:
           type: string
           format: uri
           x-graph-properties:
-            linkTo: driverIdUnitIdActionIdEventIdGET
+            linkTo: eventIdGET
         Driver@NavigationLink:
           type: string
           format: uri
@@ -2987,6 +2982,7 @@ components:
           minimum: 0
 
     Info:
+      type: object
       required:
         - name
         - version
@@ -3013,6 +3009,7 @@ components:
         uptime: "1:20:00"
 
     Error:
+      type: object
       required:
         - code
         - message

+ 403 - 0
src/test/java/cz/senslog/telemetry/MockSensLogRepository.java

@@ -0,0 +1,403 @@
+package cz.senslog.telemetry;
+
+import cz.senslog.telemetry.database.PagingRetrieve;
+import cz.senslog.telemetry.database.SortType;
+import cz.senslog.telemetry.database.domain.*;
+import cz.senslog.telemetry.database.repository.SensLogRepository;
+import io.vertx.core.Future;
+import io.vertx.core.json.JsonObject;
+
+import java.time.*;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class MockSensLogRepository implements SensLogRepository {
+
+    private static final Instant BASE_INSTANT_TIMESTAMP = LocalDateTime.of(2023, 1, 1, 0, 0).toInstant(ZoneOffset.UTC);
+
+    @Override
+    public Future<Integer> updateActionByDriverUnit(DriverAction data) {
+        return Future.succeededFuture(1);
+    }
+
+    @Override
+    public Future<Integer> saveTelemetry(UnitTelemetry data) {
+        return Future.succeededFuture(1);
+    }
+
+    @Override
+    public Future<Integer> saveAllTelemetry(List<UnitTelemetry> data) {
+        return Future.succeededFuture(1);
+    }
+
+    @Override
+    public Future<Boolean> createSensor(Sensor sensor, long unitId) {
+        return Future.succeededFuture(true);
+    }
+
+    @Override
+    public Future<List<Unit>> allUnits() {
+        return Future.succeededFuture(List.of(
+                Unit.of(1000, "mock(name)", "mock(imei)", "mock(description)"),
+                Unit.of(2000, "mock(name)", "mock(imei)", "mock(description)"),
+                Unit.of(3000, "mock(name)", "mock(imei)", "mock(description)"))
+        );
+    }
+
+    @Override
+    public Future<Unit> findUnitById(long unitId) {
+        return Future.succeededFuture(Unit.of(1000, "mock(name)", "mock(imei)", "mock(description)"));
+    }
+
+    @Override
+    public Future<Unit> findUnitByIMEI(String imei) {
+        return Future.succeededFuture(Unit.of(1000, "mock(imei)"));
+    }
+
+    @Override
+    public Future<List<Unit>> findUnitsBySensorId(long sensorId) {
+        return Future.succeededFuture(List.of(
+                Unit.of(1000, "mock(name)", "mock(imei)", "mock(description)"),
+                Unit.of(2000, "mock(name)", "mock(imei)", "mock(description)"),
+                Unit.of(3000, "mock(name)", "mock(imei)", "mock(description)"))
+        );
+    }
+
+    @Override
+    public Future<List<CampaignUnit>> findUnitsByCampaignId(long campaignId) {
+        OffsetDateTime baseTimestamp = OffsetDateTime.ofInstant(BASE_INSTANT_TIMESTAMP, ZoneOffset.UTC);
+        return Future.succeededFuture(List.of(
+                CampaignUnit.of(1000, 1, "mock(name)", "mock(description)", baseTimestamp, baseTimestamp.plusMonths(1)),
+                CampaignUnit.of(2000, 1, "mock(name)", "mock(description)", baseTimestamp, baseTimestamp.plusMonths(1))
+        ));
+    }
+
+    @Override
+    public Future<Set<Long>> findUnitsIDByCampaignId(long campaignId) {
+        return Future.succeededFuture(Set.of(1000L, 2000L, 3000L));
+    }
+
+    @Override
+    public Future<CampaignUnit> findUnitByIdAndCampaignId(long unitId, long campaignId) {
+        OffsetDateTime baseTimestamp = OffsetDateTime.ofInstant(BASE_INSTANT_TIMESTAMP, ZoneOffset.UTC);
+        return Future.succeededFuture(CampaignUnit.of(1000, 1, "mock(name)", "mock(type)", "mock(imei)", "mock(description)", baseTimestamp, baseTimestamp.plusMonths(1)));
+    }
+
+    @Override
+    public Future<Unit> findUnitByIdAndDriverId(long unitId, int driverId) {
+        return Future.succeededFuture(Unit.of(1000, "mock(name)", "mock(imei)", "mock(description)"));
+    }
+
+    @Override
+    public Future<Unit> findUnitByIdAndDriverIdAndActionId(long unitId, int driverId, int actionId) {
+        return Future.succeededFuture(Unit.of(1000, "mock(name)", "mock(imei)", "mock(description)"));
+    }
+
+    @Override
+    public Future<List<Unit>> findUnitsByDriverIdAndActionId(int driverId, int actionId) {
+        return Future.succeededFuture(List.of(
+                Unit.of(1000, "mock(name)", "mock(imei)", "mock(description)"),
+                Unit.of(2000, "mock(name)", "mock(imei)", "mock(description)"),
+                Unit.of(3000, "mock(name)", "mock(imei)", "mock(description)")
+        ));
+    }
+
+    @Override
+    public Future<List<Long>> findUnitIdsByCampaignId(long campaignId) {
+        return Future.succeededFuture(List.of(1L,2L,3L));
+    }
+
+    @Override
+    public Future<List<Sensor>> allSensors() {
+        return Future.succeededFuture(List.of(
+                Sensor.of(105, "mock(name)", "M", 98, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)"),
+                Sensor.of(106, "mock(name)", "M", 99, Phenomenon.of(16, "mock(phenomenon)"), "mock(description)"),
+                Sensor.of(107, "mock(name)", "M", 100, Phenomenon.of(17, "mock(phenomenon)"), "mock(description)"))
+        );
+    }
+
+    @Override
+    public Future<Sensor> findSensorById(long sensorId) {
+        return Future.succeededFuture(
+                Sensor.of(105, "mock(name)", "M", 98, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)")
+        );
+    }
+
+    @Override
+    public Future<Sensor> findSensorByIOAndUnitId(int ioID, long unitId) {
+        return Future.succeededFuture(Sensor.of(105, "mock(name)", "M", 98, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)"));
+    }
+
+    @Override
+    public Future<Map<Long, Sensor>> findSensorsByUnitIdGroupById(long unitId) {
+        return Future.succeededFuture(Map.of(
+                105L, Sensor.of(105, "mock(name)", "X"),
+                106L, Sensor.of(106, "mock(name)", "X"),
+                107L, Sensor.of(107, "mock(name)", "X")
+        ));
+    }
+
+    @Override
+    public Future<Map<Long, List<Sensor>>> findSensorsByCampaignIdGroupByUnitId(long campaignId) {
+        return Future.succeededFuture(Map.of(
+                1000L, List.of(
+                        Sensor.of(105, "mock(name)"),
+                        Sensor.of(106, "mock(name)")
+                ),
+                2000L, List.of(
+                        Sensor.of(107, "mock(name)"),
+                        Sensor.of(108, "mock(name)")
+                )
+        ));
+    }
+
+    @Override
+    public Future<List<Sensor>> findSensorsByUnitId(long unitId) {
+        return Future.succeededFuture(List.of(
+                Sensor.of(105, "mock(name)", "X"),
+                Sensor.of(106, "mock(name)", "X"),
+                Sensor.of(107, "mock(name)", "X"))
+        );
+    }
+
+    @Override
+    public Future<List<Sensor>> findSensorsByPhenomenonId(long phenomenonId) {
+        return Future.succeededFuture(List.of(
+                Sensor.of(105, "mock(name)", "M", 98, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)"),
+                Sensor.of(106, "mock(name)", "M", 99, Phenomenon.of(17, "mock(phenomenon)"), "mock(description)"),
+                Sensor.of(107, "mock(name)", "M", 100, Phenomenon.of(18, "mock(phenomenon)"), "mock(description)")
+        ));
+    }
+
+    @Override
+    public Future<List<Sensor>> findSensorsByCampaignIdAndUnitId(long campaignId, long unitId) {
+        return Future.succeededFuture(List.of(
+                Sensor.of(105, "mock(name)", "M"),
+                Sensor.of(106, "mock(name)", "M"),
+                Sensor.of(107, "mock(name)", "M")
+        ));
+    }
+
+    @Override
+    public Future<Sensor> findSensorByCampaignIdAndUnitId(long campaignId, long unitId, long sensorId) {
+        return Future.succeededFuture(
+                Sensor.of(105, "mock(name)", "M", 98, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)")
+        );
+    }
+
+    @Override
+    public Future<List<Phenomenon>> allPhenomenons() {
+        return Future.succeededFuture(List.of(
+                Phenomenon.of(1, "mock(name)"),
+                Phenomenon.of(2, "mock(name)"),
+                Phenomenon.of(3, "mock(name)"))
+        );
+    }
+
+    @Override
+    public Future<Phenomenon> findPhenomenonById(long phenomenonId) {
+        return Future.succeededFuture(Phenomenon.of(1, "mock(name)", "mock(uom)", "http://uomlink"));
+    }
+
+    @Override
+    public Future<List<Campaign>> allCampaigns() {
+        OffsetDateTime baseTimestamp = OffsetDateTime.ofInstant(BASE_INSTANT_TIMESTAMP, ZoneOffset.UTC);
+        return Future.succeededFuture(List.of(
+                Campaign.of(1, "mock(description)", baseTimestamp, baseTimestamp.plusYears(1)),
+                Campaign.of(2, "mock(description)", baseTimestamp, baseTimestamp.plusYears(2))));
+    }
+
+    @Override
+    public Future<Campaign> findCampaignById(long campaignId) {
+        OffsetDateTime baseTimestamp = OffsetDateTime.ofInstant(BASE_INSTANT_TIMESTAMP, ZoneOffset.UTC);
+        return Future.succeededFuture(Campaign.of(1, "mock(description)", baseTimestamp, baseTimestamp.plusYears(1)));
+    }
+
+    @Override
+    public Future<List<Campaign>> findCampaignsByUnitId(long unitId, ZoneId zone) {
+        OffsetDateTime baseTimestamp = OffsetDateTime.ofInstant(BASE_INSTANT_TIMESTAMP, ZoneOffset.UTC);
+        return Future.succeededFuture(List.of(
+                Campaign.of(1, "mock(description)", baseTimestamp, baseTimestamp.plusMonths(1)),
+                Campaign.of(2, "mock(description)", baseTimestamp.plusMonths(1), baseTimestamp.plusMonths(2)),
+                Campaign.of(3, "mock(description)", baseTimestamp.plusMonths(2), baseTimestamp.plusMonths(3)))
+        );
+    }
+
+    @Override
+    public Future<List<Driver>> allDrivers() {
+        return Future.succeededFuture(List.of(
+                Driver.of(1, "mock(name)"),
+                Driver.of(2, "mock(name)"),
+                Driver.of(3, "mock(name)")
+        ));
+    }
+
+    @Override
+    public Future<Driver> findDriverById(int driverId) {
+        return Future.succeededFuture(Driver.of(1, "mock(name)"));
+    }
+
+    @Override
+    public Future<List<Driver>> findDriversByUnitId(long unitId) {
+        return Future.succeededFuture(List.of(
+                Driver.of(1, "mock(name)"),
+                Driver.of(2, "mock(name)"))
+        );
+    }
+
+    @Override
+    public Future<List<Action>> findActionsByDriverIdAndUnitId(int driverId, long unitId) {
+        return Future.succeededFuture(List.of(
+                Action.of(1, "mock(name)"),
+                Action.of(2, "mock(name)"),
+                Action.of(3, "mock(name)")
+        ));
+    }
+
+    @Override
+    public Future<List<Action>> findActionsByDriverId(int driverId, OffsetDateTime from, OffsetDateTime to) {
+        return Future.succeededFuture(List.of(
+                Action.of(1, "mock(name)"),
+                Action.of(2, "mock(name)"),
+                Action.of(3, "mock(name)")
+        ));
+    }
+
+    @Override
+    public Future<Action> findActionByIdAndDriverId(int actionId, int driverId) {
+        return Future.succeededFuture(Action.of(1, "mock(name)"));
+    }
+
+    @Override
+    public Future<Action> findActionByIdAndDriverIdAndUnitId(int actionId, int driverId, long unitId) {
+        return Future.succeededFuture(Action.of(1, "mock(name)"));
+    }
+
+    @Override
+    public Future<List<Event>> findEventsByDriverIdAndUnitIdAndActionId(int driverId, long unitId, int actionId) {
+        OffsetDateTime baseTimestamp = OffsetDateTime.ofInstant(BASE_INSTANT_TIMESTAMP, ZoneOffset.UTC);
+        return Future.succeededFuture(List.of(
+                Event.of(1, 1, 1,1000, baseTimestamp, baseTimestamp.plusHours(8)),
+                Event.of(2, 2, 2,2000, baseTimestamp, baseTimestamp.plusHours(8)),
+                Event.of(3, 3, 3,3000, baseTimestamp, baseTimestamp.plusHours(8))
+        ));
+    }
+
+    @Override
+    public Future<Event> findEventById(long eventId) {
+        OffsetDateTime baseTimestamp = OffsetDateTime.ofInstant(BASE_INSTANT_TIMESTAMP, ZoneOffset.UTC);
+        return Future.succeededFuture(Event.of(1, 1, 1,1000, baseTimestamp, baseTimestamp.plusHours(8)));
+    }
+
+    @Override
+    public Future<List<Unit>> findUnitsByDriverId(int driverId, OffsetDateTime from, OffsetDateTime to) {
+        return Future.succeededFuture(List.of(
+                Unit.of(1000, "mock(name)", "mock(imei)", "mock(description)"),
+                Unit.of(2000, "mock(name)", "mock(imei)", "mock(description)"),
+                Unit.of(3000, "mock(name)", "mock(imei)", "mock(description)")
+        ));
+    }
+
+    private static List<UnitTelemetry> mockUnitTelemetries() {
+        OffsetDateTime baseTimestamp = OffsetDateTime.ofInstant(BASE_INSTANT_TIMESTAMP, ZoneOffset.UTC);
+        Location location = Location.of(14f, 49f, 450f, 0);
+        JsonObject observedValues = JsonObject.of("105", 0);
+        return List.of(
+                UnitTelemetry.of(1, 1000L, baseTimestamp.plusMinutes(1), location, 10, observedValues),
+                UnitTelemetry.of(2, 1000L, baseTimestamp.plusMinutes(2), location, 10, observedValues),
+                UnitTelemetry.of(3, 1000L, baseTimestamp.plusMinutes(3), location, 10, observedValues)
+        );
+    }
+
+    @Override
+    public Future<List<UnitTelemetry>> findObservationsByCampaignId(long campaignId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters) {
+        return Future.succeededFuture(mockUnitTelemetries());
+    }
+
+    @Override
+    public Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByCampaignIdWithPaging(long campaignId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters) {
+        List<UnitTelemetry> observations = mockUnitTelemetries();
+        return Future.succeededFuture(new PagingRetrieve<>(true, observations.size(), observations));
+    }
+
+    @Override
+    public Future<List<UnitTelemetry>> findObservationsByCampaignIdAndUnitId(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters) {
+        return Future.succeededFuture(mockUnitTelemetries());
+    }
+
+    @Override
+    public Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByCampaignIdAndUnitIdWithPaging(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters) {
+        List<UnitTelemetry> observations = mockUnitTelemetries();
+        return Future.succeededFuture(new PagingRetrieve<>(true, observations.size(), observations));
+    }
+
+    @Override
+    public Future<List<UnitTelemetry>> findObservationsByEventId(long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters) {
+        return Future.succeededFuture(mockUnitTelemetries());
+    }
+
+    @Override
+    public Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByEventIdWithPaging(long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters) {
+        List<UnitTelemetry> observations = mockUnitTelemetries();
+        return Future.succeededFuture(new PagingRetrieve<>(true, observations.size(), observations));
+    }
+
+    private static List<SensorTelemetry> mockSensorTelemetries() {
+        OffsetDateTime baseTimestamp = OffsetDateTime.ofInstant(BASE_INSTANT_TIMESTAMP, ZoneOffset.UTC);
+        Location location = Location.of(14f, 49f, 450f, 0);
+        return List.of(
+                SensorTelemetry.of(1, 10, baseTimestamp.plusMinutes(1), location, 10),
+                SensorTelemetry.of(2, 10, baseTimestamp.plusMinutes(2), location, 10),
+                SensorTelemetry.of(3, 10, baseTimestamp.plusMinutes(3), location, 10)
+        );
+    }
+
+    @Override
+    public Future<List<SensorTelemetry>> findObservationsByCampaignIdAndUnitIdAndSensorId(long campaignId, long unitId, long sensorId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters) {
+        return Future.succeededFuture(mockSensorTelemetries());
+    }
+
+    @Override
+    public Future<PagingRetrieve<List<SensorTelemetry>>> findObservationsByCampaignIdAndUnitIdAndSensorIdWithPaging(long campaignId, long unitId, long sensorId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters) {
+        List<SensorTelemetry> mock = mockSensorTelemetries();
+        return Future.succeededFuture(new PagingRetrieve<>(true, mock.size(), mock));
+    }
+
+    private static List<UnitLocation> mockUnitLocations() {
+        OffsetDateTime baseTimestamp = OffsetDateTime.ofInstant(BASE_INSTANT_TIMESTAMP, ZoneOffset.UTC);
+        Location location = Location.of(14f, 49f, 450f, 0);
+        return List.of(
+                UnitLocation.of(1000, baseTimestamp.plusMinutes(1), location),
+                UnitLocation.of(1000, baseTimestamp.plusMinutes(2), location),
+                UnitLocation.of(1000, baseTimestamp.plusMinutes(3), location)
+        );
+    }
+
+    @Override
+    public Future<List<UnitLocation>> findLocationsByCampaignIdAndUnitId(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters) {
+        return Future.succeededFuture(mockUnitLocations());
+    }
+
+    @Override
+    public Future<PagingRetrieve<List<UnitLocation>>> findLocationsByCampaignIdAndUnitIdWithPaging(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters) {
+        List<UnitLocation> mock = mockUnitLocations();
+        return Future.succeededFuture(new PagingRetrieve<>(true, mock.size(), mock));
+    }
+
+    @Override
+    public Future<List<UnitLocation>> findLocationsByEventId(long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters) {
+        return Future.succeededFuture(mockUnitLocations());
+    }
+
+    @Override
+    public Future<PagingRetrieve<List<UnitLocation>>> findLocationsByEventIdWithPaging(long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters) {
+        List<UnitLocation> mock = mockUnitLocations();
+        return Future.succeededFuture(new PagingRetrieve<>(true, mock.size(), mock));
+    }
+
+    @Override
+    public Future<List<UnitLocation>> findUnitsLocationsByCampaignId(long campaignId, int limitPerUnit, OffsetDateTime from, OffsetDateTime to, ZoneId zone, SortType sort, List<Filter> filters) {
+        return Future.succeededFuture(mockUnitLocations());
+    }
+}

+ 273 - 0
src/test/java/cz/senslog/telemetry/server/ws/OpenAPIHandlerAutoTest.java

@@ -0,0 +1,273 @@
+package cz.senslog.telemetry.server.ws;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import cz.senslog.telemetry.MockSensLogRepository;
+import cz.senslog.telemetry.TestPropertiesUtils;
+import cz.senslog.telemetry.app.PropertyConfig;
+import cz.senslog.telemetry.server.HttpVertxServer;
+import cz.senslog.telemetry.utils.HttpParamsBuilder;
+import cz.senslog.telemetry.utils.Tuple;
+import io.vertx.core.DeploymentOptions;
+import io.vertx.core.MultiMap;
+import io.vertx.core.Vertx;
+import io.vertx.core.http.*;
+import io.vertx.core.json.JsonObject;
+import io.vertx.junit5.Checkpoint;
+import io.vertx.junit5.VertxExtension;
+import io.vertx.junit5.VertxTestContext;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.openapi4j.core.exception.ResolutionException;
+import org.openapi4j.core.validation.ValidationException;
+import org.openapi4j.parser.OpenApi3Parser;
+import org.openapi4j.parser.model.v3.OpenApi3;
+import org.openapi4j.parser.model.v3.Path;
+import org.openapi4j.parser.model.v3.Schema;
+import org.openapi4j.schema.validator.ValidationContext;
+import org.openapi4j.schema.validator.ValidationData;
+import org.openapi4j.schema.validator.v3.SchemaValidator;
+import uk.org.webcompere.systemstubs.environment.EnvironmentVariables;
+import uk.org.webcompere.systemstubs.jupiter.SystemStub;
+import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.*;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static cz.senslog.telemetry.server.ws.ContentType.JSON;
+import static java.util.stream.Collectors.toSet;
+import static org.assertj.core.api.Assertions.assertThat;
+
+@ExtendWith({VertxExtension.class, SystemStubsExtension.class})
+class OpenAPIHandlerAutoTest {
+
+    private static OpenApi3 OPEN_API;
+
+    @SystemStub
+    private EnvironmentVariables envVariable;
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    private BlockingQueue<Tuple<String, String>> testNodes;
+
+    private static Map<String, HttpParamsBuilder> customReqParams;
+
+    private static Set<String> VISITED_TEST_NODES;
+
+    private enum SchemaType {
+        OBJECT, ARRAY
+
+        ;
+        public static SchemaType of(String type) {
+            if (type == null || type.isBlank()) {
+                return null;
+            }
+            return valueOf(type.toUpperCase());
+        }
+    }
+
+    private static String fullURIToHostPath(String fullURI)  {
+        try {
+            return new URI(fullURI).toURL().getPath();
+        } catch (URISyntaxException | MalformedURLException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    private static String getPathByOperationId(final String operationId) {
+        return OPEN_API.getPaths().entrySet().stream().filter(e -> e.getValue().getGet().getOperationId().equalsIgnoreCase(operationId))
+                .findFirst().map(Map.Entry::getKey).orElseThrow(() -> new RuntimeException("No path for the operationId '"+operationId+"'"));
+    }
+
+    private static SchemaType getSchemaType(Schema schema) {
+        String type = schema.getType();
+        if (type != null) {
+            return SchemaType.of(type);
+        } else {
+            JsonNode refNode = schema.getReference(OPEN_API.getContext()).getContent();
+            assertThat(refNode).isNotNull().isNotEmpty();
+            assertThat(refNode.has("type")).isTrue();
+            return SchemaType.of(refNode.get("type").asText());
+        }
+    }
+
+    private static Set<String> findRootOperationIds() {
+        int nodes = OPEN_API.getPaths().size();
+        Map<String, Integer> operationIdToIndex = new HashMap<>(nodes);
+        int [][] adjacencyMatrix = new int[nodes][nodes];
+
+        AtomicInteger counter = new AtomicInteger(0);
+        OPEN_API.getPaths().values().forEach(p -> operationIdToIndex.put(p.getGet().getOperationId(), counter.getAndIncrement()));
+
+        for (Map.Entry<String, Path> pathEntry : OPEN_API.getPaths().entrySet()) {
+            String sourceOperationId = pathEntry.getValue().getGet().getOperationId();
+            int sourceId = Optional.ofNullable(operationIdToIndex.get(sourceOperationId)).orElseThrow(() -> new IllegalArgumentException("No value for the '"+sourceOperationId+"'"));
+            Schema nodeSchema = pathEntry.getValue().getGet().getResponse("200").getContentMediaType(JSON.contentType()).getSchema();
+            Schema specSchemaElement;
+            switch (Objects.requireNonNull(getSchemaType(nodeSchema), "Unsupported schema type.")) {
+                case ARRAY ->   specSchemaElement = nodeSchema.getItemsSchema();
+                case OBJECT ->  specSchemaElement = nodeSchema;
+                default -> throw new IllegalArgumentException("Unsupported schema type '"+nodeSchema.getType()+"'.");
+            }
+            JsonNode schemaNavLinks = specSchemaElement.getReference(OPEN_API.getContext()).getContent().get("x-NavigationLinks");
+            Iterator<Map.Entry<String, JsonNode>> iter = Optional.ofNullable(schemaNavLinks).map(JsonNode::fields).orElse(Collections.emptyIterator());
+            while(iter.hasNext()) {
+                String destinOperationId = iter.next().getValue().get("x-graph-properties").get("linkTo").asText();
+                int destId = Optional.ofNullable(operationIdToIndex.get(destinOperationId)).orElseThrow(() -> new IllegalArgumentException("No value for the '"+destinOperationId+"'"));
+                adjacencyMatrix[sourceId][destId]++;
+            }
+        }
+        Set<String> roots = new HashSet<>();
+        for (int i = 0; i < adjacencyMatrix.length; i++) {
+            boolean isAdjacent = true;
+            for (int j = 0; j < adjacencyMatrix[i].length; j++) {
+                if (adjacencyMatrix[j][i] != 0) {
+                    isAdjacent = false; break;
+                }
+            }
+            if (isAdjacent) {
+                int finalI = i;
+                roots.add(operationIdToIndex.entrySet().stream().filter(e -> e.getValue() == finalI).map(Map.Entry::getKey).findFirst().orElse(""));
+            }
+        }
+
+        return roots;
+    }
+
+    private void testApiNode(final boolean navigationLinkParam, final Tuple<String, String> nodeToTest, final Vertx vertx, final VertxTestContext testContext, final Runnable finishHandler) {
+
+        final String operationId = nodeToTest.item1();
+        final String operationPath = nodeToTest.item2();
+
+        HttpParamsBuilder paramsBuilder = HttpParamsBuilder.create(Optional.ofNullable(customReqParams.get(operationId)).orElse(HttpParamsBuilder.create())
+                .add("navigationLinks", Boolean.toString(navigationLinkParam)));
+
+        int reqPort = PropertyConfig.getInstance().httpServerConfig().getPort();
+        RequestOptions reqOpt = new RequestOptions()
+                .setMethod(HttpMethod.GET).setPort(reqPort).setHost("localhost").setURI(operationPath+"?"+paramsBuilder.get())
+                .setHeaders(MultiMap.caseInsensitiveMultiMap()
+                        .add(HttpHeaders.ACCEPT, JSON.contentType()));
+
+        vertx.createHttpClient().request(reqOpt).compose(req -> req.send().compose(HttpClientResponse::body))
+                .onComplete(testContext.succeeding(buffer -> testContext.verify(() -> {
+                    // verify the JSON result according to the spec
+                    JsonNode responseResult = mapper.readTree(buffer.toString());
+                    Schema specSchema = OPEN_API.getOperationById(operationId).getResponse("200").getContentMediaType(JSON.contentType()).getSchema();
+                    SchemaValidator schemaValidator = new SchemaValidator(new ValidationContext<>(OPEN_API.getContext()), null, specSchema.toNode());
+                    ValidationData<Void> validation = new ValidationData<>();
+                    schemaValidator.validate(responseResult, validation);
+                    assertThat(validation.isValid()).overridingErrorMessage(validation.results().toString()).isTrue();
+
+                    // mark the 'operationId' as processed
+                    VISITED_TEST_NODES.add(operationId);
+
+                    // get first JSON element with NavigationLinks from the result
+                    JsonNode resJsonElement; Schema specSchemaElement;
+                    switch (getSchemaType(specSchema)) {
+                        case ARRAY -> {
+                            assertThat(responseResult).isNotEmpty();
+                            resJsonElement = responseResult.get(0);
+                            specSchemaElement = specSchema.getItemsSchema();
+                        }
+                        case OBJECT -> {
+                            resJsonElement = responseResult;
+                            specSchemaElement = specSchema;
+                        }
+                        default -> throw new IllegalArgumentException("Unsupported schema type '"+specSchema.getType()+"'.");
+                    }
+
+                    // get NavigationLinks from the spec
+                    JsonNode schemaNavLinks = specSchemaElement.getReference(OPEN_API.getContext()).getContent().get("x-NavigationLinks");
+                    Iterator<Map.Entry<String, JsonNode>> iter = Optional.ofNullable(schemaNavLinks).map(JsonNode::fields).orElse(Collections.emptyIterator());
+                    while(iter.hasNext()) {
+                        Map.Entry<String, JsonNode> navLinkEntry = iter.next();
+                        String navigationLink = navLinkEntry.getKey();
+
+                        // verify if the NavigationLink in spec is included in the response element...
+                        assertThat(resJsonElement.has(navigationLink))
+                            .overridingErrorMessage("Parameter '%s' should %sbe present in the endpoint '%s'.", navigationLink, navigationLinkParam ? "" : "not ", operationId)
+                            .isEqualTo(navigationLinkParam);
+
+                        if (navigationLinkParam) {
+                            // ... if so, get its 'operationId' of the NavigationLink from the spec....
+                            String destinOperationId = navLinkEntry.getValue().get("x-graph-properties").get("linkTo").asText();
+                            // ... and the link from the response element
+                            String linkToHostPath = fullURIToHostPath(resJsonElement.get(navigationLink).asText());
+
+                            if (!VISITED_TEST_NODES.contains(destinOperationId)) {
+                                // create a new node/path to test, if not processed yet
+                                testNodes.put(Tuple.of(destinOperationId, linkToHostPath));
+                            }
+                        }
+                    }
+                    finishHandler.run();
+                })));
+    }
+
+    @BeforeAll
+    static void setUp() throws MalformedURLException, ResolutionException, ValidationException {
+        OPEN_API = new OpenApi3Parser().parse(new URL("file:src/main/resources/openAPISpec.yaml"), false);
+        VISITED_TEST_NODES = new HashSet<>(OPEN_API.getPaths().size());
+
+        customReqParams = new HashMap<>();
+        customReqParams.put("campaignIdUnitsObservationsLocationsGET", HttpParamsBuilder.create()
+                .add("limitPerUnit", "1")
+        );
+    }
+
+    @BeforeEach
+    @DisplayName("Deploy the HTTP Server verticle")
+    void deployVerticle(Vertx vertx, VertxTestContext testContext) {
+        Properties props = TestPropertiesUtils.loadFromResources("tests.junit.env");
+        for (Map.Entry<Object, Object> propEntry : props.entrySet()) {
+            envVariable.set(propEntry.getKey(), propEntry.getValue());
+        }
+
+        PropertyConfig config = PropertyConfig.getInstance();
+        vertx.deployVerticle(new HttpVertxServer(new MockSensLogRepository()), new DeploymentOptions().setConfig(JsonObject.of(
+                "server", config.server(),
+                "auth", config.auth()
+        )), testContext.succeedingThenComplete());
+    }
+
+    @Test
+    void rootEntry(Vertx vertx, VertxTestContext testContext) throws InterruptedException {
+        int pathsCount = OPEN_API.getPaths().size();
+        int requestsCount = pathsCount * 2;
+
+        Checkpoint checkpoint = testContext.checkpoint(requestsCount);
+        testNodes = new LinkedBlockingDeque<>(requestsCount);
+
+        for (String rootNode : findRootOperationIds()) {
+            testNodes.put(Tuple.of(rootNode, getPathByOperationId(rootNode)));
+        }
+
+        for(int i = 0; i < pathsCount; i++) {
+            Tuple<String, String> node = testNodes.take();
+            testApiNode(true, node, vertx, testContext, checkpoint::flag);
+            testApiNode(false, node, vertx, testContext, checkpoint::flag);
+        }
+    }
+
+    @AfterEach
+    @DisplayName("Check that the verticle is still there")
+    void lastChecks(Vertx vertx) {
+        assertThat(vertx.deploymentIDs())
+                .isNotEmpty()
+                .hasSize(1);
+    }
+
+    @AfterAll
+    static void tearDown() {
+        if (OPEN_API.getPaths().size() != VISITED_TEST_NODES.size()) {
+            assertThat(VISITED_TEST_NODES).containsAll(OPEN_API.getPaths().values()
+                    .stream().map(p -> p.getGet().getOperationId()).collect(toSet()));
+        }
+    }
+}

+ 226 - 0
src/test/java/cz/senslog/telemetry/server/ws/OpenAPIHandlerVertxAutoTest.java

@@ -0,0 +1,226 @@
+package cz.senslog.telemetry.server.ws;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import cz.senslog.telemetry.MockSensLogRepository;
+import cz.senslog.telemetry.TestPropertiesUtils;
+import cz.senslog.telemetry.app.PropertyConfig;
+import cz.senslog.telemetry.server.HttpVertxServer;
+import cz.senslog.telemetry.utils.Tuple;
+import io.vertx.core.DeploymentOptions;
+import io.vertx.core.MultiMap;
+import io.vertx.core.Vertx;
+import io.vertx.core.http.*;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.unit.Async;
+import io.vertx.ext.unit.TestContext;
+import io.vertx.ext.unit.TestSuite;
+import io.vertx.junit5.VertxExtension;
+import io.vertx.junit5.VertxTestContext;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.openapi4j.core.exception.EncodeException;
+import org.openapi4j.core.exception.ResolutionException;
+import org.openapi4j.core.validation.ValidationException;
+import org.openapi4j.parser.OpenApi3Parser;
+import org.openapi4j.parser.model.v3.OpenApi3;
+import org.openapi4j.parser.model.v3.Operation;
+import org.openapi4j.parser.model.v3.Schema;
+import org.openapi4j.schema.validator.ValidationContext;
+import org.openapi4j.schema.validator.ValidationData;
+import org.openapi4j.schema.validator.v3.SchemaValidator;
+import uk.org.webcompere.systemstubs.environment.EnvironmentVariables;
+import uk.org.webcompere.systemstubs.jupiter.SystemStub;
+import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.*;
+
+import static cz.senslog.telemetry.server.ws.ContentType.JSON;
+import static java.util.stream.Collectors.toSet;
+import static org.assertj.core.api.Assertions.assertThat;
+
+@ExtendWith(SystemStubsExtension.class)
+class OpenAPIHandlerVertxAutoTest {
+
+    private static final int PORT = 8080;
+    private static final String HOST = "localhost";
+    private static OpenApi3 OPEN_API;
+
+    @SystemStub
+    private EnvironmentVariables envVariable;
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    private static Queue<Tuple<String, String>> TEST_NODES;
+
+    private static Set<String> VISITED_TEST_NODES;
+
+    enum SchemaType {
+        OBJECT, ARRAY
+
+        ;
+        public static SchemaType of(String type) {
+            if (type == null || type.isBlank()) {
+                return null;
+            }
+            return valueOf(type.toUpperCase());
+        }
+    }
+
+    private static String fullURIToHostPath(String fullURI)  {
+        try {
+            return new URI(fullURI).toURL().getPath();
+        } catch (URISyntaxException | MalformedURLException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    private static String getPathByOperationId(final String operationId) {
+        return OPEN_API.getPaths().entrySet().stream().filter(e -> e.getValue().getGet().getOperationId().equalsIgnoreCase(operationId))
+                .findFirst().map(Map.Entry::getKey).orElseThrow(() -> new RuntimeException("No path for the operationId '"+operationId+"'"));
+    }
+
+    private static SchemaType getSchemaType(Schema schema) {
+        String type = schema.getType();
+        if (type != null) {
+            return SchemaType.of(type);
+        } else {
+            JsonNode refNode = schema.getReference(OPEN_API.getContext()).getContent();
+            assertThat(refNode).isNotNull().isNotEmpty();
+            assertThat(refNode.has("type")).isTrue();
+            return SchemaType.of(refNode.get("type").asText());
+        }
+    }
+
+    private static void testApiNode(Vertx vertx, TestContext testContext) {
+
+        final int expectedHttpStatusCode = 200;
+
+        Async async = testContext.async();
+        Tuple<String, String> nodeToTest = TEST_NODES.remove();
+        final String operationId = nodeToTest.item1();
+        final String operationPath = nodeToTest.item2();
+
+        final boolean navigationLinkParam = true;
+
+
+        HttpClient httpClient = vertx.createHttpClient();
+        Operation operation = OPEN_API.getOperationById(operationId);
+        RequestOptions reqOpt = new RequestOptions()
+                .setMethod(HttpMethod.GET).setPort(PORT).setHost(HOST).setURI(operationPath+"?navigationLinks="+navigationLinkParam)
+                .setHeaders(MultiMap.caseInsensitiveMultiMap()
+                        .add(HttpHeaders.ACCEPT, JSON.contentType()));
+
+        httpClient.request(reqOpt).compose(req -> req.send().compose(HttpClientResponse::body))
+                .onComplete(testContext.asyncAssertSuccess(buffer -> testContext.verify(v -> {
+                    // verify the JSON result according to the spec
+                    JsonNode responseResult = null;
+                    try {
+                        responseResult = mapper.readTree(buffer.toString());
+                    } catch (JsonProcessingException e) {
+                        throw new RuntimeException(e);
+                    }
+                    Schema specSchema = operation.getResponse(Integer.toString(expectedHttpStatusCode)).getContentMediaType(JSON.contentType()).getSchema();
+                    SchemaValidator schemaValidator = null;
+                    try {
+                        schemaValidator = new SchemaValidator(new ValidationContext<>(OPEN_API.getContext()), null, specSchema.toNode());
+                    } catch (EncodeException e) {
+                        throw new RuntimeException(e);
+                    }
+                    ValidationData<Void> validation = new ValidationData<>();
+                    schemaValidator.validate(responseResult, validation);
+                    assertThat(validation.isValid()).overridingErrorMessage(validation.results().toString()).isTrue();
+
+                    // mark the 'operationId' as processed
+                    VISITED_TEST_NODES.add(operationId);
+
+                    // get first JSON element with NavigationLinks from the result
+                    JsonNode resJsonElement; Schema specSchemaElement;
+                    switch (getSchemaType(specSchema)) {
+                        case ARRAY -> {
+                            assertThat(responseResult).isNotEmpty();
+                            resJsonElement = responseResult.get(0);
+                            specSchemaElement = specSchema.getItemsSchema();
+                        }
+                        case OBJECT -> {
+                            resJsonElement = responseResult;
+                            specSchemaElement = specSchema;
+                        }
+                        default -> throw new IllegalArgumentException("Unsupported schema type '"+specSchema.getType()+"'.");
+                    }
+
+                    // get NavigationLinks from the spec
+                    JsonNode schemaNavLinks = specSchemaElement.getReference(OPEN_API.getContext()).getContent().get("x-NavigationLinks");
+                    if (schemaNavLinks != null && !schemaNavLinks.isEmpty()) {
+                        Iterator<String> navLinkIter = schemaNavLinks.fieldNames();
+                        while (navLinkIter.hasNext()) {
+                            String navigationLink = navLinkIter.next();
+
+                            // verify if the NavigationLink in spec is included in the response element...
+                            assertThat(resJsonElement.has(navigationLink))
+                                    .overridingErrorMessage("Parameter '%s' should %sbe present.", navigationLink, navigationLinkParam ? "" : "not ")
+                                    .isEqualTo(navigationLinkParam);
+
+                            if (navigationLinkParam) {
+                                // ... if so, get its 'operationId' of the NavigationLink from the spec....
+                                String linkToOperationId = schemaNavLinks.get(navigationLink).get("x-graph-properties").get("linkTo").asText();
+                                // ... and the link from the response element
+                                String linkToHostPath = fullURIToHostPath(resJsonElement.get(navigationLink).asText());
+
+                                if (!VISITED_TEST_NODES.contains(linkToOperationId)) {
+                                    // create a new node/path to test, if not processed yet
+                                    TEST_NODES.add(Tuple.of(linkToOperationId, linkToHostPath));
+                                }
+                            }
+                        }
+                    }
+                    async.complete();
+                })));
+    }
+
+    @Test
+    void rootEntry() throws MalformedURLException, ResolutionException, ValidationException {
+        OPEN_API = new OpenApi3Parser().parse(new URL("file:src/main/resources/openAPISpec.yaml"), false);
+        VISITED_TEST_NODES = new HashSet<>(OPEN_API.getPaths().size());
+        TEST_NODES = new LinkedList<>();
+
+        Vertx vertx = Vertx.vertx();
+
+        TestSuite testSuite = TestSuite.create("rootEntry");
+
+        testSuite.before(context -> {
+            Properties props = TestPropertiesUtils.loadFromResources("tests.junit.env");
+            for (Map.Entry<Object, Object> propEntry : props.entrySet()) {
+                envVariable.set(propEntry.getKey(), propEntry.getValue());
+            }
+
+            PropertyConfig config = PropertyConfig.getInstance();
+            vertx.deployVerticle(new HttpVertxServer(new MockSensLogRepository()), new DeploymentOptions().setConfig(JsonObject.of(
+                    "server", config.server(),
+                    "auth", config.auth()
+            )), context.asyncAssertSuccess());
+        });
+
+        testSuite.afterEach(context -> context.assertEquals(vertx.deploymentIDs(), 1));
+
+        testSuite.after(context -> System.out.println("Completed " + VISITED_TEST_NODES.size() + " nodes."));
+
+
+        final String[] rootNodes = new String[] { "infoGET",  "campaignsGET"};
+        for (String rootNode : rootNodes) {
+            TEST_NODES.add(Tuple.of(rootNode, getPathByOperationId(rootNode)));
+        }
+
+        testSuite.test("infoGET", context -> testApiNode(vertx, context));
+
+        testSuite.run(vertx);
+    }
+}