Added basic black-box tests for geofencing CF

This commit is contained in:
dshvaika 2025-08-19 18:37:18 +03:00
parent 1421f9cc9f
commit d49d1e9a5a
5 changed files with 148 additions and 18 deletions

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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

View File

@ -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();

View File

@ -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