diff --git a/services/device-registry-base/src/test/java/org/eclipse/hono/service/management/device/AbstractDeviceManagementSearchDevicesTest.java b/services/device-registry-base/src/test/java/org/eclipse/hono/service/management/device/AbstractDeviceManagementSearchDevicesTest.java index 17b57afca1..aab5a6ee72 100644 --- a/services/device-registry-base/src/test/java/org/eclipse/hono/service/management/device/AbstractDeviceManagementSearchDevicesTest.java +++ b/services/device-registry-base/src/test/java/org/eclipse/hono/service/management/device/AbstractDeviceManagementSearchDevicesTest.java @@ -178,27 +178,56 @@ default void testSearchDevicesWithPageSize(final VertxTestContext ctx) { @Test default void testSearchDevicesWithPageOffset(final VertxTestContext ctx) { final String tenantId = DeviceRegistryUtils.getUniqueIdentifier(); - final int pageSize = 1; - final int pageOffset = 1; + final int pageSize = 6; final Filter filter = new Filter("/enabled", true); - final Sort sortOption = new Sort("/id"); - sortOption.setDirection(Sort.Direction.DESC); createDevices(tenantId, Map.of( + "testDevice0", new Device().setEnabled(true), "testDevice1", new Device().setEnabled(true), - "testDevice2", new Device().setEnabled(true))) - .compose(ok -> getDeviceManagementService() - .searchDevices(tenantId, pageSize, pageOffset, List.of(filter), - List.of(sortOption), - NoopSpan.INSTANCE)) + "testDevice2", new Device().setEnabled(true), + "testDevice3", new Device().setEnabled(true), + "testDevice4", new Device().setEnabled(true), + "testDevice5", new Device().setEnabled(true), + "testDevice6", new Device().setEnabled(true), + "testDevice7", new Device().setEnabled(true))) + .compose(ok -> getDeviceManagementService().searchDevices( + tenantId, + pageSize, + 0, + List.of(filter), + List.of(), + NoopSpan.INSTANCE)) + .compose(response -> { + ctx.verify(() -> { + assertThat(response.getStatus()).isEqualTo(HttpURLConnection.HTTP_OK); + + final var searchResult = response.getPayload(); + assertThat(searchResult.getTotal()).isEqualTo(8); + assertThat(searchResult.getResult()).hasSize(6); + assertThat(searchResult.getResult().get(0).getId()).isEqualTo("testDevice0"); + assertThat(searchResult.getResult().get(1).getId()).isEqualTo("testDevice1"); + assertThat(searchResult.getResult().get(2).getId()).isEqualTo("testDevice2"); + assertThat(searchResult.getResult().get(3).getId()).isEqualTo("testDevice3"); + assertThat(searchResult.getResult().get(4).getId()).isEqualTo("testDevice4"); + assertThat(searchResult.getResult().get(5).getId()).isEqualTo("testDevice5"); + }); + return getDeviceManagementService().searchDevices( + tenantId, + pageSize, + 1, + List.of(filter), + List.of(), + NoopSpan.INSTANCE); + }) .onComplete(ctx.succeeding(s -> { ctx.verify(() -> { assertThat(s.getStatus()).isEqualTo(HttpURLConnection.HTTP_OK); - final SearchResult searchResult = s.getPayload(); - assertThat(searchResult.getTotal()).isEqualTo(2); - assertThat(searchResult.getResult()).hasSize(1); - assertThat(searchResult.getResult().get(0).getId()).isEqualTo("testDevice1"); + final var searchResult = s.getPayload(); + assertThat(searchResult.getTotal()).isEqualTo(8); + assertThat(searchResult.getResult()).hasSize(2); + assertThat(searchResult.getResult().get(0).getId()).isEqualTo("testDevice6"); + assertThat(searchResult.getResult().get(1).getId()).isEqualTo("testDevice7"); }); ctx.completeNow(); })); @@ -238,6 +267,65 @@ default void testSearchDevicesWithSortOption(final VertxTestContext ctx) { })); } + /** + * Verifies that a request to search devices without a sort option succeeds and the result set is sorted + * by device ID. + * + * @param ctx The vert.x test context. + */ + @Test + default void testSearchDevicesSortsResultById(final VertxTestContext ctx) { + final String tenantId = DeviceRegistryUtils.getUniqueIdentifier(); + final int pageSize = 4; + + createDevices(tenantId, Map.of( + "testDevice1", new Device(), + "testDevice5", new Device(), + "testDevice3", new Device(), + "testDevice4", new Device(), + "testDevice2", new Device(), + "testDevice6", new Device())) + .compose(ok -> getDeviceManagementService().searchDevices( + tenantId, + pageSize, + 0, + List.of(), + List.of(), + NoopSpan.INSTANCE)) + .compose(response -> { + ctx.verify(() -> { + assertThat(response.getStatus()).isEqualTo(HttpURLConnection.HTTP_OK); + + final var searchResult = response.getPayload(); + assertThat(searchResult.getTotal()).isEqualTo(6); + assertThat(searchResult.getResult()).hasSize(4); + assertThat(searchResult.getResult().get(0).getId()).isEqualTo("testDevice1"); + assertThat(searchResult.getResult().get(1).getId()).isEqualTo("testDevice2"); + assertThat(searchResult.getResult().get(2).getId()).isEqualTo("testDevice3"); + assertThat(searchResult.getResult().get(3).getId()).isEqualTo("testDevice4"); + }); + return getDeviceManagementService().searchDevices( + tenantId, + pageSize, + 1, + List.of(), + List.of(), + NoopSpan.INSTANCE); + }) + .onComplete(ctx.succeeding(s -> { + ctx.verify(() -> { + assertThat(s.getStatus()).isEqualTo(HttpURLConnection.HTTP_OK); + + final var searchResult = s.getPayload(); + assertThat(searchResult.getTotal()).isEqualTo(6); + assertThat(searchResult.getResult()).hasSize(2); + assertThat(searchResult.getResult().get(0).getId()).isEqualTo("testDevice5"); + assertThat(searchResult.getResult().get(1).getId()).isEqualTo("testDevice6"); + }); + ctx.completeNow(); + })); + } + /** * Verifies that a request to search devices with filters containing the wildcard character '*' * succeeds and matching devices are found. diff --git a/services/device-registry-base/src/test/java/org/eclipse/hono/service/management/tenant/AbstractTenantManagementSearchTenantsTest.java b/services/device-registry-base/src/test/java/org/eclipse/hono/service/management/tenant/AbstractTenantManagementSearchTenantsTest.java index ecc2324236..d4a9f3a470 100644 --- a/services/device-registry-base/src/test/java/org/eclipse/hono/service/management/tenant/AbstractTenantManagementSearchTenantsTest.java +++ b/services/device-registry-base/src/test/java/org/eclipse/hono/service/management/tenant/AbstractTenantManagementSearchTenantsTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2020 Contributors to the Eclipse Foundation + * Copyright (c) 2020, 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -15,6 +15,8 @@ import static com.google.common.truth.Truth.assertThat; import java.net.HttpURLConnection; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; @@ -227,6 +229,64 @@ tenantId2, new Tenant().setEnabled(true).setExtensions(Map.of("id", "2")))) })); } + /** + * Verifies that a request to search tenants without a sort option succeeds and the result set is sorted + * by tenant ID. + * + * @param ctx The vert.x test context. + */ + @Test + default void testSearchTenantsSortsResultById(final VertxTestContext ctx) { + final var tenants = Map.of( + DeviceRegistryUtils.getUniqueIdentifier(), new Tenant(), + DeviceRegistryUtils.getUniqueIdentifier(), new Tenant(), + DeviceRegistryUtils.getUniqueIdentifier(), new Tenant(), + DeviceRegistryUtils.getUniqueIdentifier(), new Tenant(), + DeviceRegistryUtils.getUniqueIdentifier(), new Tenant(), + DeviceRegistryUtils.getUniqueIdentifier(), new Tenant()); + final var tenantIds = new ArrayList(tenants.keySet()); + Collections.sort(tenantIds); + final int pageSize = 4; + + createTenants(tenants) + .onFailure(ctx::failNow) + .compose(ok -> getTenantManagementService().searchTenants( + pageSize, + 0, + List.of(), + List.of(), + NoopSpan.INSTANCE)) + .compose(response -> { + ctx.verify(() -> { + assertThat(response.getStatus()).isEqualTo(HttpURLConnection.HTTP_OK); + + final var payload = response.getPayload(); + assertThat(payload.getTotal()).isEqualTo(6); + assertThat(payload.getResult()).hasSize(4); + final var foundTenantIds = payload.getResult().stream().map(TenantWithId::getId).collect(Collectors.toList()); + assertThat(foundTenantIds).containsExactlyElementsIn(tenantIds.subList(0, 4)); + }); + return getTenantManagementService().searchTenants( + pageSize, + 1, + List.of(), + List.of(), + NoopSpan.INSTANCE); + }) + .onComplete(ctx.succeeding(response -> { + ctx.verify(() -> { + assertThat(response.getStatus()).isEqualTo(HttpURLConnection.HTTP_OK); + + final var payload = response.getPayload(); + assertThat(payload.getTotal()).isEqualTo(6); + assertThat(payload.getResult()).hasSize(2); + final var foundTenantIds = payload.getResult().stream().map(TenantWithId::getId).collect(Collectors.toList()); + assertThat(foundTenantIds).containsExactlyElementsIn(tenantIds.subList(4, 6)); + }); + ctx.completeNow(); + })); + } + /** * Verifies that a request to search tenants with a sort option succeeds and the result is in accordance with the * specified sort option. diff --git a/services/device-registry-mongodb/src/main/java/org/eclipse/hono/deviceregistry/mongodb/utils/MongoDbDocumentBuilder.java b/services/device-registry-mongodb/src/main/java/org/eclipse/hono/deviceregistry/mongodb/utils/MongoDbDocumentBuilder.java index 03b147a74d..ea16092a20 100644 --- a/services/device-registry-mongodb/src/main/java/org/eclipse/hono/deviceregistry/mongodb/utils/MongoDbDocumentBuilder.java +++ b/services/device-registry-mongodb/src/main/java/org/eclipse/hono/deviceregistry/mongodb/utils/MongoDbDocumentBuilder.java @@ -288,8 +288,14 @@ private void applySearchFilters(final List filters, final Function sortOptions, final Function fieldMapper) { - sortOptions.forEach(sortOption -> document.put(fieldMapper.apply(sortOption.getField()), - mapSortingDirection(sortOption.getDirection()))); + if (sortOptions.isEmpty()) { + // if no fields to sort by have been specified, sort by "/id" + document.put(fieldMapper.apply(FIELD_ID), mapSortingDirection(Sort.Direction.ASC)); + } else { + sortOptions.forEach(sortOption -> document.put( + fieldMapper.apply(sortOption.getField()), + mapSortingDirection(sortOption.getDirection()))); + } } private MongoDbDocumentBuilder withCredentialsPredicate(final String field, final String value) { diff --git a/tests/src/test/java/org/eclipse/hono/tests/registry/TenantManagementIT.java b/tests/src/test/java/org/eclipse/hono/tests/registry/TenantManagementIT.java index 14b2308956..394208f87f 100644 --- a/tests/src/test/java/org/eclipse/hono/tests/registry/TenantManagementIT.java +++ b/tests/src/test/java/org/eclipse/hono/tests/registry/TenantManagementIT.java @@ -23,7 +23,9 @@ import java.security.PublicKey; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; @@ -31,6 +33,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.eclipse.hono.service.management.SearchResult; import org.eclipse.hono.service.management.device.Device; @@ -56,6 +59,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import io.vertx.core.CompositeFuture; +import io.vertx.core.Future; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpHeaders; import io.vertx.core.json.JsonArray; @@ -890,29 +894,54 @@ public void testSearchTenantsWithInvalidPageOffsetFails(final VertxTestContext c */ @Test public void testSearchTenantsWithValidPageOffsetSucceeds(final VertxTestContext ctx) { - final String tenantId1 = getHelper().getRandomTenantId(); - final String tenantId2 = getHelper().getRandomTenantId(); - final Tenant tenant1 = new Tenant().setExtensions(Map.of("id", "aaa")); - final Tenant tenant2 = new Tenant().setExtensions(Map.of("id", "bbb")); - final int pageSize = 1; - final int pageOffset = 1; - final String sortJson = getSortJson("/ext/id", "desc"); - CompositeFuture.all( - getHelper().registry.addTenant(tenantId1, tenant1), - getHelper().registry.addTenant(tenantId2, tenant2)) - .compose(ok -> getHelper().registry.searchTenants(Optional.of(pageSize), Optional.of(pageOffset), - List.of(), List.of(sortJson), HttpURLConnection.HTTP_OK)) - .onComplete(ctx.succeeding(httpResponse -> { - ctx.verify(() -> { - final SearchResult searchResult = JacksonCodec - .decodeValue(httpResponse.body(), new TypeReference<>() { }); - assertThat(searchResult.getTotal()).isEqualTo(2); - assertThat(searchResult.getResult()).hasSize(1); - assertThat(searchResult.getResult().get(0).getId()).isEqualTo(tenantId1); - }); - ctx.completeNow(); - })); + final var tenants = Map.of( + getHelper().getRandomTenantId(), new Tenant(), + getHelper().getRandomTenantId(), new Tenant(), + getHelper().getRandomTenantId(), new Tenant(), + getHelper().getRandomTenantId(), new Tenant(), + getHelper().getRandomTenantId(), new Tenant(), + getHelper().getRandomTenantId(), new Tenant()); + final var tenantIds = new ArrayList(tenants.keySet()); + Collections.sort(tenantIds); + final int pageSize = 4; + + createTenants(tenants) + .compose(ok -> getHelper().registry.searchTenants( + Optional.of(pageSize), + Optional.empty(), + List.of(), + List.of(), + HttpURLConnection.HTTP_OK)) + .compose(response -> { + ctx.verify(() -> { + final SearchResult searchResult = JacksonCodec + .decodeValue(response.body(), new TypeReference<>() { }); + + assertThat(searchResult.getTotal()).isEqualTo(6); + assertThat(searchResult.getResult()).hasSize(4); + final var foundTenantIds = searchResult.getResult().stream().map(TenantWithId::getId).collect(Collectors.toList()); + assertThat(foundTenantIds).containsExactlyElementsIn(tenantIds.subList(0, 4)); + }); + return getHelper().registry.searchTenants( + Optional.of(pageSize), + Optional.of(1), + List.of(), + List.of(), + HttpURLConnection.HTTP_OK); + }) + .onComplete(ctx.succeeding(response -> { + ctx.verify(() -> { + final SearchResult searchResult = JacksonCodec + .decodeValue(response.body(), new TypeReference<>() { }); + + assertThat(searchResult.getTotal()).isEqualTo(6); + assertThat(searchResult.getResult()).hasSize(2); + final var foundTenantIds = searchResult.getResult().stream().map(TenantWithId::getId).collect(Collectors.toList()); + assertThat(foundTenantIds).containsExactlyElementsIn(tenantIds.subList(4, 6)); + }); + ctx.completeNow(); + })); } /** @@ -1111,6 +1140,25 @@ public void testSearchTenantsWithValidSortOptionSucceeds(final VertxTestContext })); } + /** + * Creates a set of tenants. + * + * @param tenantsToCreate The tenants to be created. The keys are the tenant identifiers. + * @return A succeeded future if all the tenants have been created successfully. + */ + private Future createTenants(final Map tenantsToCreate) { + + @SuppressWarnings("rawtypes") + final List creationResult = tenantsToCreate.entrySet().stream() + .map(entry -> getHelper().registry.addTenant(entry.getKey(), entry.getValue()) + .map(response -> { + assertThat(response.statusCode()).isEqualTo(HttpURLConnection.HTTP_CREATED); + return null; + })) + .collect(Collectors.toList()); + return CompositeFuture.all(creationResult).mapEmpty(); + } + private String getFilterJson(final String field, final T value, final String operator) { final JsonObject filterJson = new JsonObject() .put(RegistryManagementConstants.FIELD_FILTER_FIELD, field)