Added basic black-box tests for geofencing CF
This commit is contained in:
parent
1421f9cc9f
commit
d49d1e9a5a
@ -16,6 +16,7 @@
|
||||
package org.thingsboard.server.msa;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.common.mapper.TypeRef;
|
||||
@ -231,9 +232,9 @@ public class TestRestClient {
|
||||
.statusCode(anyOf(is(HTTP_OK), is(HTTP_NOT_FOUND)));
|
||||
}
|
||||
|
||||
public ValidatableResponse postTelemetryAttribute(EntityId entityId, String scope, JsonNode attribute) {
|
||||
public ValidatableResponse postTelemetryAttribute(EntityId entityId, AttributeScope scope, JsonNode attribute) {
|
||||
return given().spec(requestSpec).body(attribute)
|
||||
.post("/api/plugins/telemetry/{entityType}/{entityId}/attributes/{scope}", entityId.getEntityType(), entityId.getId(), scope)
|
||||
.post("/api/plugins/telemetry/{entityType}/{entityId}/attributes/{scope}", entityId.getEntityType(), entityId.getId(), scope.name())
|
||||
.then()
|
||||
.statusCode(HTTP_OK);
|
||||
}
|
||||
@ -256,13 +257,13 @@ public class TestRestClient {
|
||||
.as(JsonNode.class);
|
||||
}
|
||||
|
||||
public JsonNode getAttributes(EntityId entityId, AttributeScope scope, String keys) {
|
||||
public ArrayNode getAttributes(EntityId entityId, AttributeScope scope, String keys) {
|
||||
return given().spec(requestSpec)
|
||||
.get("/api/plugins/telemetry/{entityType}/{entityId}/values/attributes/{scope}?keys={keys}", entityId.getEntityType(), entityId.getId(), scope, keys)
|
||||
.then()
|
||||
.statusCode(HTTP_OK)
|
||||
.extract()
|
||||
.as(JsonNode.class);
|
||||
.as(ArrayNode.class);
|
||||
}
|
||||
|
||||
public JsonNode getLatestTelemetry(EntityId entityId) {
|
||||
@ -367,6 +368,16 @@ public class TestRestClient {
|
||||
});
|
||||
}
|
||||
|
||||
public EntityRelation postEntityRelation(EntityRelation entityRelation) {
|
||||
return given().spec(requestSpec)
|
||||
.body(entityRelation)
|
||||
.post("/api/v2/relation")
|
||||
.then()
|
||||
.statusCode(HTTP_OK)
|
||||
.extract()
|
||||
.as(EntityRelation.class);
|
||||
}
|
||||
|
||||
public JsonNode postServerSideRpc(DeviceId deviceId, JsonNode serverRpcPayload) {
|
||||
return given().spec(requestSpec)
|
||||
.body(serverRpcPayload)
|
||||
|
||||
@ -16,14 +16,13 @@
|
||||
package org.thingsboard.server.msa.cf;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.testng.annotations.AfterClass;
|
||||
import org.testng.annotations.BeforeClass;
|
||||
import org.testng.annotations.BeforeMethod;
|
||||
import org.testng.annotations.Test;
|
||||
import org.thingsboard.common.util.JacksonUtil;
|
||||
import org.thingsboard.server.common.data.AttributeScope;
|
||||
import org.thingsboard.server.common.data.DataConstants;
|
||||
import org.thingsboard.server.common.data.Device;
|
||||
import org.thingsboard.server.common.data.DeviceProfile;
|
||||
import org.thingsboard.server.common.data.asset.Asset;
|
||||
@ -31,9 +30,11 @@ import org.thingsboard.server.common.data.cf.CalculatedField;
|
||||
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
|
||||
import org.thingsboard.server.common.data.cf.configuration.Argument;
|
||||
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
|
||||
import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration;
|
||||
import org.thingsboard.server.common.data.cf.configuration.Output;
|
||||
import org.thingsboard.server.common.data.cf.configuration.OutputType;
|
||||
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
|
||||
import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration;
|
||||
import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration;
|
||||
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
|
||||
import org.thingsboard.server.common.data.debug.DebugSettings;
|
||||
@ -45,14 +46,19 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
|
||||
import org.thingsboard.server.common.data.id.EntityId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.data.id.UserId;
|
||||
import org.thingsboard.server.common.data.relation.EntityRelation;
|
||||
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
|
||||
import org.thingsboard.server.msa.AbstractContainerTest;
|
||||
import org.thingsboard.server.msa.ui.utils.EntityPrototypes;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.thingsboard.server.common.data.AttributeScope.SERVER_SCOPE;
|
||||
import static org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS;
|
||||
import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultAssetProfile;
|
||||
import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultDeviceProfile;
|
||||
import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultTenantAdmin;
|
||||
@ -98,13 +104,13 @@ public class CalculatedFieldTest extends AbstractContainerTest {
|
||||
asset = testRestClient.postAsset(createAsset("Asset 1", assetProfileId));
|
||||
|
||||
testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}"));
|
||||
testRestClient.postTelemetryAttribute(device.getId(), DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"deviceTemperature\":40}"));
|
||||
testRestClient.postTelemetryAttribute(device.getId(), SERVER_SCOPE, JacksonUtil.toJsonNode("{\"deviceTemperature\":40}"));
|
||||
|
||||
testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperatureInF\":72.32}"));
|
||||
testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperatureInF\":72.86}"));
|
||||
testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperatureInF\":73.58}"));
|
||||
|
||||
testRestClient.postTelemetryAttribute(asset.getId(), DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"altitude\":1035}"));
|
||||
testRestClient.postTelemetryAttribute(asset.getId(), SERVER_SCOPE, JacksonUtil.toJsonNode("{\"altitude\":1035}"));
|
||||
}
|
||||
|
||||
@BeforeMethod
|
||||
@ -147,7 +153,7 @@ public class CalculatedFieldTest extends AbstractContainerTest {
|
||||
CalculatedField savedCalculatedField = createSimpleCalculatedField();
|
||||
|
||||
Argument savedArgument = savedCalculatedField.getConfiguration().getArguments().get("T");
|
||||
savedArgument.setRefEntityKey(new ReferencedEntityKey("deviceTemperature", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE));
|
||||
savedArgument.setRefEntityKey(new ReferencedEntityKey("deviceTemperature", ArgumentType.ATTRIBUTE, SERVER_SCOPE));
|
||||
testRestClient.postCalculatedField(savedCalculatedField);
|
||||
|
||||
await().alias("update CF argument -> perform calculation with new argument").atMost(TIMEOUT, TimeUnit.SECONDS)
|
||||
@ -170,14 +176,14 @@ public class CalculatedFieldTest extends AbstractContainerTest {
|
||||
|
||||
Output savedOutput = savedCalculatedField.getConfiguration().getOutput();
|
||||
savedOutput.setType(OutputType.ATTRIBUTES);
|
||||
savedOutput.setScope(AttributeScope.SERVER_SCOPE);
|
||||
savedOutput.setScope(SERVER_SCOPE);
|
||||
savedOutput.setName("temperatureF");
|
||||
testRestClient.postCalculatedField(savedCalculatedField);
|
||||
|
||||
await().alias("update CF output -> perform calculation with updated output").atMost(TIMEOUT, TimeUnit.SECONDS)
|
||||
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
|
||||
.untilAsserted(() -> {
|
||||
JsonNode temperatureF = testRestClient.getAttributes(device.getId(), AttributeScope.SERVER_SCOPE, "temperatureF");
|
||||
ArrayNode temperatureF = testRestClient.getAttributes(device.getId(), SERVER_SCOPE, "temperatureF");
|
||||
assertThat(temperatureF).isNotNull();
|
||||
assertThat(temperatureF.get(0).get("value").asText()).isEqualTo("77.0");
|
||||
});
|
||||
@ -296,7 +302,7 @@ public class CalculatedFieldTest extends AbstractContainerTest {
|
||||
assertThat(airDensity.get("airDensity").get(0).get("value").asText()).isEqualTo("1.05");
|
||||
});
|
||||
|
||||
testRestClient.postTelemetryAttribute(asset.getId(), DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"altitude\":1531}"));
|
||||
testRestClient.postTelemetryAttribute(asset.getId(), SERVER_SCOPE, JacksonUtil.toJsonNode("{\"altitude\":1531}"));
|
||||
|
||||
await().alias("create CF -> update telemetry for common entity").atMost(TIMEOUT, TimeUnit.SECONDS)
|
||||
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
|
||||
@ -309,6 +315,112 @@ public class CalculatedFieldTest extends AbstractContainerTest {
|
||||
testRestClient.deleteCalculatedFieldIfExists(savedCalculatedField.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPerformSerialsOfCalculationsForGeofencingType() {
|
||||
// login tenant admin
|
||||
testRestClient.getAndSetUserToken(tenantAdminId);
|
||||
|
||||
// Device and initial coords (inside Allowed, outside Restricted)
|
||||
String deviceToken = "geoDeviceTokenA";
|
||||
Device device = testRestClient.postDevice(deviceToken, createDevice("GF Device", deviceProfileId));
|
||||
testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"latitude\":50.4730,\"longitude\":30.5050}"));
|
||||
|
||||
// Create zones
|
||||
Asset allowed = testRestClient.postAsset(createAsset("Allowed Zone", null));
|
||||
testRestClient.postTelemetryAttribute(allowed.getId(), SERVER_SCOPE,
|
||||
JacksonUtil.toJsonNode("{\"zone\":[[50.472000,30.504000],[50.472000,30.506000],[50.474000,30.506000],[50.474000,30.504000]]}"));
|
||||
|
||||
Asset restricted = testRestClient.postAsset(createAsset("Restricted Zone", null));
|
||||
testRestClient.postTelemetryAttribute(restricted.getId(), SERVER_SCOPE,
|
||||
JacksonUtil.toJsonNode("{\"zone\":[[50.475000,30.510000],[50.475000,30.512000],[50.477000,30.512000],[50.477000,30.510000]]}"));
|
||||
|
||||
// Relations FROM device
|
||||
testRestClient.postEntityRelation(new EntityRelation(device.getId(), allowed.getId(), "AllowedZone"));
|
||||
testRestClient.postEntityRelation(new EntityRelation(device.getId(), restricted.getId(), "RestrictedZone"));
|
||||
|
||||
// Build CF: GEOFENCING -> attributes output
|
||||
CalculatedField cf = new CalculatedField();
|
||||
cf.setEntityId(device.getId());
|
||||
cf.setType(CalculatedFieldType.GEOFENCING);
|
||||
cf.setName("Geofencing CF");
|
||||
cf.setDebugSettings(DebugSettings.off());
|
||||
|
||||
GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration();
|
||||
|
||||
Argument lat = new Argument();
|
||||
lat.setRefEntityKey(new ReferencedEntityKey("latitude", ArgumentType.TS_LATEST, null));
|
||||
Argument lon = new Argument();
|
||||
lon.setRefEntityKey(new ReferencedEntityKey("longitude", ArgumentType.TS_LATEST, null));
|
||||
|
||||
// Dynamic groups via relations
|
||||
Argument allowedArg = new Argument();
|
||||
var dynAllowed = new RelationQueryDynamicSourceConfiguration();
|
||||
dynAllowed.setDirection(EntitySearchDirection.FROM);
|
||||
dynAllowed.setRelationType("AllowedZone");
|
||||
dynAllowed.setMaxLevel(1);
|
||||
dynAllowed.setFetchLastLevelOnly(true);
|
||||
allowedArg.setRefEntityKey(new ReferencedEntityKey("zone", ArgumentType.ATTRIBUTE, SERVER_SCOPE));
|
||||
allowedArg.setRefDynamicSourceConfiguration(dynAllowed);
|
||||
|
||||
Argument restrictedArg = new Argument();
|
||||
var dynRestricted = new RelationQueryDynamicSourceConfiguration();
|
||||
dynRestricted.setDirection(EntitySearchDirection.FROM);
|
||||
dynRestricted.setRelationType("RestrictedZone");
|
||||
dynRestricted.setMaxLevel(1);
|
||||
dynRestricted.setFetchLastLevelOnly(true);
|
||||
restrictedArg.setRefEntityKey(new ReferencedEntityKey("zone", ArgumentType.ATTRIBUTE, SERVER_SCOPE));
|
||||
restrictedArg.setRefDynamicSourceConfiguration(dynRestricted);
|
||||
|
||||
cfg.setArguments(Map.of(
|
||||
GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY, lat,
|
||||
GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY, lon,
|
||||
"allowedZones", allowedArg,
|
||||
"restrictedZones", restrictedArg
|
||||
));
|
||||
cfg.setZoneGroupReportStrategies(Map.of(
|
||||
"allowedZones", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS,
|
||||
"restrictedZones", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS
|
||||
));
|
||||
Output out = new Output();
|
||||
out.setType(OutputType.ATTRIBUTES);
|
||||
out.setScope(SERVER_SCOPE);
|
||||
cfg.setOutput(out);
|
||||
cf.setConfiguration(cfg);
|
||||
|
||||
CalculatedField saved = testRestClient.postCalculatedField(cf);
|
||||
|
||||
// Initial ENTERED/INSIDE and OUTSIDE
|
||||
await().alias("initial geofencing evaluation").atMost(TIMEOUT, TimeUnit.SECONDS)
|
||||
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
|
||||
.untilAsserted(() -> {
|
||||
ArrayNode attrs = testRestClient.getAttributes(device.getId(), SERVER_SCOPE,
|
||||
"allowedZonesEvent,allowedZonesStatus,restrictedZonesStatus");
|
||||
assertThat(attrs).isNotNull().hasSize(3);
|
||||
Map<String, String> m = kv(attrs);
|
||||
assertThat(m).containsEntry("allowedZonesEvent", "ENTERED")
|
||||
.containsEntry("allowedZonesStatus", "INSIDE")
|
||||
.containsEntry("restrictedZonesStatus", "OUTSIDE");
|
||||
});
|
||||
|
||||
// Move device into Restricted zone -> expect LEFT/ENTERED and statuses flipped
|
||||
testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"latitude\":50.4760,\"longitude\":30.5110}"));
|
||||
|
||||
await().alias("transition after movement").atMost(TIMEOUT, TimeUnit.SECONDS)
|
||||
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
|
||||
.untilAsserted(() -> {
|
||||
ArrayNode attrs = testRestClient.getAttributes(device.getId(), SERVER_SCOPE,
|
||||
"allowedZonesEvent,allowedZonesStatus,restrictedZonesEvent,restrictedZonesStatus");
|
||||
assertThat(attrs).isNotNull().hasSize(4);
|
||||
Map<String, String> m = kv(attrs);
|
||||
assertThat(m).containsEntry("allowedZonesEvent", "LEFT")
|
||||
.containsEntry("allowedZonesStatus", "OUTSIDE")
|
||||
.containsEntry("restrictedZonesEvent", "ENTERED")
|
||||
.containsEntry("restrictedZonesStatus", "INSIDE");
|
||||
});
|
||||
|
||||
testRestClient.deleteCalculatedFieldIfExists(saved.getId());
|
||||
}
|
||||
|
||||
private CalculatedField createSimpleCalculatedField() {
|
||||
return createSimpleCalculatedField(device.getId());
|
||||
}
|
||||
@ -356,7 +468,7 @@ public class CalculatedFieldTest extends AbstractContainerTest {
|
||||
|
||||
Argument argument1 = new Argument();
|
||||
argument1.setRefEntityId(asset.getId());
|
||||
ReferencedEntityKey refEntityKey1 = new ReferencedEntityKey("altitude", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE);
|
||||
ReferencedEntityKey refEntityKey1 = new ReferencedEntityKey("altitude", ArgumentType.ATTRIBUTE, SERVER_SCOPE);
|
||||
argument1.setRefEntityKey(refEntityKey1);
|
||||
Argument argument2 = new Argument();
|
||||
ReferencedEntityKey refEntityKey2 = new ReferencedEntityKey("temperatureInF", ArgumentType.TS_ROLLING, null);
|
||||
@ -396,4 +508,12 @@ public class CalculatedFieldTest extends AbstractContainerTest {
|
||||
return asset;
|
||||
}
|
||||
|
||||
private static Map<String, String> kv(ArrayNode attrs) {
|
||||
Map<String, String> m = new HashMap<>();
|
||||
for (JsonNode n : attrs) {
|
||||
m.put(n.get("key").asText(), n.get("value").asText());
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -35,8 +35,7 @@ import java.util.Arrays;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.thingsboard.server.common.data.DataConstants.DEVICE;
|
||||
import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE;
|
||||
import static org.thingsboard.server.common.data.AttributeScope.SHARED_SCOPE;
|
||||
import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype;
|
||||
|
||||
@DisableUIListeners
|
||||
|
||||
@ -81,7 +81,7 @@ import java.util.concurrent.TimeoutException;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.testng.Assert.assertNotNull;
|
||||
import static org.testng.Assert.fail;
|
||||
import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE;
|
||||
import static org.thingsboard.server.common.data.AttributeScope.SHARED_SCOPE;
|
||||
import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype;
|
||||
|
||||
@DisableUIListeners
|
||||
@ -577,7 +577,7 @@ public class MqttClientTest extends AbstractContainerTest {
|
||||
Awaitility
|
||||
.await()
|
||||
.alias("Check device disconnect.")
|
||||
.atMost(TIMEOUT*timeoutMultiplier, TimeUnit.SECONDS)
|
||||
.atMost(TIMEOUT * timeoutMultiplier, TimeUnit.SECONDS)
|
||||
.until(() -> !returnCodeByteValue.isEmpty());
|
||||
|
||||
assertThat(returnCodeByteValueSecondClient).isEmpty();
|
||||
|
||||
@ -64,7 +64,7 @@ import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE;
|
||||
import static org.thingsboard.server.common.data.AttributeScope.SHARED_SCOPE;
|
||||
import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultGatewayPrototype;
|
||||
|
||||
@DisableUIListeners
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user