From 3748f18e864767291235e9c32fa80a5d3be928c4 Mon Sep 17 00:00:00 2001 From: Karthees Kalidass Date: Thu, 3 Sep 2020 17:43:30 +0200 Subject: [PATCH] [#2112] Add integration tests for search devices operation Signed-off-by: Kartheeswaran Kalidass --- tests/pom.xml | 4 + .../eclipse/hono/tests/CrudHttpClient.java | 48 +++- .../hono/tests/DeviceRegistryHttpClient.java | 50 ++++ .../tests/registry/DeviceManagementIT.java | 232 ++++++++++++++++++ 4 files changed, 333 insertions(+), 1 deletion(-) diff --git a/tests/pom.xml b/tests/pom.xml index 0f00780c93..9c976ed4f7 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -43,6 +43,8 @@ Test cases are run against Docker images of Hono server + (Apache Qpid Dispatch true true + + false hono-dispatch-router.hono hono-service-device-connection.hono hono-service-device-registry.hono @@ -202,6 +204,7 @@ Test cases are run against Docker images of Hono server + (Apache Qpid Dispatch hono-it-tests deviceregistry-mongodb false + true @@ -1093,6 +1096,7 @@ Test cases are run against Docker images of Hono server + (Apache Qpid Dispatch ${deviceregistry.http.port} ${deviceregistry.supportsGatewayMode} ${deviceregistry.credentials.supportsClientContext} + ${deviceregistry.supportsSearchDevices} ${coap.ip} ${coap.port} ${coaps.port} diff --git a/tests/src/test/java/org/eclipse/hono/tests/CrudHttpClient.java b/tests/src/test/java/org/eclipse/hono/tests/CrudHttpClient.java index 3c46a341ca..2d90c62338 100644 --- a/tests/src/test/java/org/eclipse/hono/tests/CrudHttpClient.java +++ b/tests/src/test/java/org/eclipse/hono/tests/CrudHttpClient.java @@ -484,6 +484,28 @@ public Future> get(final String uri, final ResponsePredicat return get(createRequestOptions().setURI(uri), successPredicates); } + /** + * Retrieves a resource representation using a HTTP GET request. + * + * @param uri The resource to retrieve. + * @param queryParams The query parameters for the request. + * @param successPredicates Checks on the HTTP response that need to pass for the request + * to be considered successful. + * @return A future indicating the outcome of the request. The future will be completed with the + * HTTP response if all checks on the response have succeeded. + * Otherwise the future will be failed with the error produced by the first failing + * predicate. + * @throws NullPointerException if URI is {@code null}. + */ + public Future> get( + final String uri, + final MultiMap queryParams, + final ResponsePredicate ... successPredicates) { + + Objects.requireNonNull(uri); + return get(createRequestOptions().setURI(uri), queryParams, successPredicates); + } + /** * Retrieves a resource representation using an HTTP GET request. * @@ -494,9 +516,31 @@ public Future> get(final String uri, final ResponsePredicat * HTTP response if all checks on the response have succeeded. * Otherwise the future will be failed with the error produced by the first failing * predicate. + */ + public Future> get( + final RequestOptions requestOptions, + final ResponsePredicate... successPredicates) { + + return get(requestOptions, null, successPredicates); + } + + /** + * Retrieves a resource representation using a HTTP GET request. + * + * @param requestOptions The options to use for the request. + * @param queryParams The query parameters for the request. + * @param successPredicates Checks on the HTTP response that need to pass for the request + * to be considered successful. + * @return A future indicating the outcome of the request. The future will be completed with the + * HTTP response if all checks on the response have succeeded. + * Otherwise the future will be failed with the error produced by the first failing + * predicate. * @throws NullPointerException if options is {@code null}. */ - public Future> get(final RequestOptions requestOptions, final ResponsePredicate ... successPredicates) { + public Future> get( + final RequestOptions requestOptions, + final MultiMap queryParams, + final ResponsePredicate... successPredicates) { Objects.requireNonNull(requestOptions); @@ -505,6 +549,8 @@ public Future> get(final RequestOptions requestOptions, fin context.runOnContext(go -> { final HttpRequest req = client.request(HttpMethod.GET, requestOptions); addResponsePredicates(req, successPredicates); + Optional.ofNullable(queryParams) + .ifPresent(params -> req.queryParams().addAll(queryParams)); req.send(result); }); diff --git a/tests/src/test/java/org/eclipse/hono/tests/DeviceRegistryHttpClient.java b/tests/src/test/java/org/eclipse/hono/tests/DeviceRegistryHttpClient.java index f298f3422b..ffb8a66386 100644 --- a/tests/src/test/java/org/eclipse/hono/tests/DeviceRegistryHttpClient.java +++ b/tests/src/test/java/org/eclipse/hono/tests/DeviceRegistryHttpClient.java @@ -87,6 +87,12 @@ public final class DeviceRegistryHttpClient { public static final String TEMPLATE_URI_CREDENTIALS_INSTANCE = String.format("/%s/%s/%%s/%%s/%%s", RegistryManagementConstants.API_VERSION, RegistryManagementConstants.CREDENTIALS_HTTP_ENDPOINT); + /** + * The URI pattern for searching devices. + */ + public static final String TEMPLATE_URI_SEARCH_DEVICES_INSTANCE = String.format("/%s/%s/%%s", + RegistryManagementConstants.API_VERSION, RegistryManagementConstants.DEVICES_HTTP_ENDPOINT); + private static final Logger LOG = LoggerFactory.getLogger(DeviceRegistryHttpClient.class); private static final String CONTENT_TYPE_APPLICATION_JSON = "application/json"; @@ -156,6 +162,10 @@ private static String registrationWithoutIdUri(final String tenant) { .orElse("")); } + private static String searchDevicesUri(final String tenant) { + return String.format(TEMPLATE_URI_SEARCH_DEVICES_INSTANCE, tenant); + } + // tenant management /** @@ -642,6 +652,46 @@ public Future> deregisterDevice( } + /** + * Finds devices belonging to the given tenant with optional filters, paging and sorting options. + * + * @param tenantId The tenant that the device belongs to. + * @param pageSize The maximum number of results to include in a response. + * @param pageOffset The offset into the result set from which to include objects in the response. + * @param filters The filters are predicates that objects in the result set must match. + * @param sortOptions A list of sort options. + * @param expectedStatusCode The status code indicating a successful outcome. + * @return A future indicating the outcome of the operation. The future will contain the response if the + * response contained the expected status code. Otherwise the future will fail. + * @throws NullPointerException if any of the parameters is {@code null}. + */ + public Future> searchDevices( + final String tenantId, + final Optional pageSize, + final Optional pageOffset, + final List filters, + final List sortOptions, + final int expectedStatusCode) { + + Objects.requireNonNull(tenantId); + Objects.requireNonNull(pageSize); + Objects.requireNonNull(pageOffset); + Objects.requireNonNull(filters); + Objects.requireNonNull(sortOptions); + + final String requestUri = searchDevicesUri(tenantId); + final MultiMap queryParams = MultiMap.caseInsensitiveMultiMap(); + + pageSize.ifPresent( + pSize -> queryParams.add(RegistryManagementConstants.PARAM_PAGE_SIZE, String.valueOf(pSize))); + pageOffset.ifPresent( + pOffset -> queryParams.add(RegistryManagementConstants.PARAM_PAGE_OFFSET, String.valueOf(pOffset))); + filters.forEach(filterJson -> queryParams.add(RegistryManagementConstants.PARAM_FILTER_JSON, filterJson)); + sortOptions.forEach(sortJson -> queryParams.add(RegistryManagementConstants.PARAM_SORT_JSON, sortJson)); + + return httpClient.get(requestUri, queryParams, ResponsePredicate.status(expectedStatusCode)); + } + // credentials management /** diff --git a/tests/src/test/java/org/eclipse/hono/tests/registry/DeviceManagementIT.java b/tests/src/test/java/org/eclipse/hono/tests/registry/DeviceManagementIT.java index 7913c56a56..dbed453b47 100644 --- a/tests/src/test/java/org/eclipse/hono/tests/registry/DeviceManagementIT.java +++ b/tests/src/test/java/org/eclipse/hono/tests/registry/DeviceManagementIT.java @@ -17,12 +17,16 @@ import java.net.HttpURLConnection; import java.time.Instant; import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.hono.service.management.device.Device; +import org.eclipse.hono.service.management.device.SearchDevicesResult; import org.eclipse.hono.tests.CrudHttpClient; import org.eclipse.hono.tests.DeviceRegistryHttpClient; import org.eclipse.hono.tests.IntegrationTestSupport; @@ -31,12 +35,15 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; import org.junit.jupiter.api.extension.ExtendWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.vertx.core.CompositeFuture; import io.vertx.core.MultiMap; import io.vertx.core.Vertx; import io.vertx.core.http.HttpHeaders; @@ -420,6 +427,231 @@ public void testDeregisterDeviceFailsForNonExistingDevice(final VertxTestContext .onComplete(ctx.completing()); } + /** + * Tests verifying the search devices operation. + * + * @see + * Device Registry Management API - Search Devices + */ + @Nested + @EnabledIfSystemProperty(named = "hono.deviceregistry.supportsSearchDevices", matches = "true") + class SearchDevicesIT { + /** + * Verifies that a request to search devices fails with a {@value HttpURLConnection#HTTP_NOT_FOUND} + * when no matching devices are found. + * + * @param ctx The vert.x test context. + */ + @Test + public void testSearchDevicesFailsWhenNoDevicesAreFound(final VertxTestContext ctx) { + final Device device = new Device().setEnabled(false); + final String filterJson = getFilterJson("/enabled", true, "eq"); + + registry.registerDevice(tenantId, deviceId, device) + .compose(ok -> registry.searchDevices(tenantId, Optional.empty(), Optional.empty(), + List.of(filterJson), List.of(), HttpURLConnection.HTTP_NOT_FOUND)) + .onComplete(ctx.completing()); + } + + /** + * Verifies that a request to search devices fails when page size is invalid. + * + * @param ctx The vert.x test context. + */ + @Test + public void testSearchDevicesWithInvalidPageSizeFails(final VertxTestContext ctx) { + final int invalidPageSize = -100; + + registry.registerDevice(tenantId, deviceId) + .compose(ok -> registry.searchDevices(tenantId, Optional.of(invalidPageSize), Optional.empty(), + List.of(), List.of(), HttpURLConnection.HTTP_BAD_REQUEST)) + .onComplete(ctx.completing()); + } + + /** + * Verifies that a request to search devices with pageSize succeeds and the result is in accordance + * with the specified page size. + * + * @param ctx The vert.x test context. + */ + @Test + public void testSearchDevicesWithValidPageSizeSucceeds(final VertxTestContext ctx) { + final int pageSize = 1; + + CompositeFuture.all(registry.registerDevice(tenantId, new Device()), registry + .registerDevice(tenantId, new Device()) + .compose(ok -> registry.searchDevices(tenantId, Optional.of(pageSize), Optional.empty(), List.of(), + List.of(), HttpURLConnection.HTTP_OK)) + .onComplete(ctx.succeeding(httpResponse -> { + ctx.verify(() -> { + final SearchDevicesResult searchDevicesResult = httpResponse + .bodyAsJson(SearchDevicesResult.class); + assertThat(searchDevicesResult.getTotal()).isEqualTo(2); + assertThat(searchDevicesResult.getResult()).hasSize(1); + }); + ctx.completeNow(); + }))); + } + + /** + * Verifies that a request to search devices fails when page offset is invalid. + * + * @param ctx The vert.x test context. + */ + @Test + public void testSearchDevicesWithInvalidPageOffsetFails(final VertxTestContext ctx) { + final int invalidPageOffset = -100; + + registry.registerDevice(tenantId, deviceId) + .compose(ok -> registry.searchDevices(tenantId, Optional.empty(), Optional.of(invalidPageOffset), + List.of(), List.of(), HttpURLConnection.HTTP_BAD_REQUEST)) + .onComplete(ctx.completing()); + } + + /** + * Verifies that a request to search devices with page offset succeeds and the result is in accordance with + * the specified page offset. + * + * @param ctx The vert.x test context. + */ + @Test + public void testSearchDevicesWithValidPageOffsetSucceeds(final VertxTestContext ctx) { + final String deviceId1 = helper.getRandomDeviceId(tenantId); + final String deviceId2 = helper.getRandomDeviceId(tenantId); + final Device device1 = new Device().setExtensions(Map.of("id", "aaa")); + final Device device2 = new Device().setExtensions(Map.of("id", "bbb")); + final int pageSize = 1; + final int pageOffset = 1; + final String sortJson = getSortJson("/ext/id", "desc"); + + CompositeFuture.all(registry.registerDevice(tenantId, deviceId1, device1), + registry.registerDevice(tenantId, deviceId2, device2)) + .compose(ok -> registry.searchDevices(tenantId, Optional.of(pageSize), Optional.of(pageOffset), + List.of(), List.of(sortJson), HttpURLConnection.HTTP_OK)) + .onComplete(ctx.succeeding(httpResponse -> { + ctx.verify(() -> { + final SearchDevicesResult searchDevicesResult = httpResponse + .bodyAsJson(SearchDevicesResult.class); + assertThat(searchDevicesResult.getTotal()).isEqualTo(2); + assertThat(searchDevicesResult.getResult()).hasSize(1); + assertThat(searchDevicesResult.getResult().get(0).getId()).isEqualTo(deviceId1); + }); + ctx.completeNow(); + })); + } + + /** + * Verifies that a request to search devices fails when filterJson is invalid. + * + * @param ctx The vert.x test context. + */ + @Test + public void testSearchDevicesWithInvalidFilterJsonFails(final VertxTestContext ctx) { + + registry.registerDevice(tenantId, deviceId) + .compose(ok -> registry.searchDevices(tenantId, Optional.empty(), Optional.empty(), + List.of("Invalid filterJson"), List.of(), HttpURLConnection.HTTP_BAD_REQUEST)) + .onComplete(ctx.completing()); + } + + /** + * Verifies that a request to search devices with multiple filters succeeds and matching devices are found. + * + * @param ctx The vert.x test context. + */ + @Test + public void testSearchDevicesWithValidMultipleFiltersSucceeds(final VertxTestContext ctx) { + final String deviceId1 = helper.getRandomDeviceId(tenantId); + final String deviceId2 = helper.getRandomDeviceId(tenantId); + final Device device1 = new Device().setEnabled(false).setExtensions(Map.of("id", "1")); + final Device device2 = new Device().setEnabled(true).setExtensions(Map.of("id", "2")); + final String filterJson1 = getFilterJson("/ext/id", "1", "eq"); + final String filterJson2 = getFilterJson("/enabled", true, "eq"); + final String filterJson3 = getFilterJson("/enabled", false, "eq"); + + CompositeFuture + .all(registry.registerDevice(tenantId, deviceId1, device1), + registry.registerDevice(tenantId, deviceId2, device2)) + .compose(ok -> registry.searchDevices(tenantId, Optional.empty(), Optional.empty(), + List.of(filterJson1, filterJson2), List.of(), HttpURLConnection.HTTP_NOT_FOUND)) + .compose(ok -> registry.searchDevices(tenantId, Optional.empty(), Optional.empty(), + List.of(filterJson1, filterJson3), List.of(), HttpURLConnection.HTTP_OK)) + .onComplete(ctx.succeeding(httpResponse -> { + ctx.verify(() -> { + final SearchDevicesResult searchDevicesResult = httpResponse + .bodyAsJson(SearchDevicesResult.class); + assertThat(searchDevicesResult.getTotal()).isEqualTo(1); + assertThat(searchDevicesResult.getResult()).hasSize(1); + assertThat(searchDevicesResult.getResult().get(0).getId()).isEqualTo(deviceId1); + }); + ctx.completeNow(); + })); + } + + /** + * Verifies that a request to search devices fails when sortJson is invalid. + * + * @param ctx The vert.x test context. + */ + @Test + public void testSearchDevicesWithInvalidSortJsonFails(final VertxTestContext ctx) { + + registry.registerDevice(tenantId, deviceId) + .compose(ok -> registry.searchDevices(tenantId, Optional.empty(), Optional.empty(), List.of(), + List.of("Invalid sortJson"), HttpURLConnection.HTTP_BAD_REQUEST)) + .onComplete(ctx.completing()); + } + + /** + * Verifies that a request to search devices with a valid sort option succeeds and the result is sorted + * accordingly. + * + * @param ctx The vert.x test context. + */ + @Test + public void testSearchDevicesWithValidSortOptionSucceeds(final VertxTestContext ctx) { + final String deviceId1 = helper.getRandomDeviceId(tenantId); + final String deviceId2 = helper.getRandomDeviceId(tenantId); + final Device device1 = new Device().setExtensions(Map.of("id", "aaa")); + final Device device2 = new Device().setExtensions(Map.of("id", "bbb")); + final String sortJson = getSortJson("/ext/id", "desc"); + + CompositeFuture.all(registry.registerDevice(tenantId, deviceId1, device1), + registry.registerDevice(tenantId, deviceId2, device2)) + .compose(ok -> registry.searchDevices(tenantId, Optional.empty(), Optional.empty(), List.of(), + List.of(sortJson), HttpURLConnection.HTTP_OK)) + .onComplete(ctx.succeeding(httpResponse -> { + ctx.verify(() -> { + final SearchDevicesResult searchDevicesResult = httpResponse + .bodyAsJson(SearchDevicesResult.class); + assertThat(searchDevicesResult.getTotal()).isEqualTo(2); + assertThat(searchDevicesResult.getResult()).hasSize(2); + assertThat(searchDevicesResult.getResult().get(0).getId()).isEqualTo(deviceId2); + assertThat(searchDevicesResult.getResult().get(1).getId()).isEqualTo(deviceId1); + }); + ctx.completeNow(); + })); + } + + private String getFilterJson(final String field, final T value, final String operator) { + final JsonObject filterJson = new JsonObject() + .put(RegistryManagementConstants.FIELD_FILTER_FIELD, field) + .put(RegistryManagementConstants.FIELD_FILTER_VALUE, value); + Optional.ofNullable(operator) + .ifPresent(op -> filterJson.put(RegistryManagementConstants.FIELD_FILTER_OPERATOR, op)); + + return filterJson.toString(); + } + + private String getSortJson(final String field, final String direction) { + final JsonObject sortJson = new JsonObject().put(RegistryManagementConstants.FIELD_FILTER_FIELD, field); + Optional.ofNullable(direction) + .ifPresent(dir -> sortJson.put(RegistryManagementConstants.FIELD_SORT_DIRECTION, dir)); + + return sortJson.toString(); + } + } + private static String assertLocationHeader(final MultiMap responseHeaders, final String tenantId) { final String location = responseHeaders.get(HttpHeaders.LOCATION); assertThat(location).isNotNull();