diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java index f15ab09ab2..6c1a63194c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java @@ -23,6 +23,7 @@ import org.springframework.test.context.TestPropertySource; import org.thingsboard.server.common.data.edqs.EdqsState; import org.thingsboard.server.common.data.edqs.EdqsState.EdqsApiMode; import org.thingsboard.server.common.data.edqs.ToCoreEdqsRequest; +import org.awaitility.core.ThrowingRunnable; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; @@ -86,6 +87,11 @@ public class EdqsEntityQueryControllerTest extends EntityQueryControllerTest { result -> result == expectedResult); } + @Override + protected void verifyAvailableKeysByQueryV2(ThrowingRunnable assertion) { + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(assertion); + } + @Test public void testEdqsState() throws Exception { loginSysAdmin(); diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index 1c8060989b..835f00985b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.node.BooleanNode; import com.fasterxml.jackson.databind.node.DoubleNode; import com.fasterxml.jackson.databind.node.IntNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import org.awaitility.core.ThrowingRunnable; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -1467,6 +1468,10 @@ public class EntityQueryControllerTest extends AbstractControllerTest { return result; } + protected void verifyAvailableKeysByQueryV2(ThrowingRunnable assertion) throws Throwable { + assertion.run(); + } + private KeyFilter getEntityFieldEqualFilter(String keyName, String value) { return getEntityFieldKeyFilter(keyName, value, StringFilterPredicate.StringOperation.EQUAL); } @@ -1510,7 +1515,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { // --- findAvailableEntityKeysV2 tests --- @Test - public void testFindAvailableKeysByQueryV2() throws Exception { + public void testFindAvailableKeysByQueryV2() throws Throwable { // GIVEN — two devices matched by query; a third device should not be matched var device1 = createDevice("Test device 1"); var device2 = createDevice("Test device 2"); @@ -1550,100 +1555,106 @@ public class EntityQueryControllerTest extends AbstractControllerTest { EntityDataPageLink pageLink = new EntityDataPageLink(100, 0, null, null); EntityDataQuery query = new EntityDataQuery(filter, pageLink, List.of(), null, null); - AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2(query, - true, true, List.of(AttributeScope.SHARED_SCOPE, AttributeScope.CLIENT_SCOPE), true); - // THEN - assertThat(result.entityTypes()).containsExactly(EntityType.DEVICE); - - // timeseries: keys collected from both devices, samples contain the freshest data points - assertThat(result.timeseries()).extracting(KeyInfo::key) - .containsExactly("timeseries1", "timeseries2", "timeseries3"); - assertThat(result.timeseries()).allSatisfy(ki -> assertThat(ki.sample()).isNotNull()); - assertKeySample(result.timeseries(), "timeseries1", new DoubleNode(20.5), 2000); // from device1 - assertKeySample(result.timeseries(), "timeseries2", new IntNode(300), 3000); // from device2 (newer) - assertKeySample(result.timeseries(), "timeseries3", new DoubleNode(99.9), 5000); // only on device2 - - // SERVER_SCOPE must be fully omitted from the response - assertThat(result.attributes()).containsOnlyKeys(AttributeScope.SHARED_SCOPE, AttributeScope.CLIENT_SCOPE); - - // SHARED_SCOPE: from device1 (alphabetical order) - assertThat(result.attributes().get(AttributeScope.SHARED_SCOPE)) - .extracting(KeyInfo::key).containsExactly("sharedAttribute1", "sharedAttribute2"); - assertKeySample(result.attributes().get(AttributeScope.SHARED_SCOPE), "sharedAttribute1", BooleanNode.TRUE); - assertKeySample(result.attributes().get(AttributeScope.SHARED_SCOPE), "sharedAttribute2", new DoubleNode(3.14)); - - // CLIENT_SCOPE: from device2 (alphabetical order) - assertThat(result.attributes().get(AttributeScope.CLIENT_SCOPE)) - .extracting(KeyInfo::key).containsExactly("clientAttribute1", "clientAttribute2"); - assertKeySample(result.attributes().get(AttributeScope.CLIENT_SCOPE), "clientAttribute1", JacksonUtil.toJsonNode("{\"key\":\"val\"}")); - assertKeySample(result.attributes().get(AttributeScope.CLIENT_SCOPE), "clientAttribute2", BooleanNode.FALSE); + verifyAvailableKeysByQueryV2(() -> { + AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2(query, + true, true, List.of(AttributeScope.SHARED_SCOPE, AttributeScope.CLIENT_SCOPE), true); + + assertThat(result.entityTypes()).containsExactly(EntityType.DEVICE); + + // timeseries: keys collected from both devices, samples contain the freshest data points + assertThat(result.timeseries()).extracting(KeyInfo::key) + .containsExactly("timeseries1", "timeseries2", "timeseries3"); + assertThat(result.timeseries()).allSatisfy(ki -> assertThat(ki.sample()).isNotNull()); + assertKeySample(result.timeseries(), "timeseries1", new DoubleNode(20.5), 2000); // from device1 + assertKeySample(result.timeseries(), "timeseries2", new IntNode(300), 3000); // from device2 (newer) + assertKeySample(result.timeseries(), "timeseries3", new DoubleNode(99.9), 5000); // only on device2 + + // SERVER_SCOPE must be fully omitted from the response + assertThat(result.attributes()).containsOnlyKeys(AttributeScope.SHARED_SCOPE, AttributeScope.CLIENT_SCOPE); + + // SHARED_SCOPE: from device1 (alphabetical order) + assertThat(result.attributes().get(AttributeScope.SHARED_SCOPE)) + .extracting(KeyInfo::key).containsExactly("sharedAttribute1", "sharedAttribute2"); + assertKeySample(result.attributes().get(AttributeScope.SHARED_SCOPE), "sharedAttribute1", BooleanNode.TRUE); + assertKeySample(result.attributes().get(AttributeScope.SHARED_SCOPE), "sharedAttribute2", new DoubleNode(3.14)); + + // CLIENT_SCOPE: from device2 (alphabetical order) + assertThat(result.attributes().get(AttributeScope.CLIENT_SCOPE)) + .extracting(KeyInfo::key).containsExactly("clientAttribute1", "clientAttribute2"); + assertKeySample(result.attributes().get(AttributeScope.CLIENT_SCOPE), "clientAttribute1", JacksonUtil.toJsonNode("{\"key\":\"val\"}")); + assertKeySample(result.attributes().get(AttributeScope.CLIENT_SCOPE), "clientAttribute2", BooleanNode.FALSE); + }); } @Test - public void testFindAvailableKeysByQueryV2_withoutSamples() throws Exception { + public void testFindAvailableKeysByQueryV2_withoutSamples() throws Throwable { // GIVEN var device = createDevice("Test device"); postTelemetry(device.getId(), new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry("temperature", 10.0))); postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, new StringDataEntry("firmware", "v1.0")); - // WHEN - AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2( - buildDeviceQuery("Test device"), true, true, null, false); - // THEN - assertThat(result.timeseries()).allSatisfy(ki -> assertThat(ki.sample()).isNull()); - assertThat(result.attributes().get(AttributeScope.SERVER_SCOPE)) - .allSatisfy(ki -> assertThat(ki.sample()).isNull()); + verifyAvailableKeysByQueryV2(() -> { + AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2( + buildDeviceQuery("Test device"), true, true, null, false); + + assertThat(result.timeseries()).allSatisfy(ki -> assertThat(ki.sample()).isNull()); + assertThat(result.attributes().get(AttributeScope.SERVER_SCOPE)) + .allSatisfy(ki -> assertThat(ki.sample()).isNull()); + }); } @Test - public void testFindAvailableKeysByQueryV2_timeseriesOnly() throws Exception { + public void testFindAvailableKeysByQueryV2_timeseriesOnly() throws Throwable { // GIVEN var device = createDevice("Test device"); postTelemetry(device.getId(), new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry("temperature", 10.0))); postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, new StringDataEntry("firmware", "v1.0")); - // WHEN - AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2( - buildDeviceQuery("Test device"), true, false, null, false); - // THEN - assertThat(result.timeseries()).extracting(KeyInfo::key).contains("temperature"); - assertThat(result.attributes()).isNull(); + verifyAvailableKeysByQueryV2(() -> { + AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2( + buildDeviceQuery("Test device"), true, false, null, false); + + assertThat(result.timeseries()).extracting(KeyInfo::key).contains("temperature"); + assertThat(result.attributes()).isNull(); + }); } @Test - public void testFindAvailableKeysByQueryV2_attributesOnly() throws Exception { + public void testFindAvailableKeysByQueryV2_attributesOnly() throws Throwable { // GIVEN var device = createDevice("Test device"); postTelemetry(device.getId(), new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry("temperature", 10.0))); postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, new StringDataEntry("firmware", "v1.0")); - // WHEN - AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2( - buildDeviceQuery("Test device"), false, true, null, false); - // THEN - assertThat(result.timeseries()).isNull(); - assertThat(result.attributes().get(AttributeScope.SERVER_SCOPE)) - .extracting(KeyInfo::key).contains("firmware"); + verifyAvailableKeysByQueryV2(() -> { + AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2( + buildDeviceQuery("Test device"), false, true, null, false); + + assertThat(result.timeseries()).isNull(); + assertThat(result.attributes().get(AttributeScope.SERVER_SCOPE)) + .extracting(KeyInfo::key).contains("firmware"); + }); } @Test - public void testFindAvailableKeysByQueryV2_noMatchingEntities() throws Exception { - // WHEN - AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2( - buildDeviceQuery("NonExistentDevice_" + UUID.randomUUID()), true, true, null, true); - + public void testFindAvailableKeysByQueryV2_noMatchingEntities() throws Throwable { // THEN - assertThat(result.entityTypes()).isEmpty(); - assertThat(result.timeseries()).isEmpty(); - assertThat(result.attributes()).isEmpty(); + verifyAvailableKeysByQueryV2(() -> { + AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2( + buildDeviceQuery("NonExistentDevice_" + UUID.randomUUID()), true, true, null, true); + + assertThat(result.entityTypes()).isEmpty(); + assertThat(result.timeseries()).isEmpty(); + assertThat(result.attributes()).isEmpty(); + }); } @Test - public void testFindAvailableKeysByQueryV2_assetUsesServerScopeOnly() throws Exception { + public void testFindAvailableKeysByQueryV2_assetUsesServerScopeOnly() throws Throwable { // GIVEN var asset = new Asset(); asset.setName("Test asset"); @@ -1656,13 +1667,15 @@ public class EntityQueryControllerTest extends AbstractControllerTest { filter.setSingleEntity(AliasEntityId.fromEntityId(asset.getId())); var query = new EntityDataQuery(filter, new EntityDataPageLink(1, 0, null, null), Collections.emptyList(), null, null); - AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2(query, false, true, null, false); - // THEN - assertThat(result.entityTypes()).containsExactly(EntityType.ASSET); - assertThat(result.attributes()).containsOnlyKeys(AttributeScope.SERVER_SCOPE); - assertThat(result.attributes().get(AttributeScope.SERVER_SCOPE)) - .extracting(KeyInfo::key).containsExactly("location"); + verifyAvailableKeysByQueryV2(() -> { + AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2(query, false, true, null, false); + + assertThat(result.entityTypes()).containsExactly(EntityType.ASSET); + assertThat(result.attributes()).containsOnlyKeys(AttributeScope.SERVER_SCOPE); + assertThat(result.attributes().get(AttributeScope.SERVER_SCOPE)) + .extracting(KeyInfo::key).containsExactly("location"); + }); } @Test @@ -1674,9 +1687,9 @@ public class EntityQueryControllerTest extends AbstractControllerTest { query, 30_000L).andExpect(status().isBadRequest()); } - private AvailableEntityKeysV2 findAvailableEntityKeysByQueryV2(EntityDataQuery query, - boolean includeTimeseries, boolean includeAttributes, - List scopes, boolean includeSamples) throws Exception { + protected AvailableEntityKeysV2 findAvailableEntityKeysByQueryV2(EntityDataQuery query, + boolean includeTimeseries, boolean includeAttributes, + List scopes, boolean includeSamples) throws Exception { StringBuilder url = new StringBuilder("/api/v2/entitiesQuery/find/keys?") .append("includeTimeseries=").append(includeTimeseries) .append("&includeAttributes=").append(includeAttributes)