Added service layer test with validation of scheduling config updates

This commit is contained in:
dshvaika 2025-08-13 14:43:59 +03:00
parent bf83848076
commit 43b07c242f
7 changed files with 216 additions and 29 deletions

View File

@ -32,7 +32,7 @@ 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.GeofencingEvent;
import org.thingsboard.server.common.data.cf.configuration.ZoneGroupConfiguration;
import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration;
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;
@ -701,8 +701,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
// Zone group reporting config
List<GeofencingEvent> reportEvents = Arrays.stream(GeofencingEvent.values()).toList();
ZoneGroupConfiguration allowedCfg = new ZoneGroupConfiguration("allowedZone", reportEvents);
ZoneGroupConfiguration restrictedCfg = new ZoneGroupConfiguration("restrictedZone", reportEvents);
GeofencingZoneGroupConfiguration allowedCfg = new GeofencingZoneGroupConfiguration("allowedZone", reportEvents);
GeofencingZoneGroupConfiguration restrictedCfg = new GeofencingZoneGroupConfiguration("restrictedZone", reportEvents);
cfg.setZoneGroupConfigurations(Map.of(
"allowedZones", allowedCfg,

View File

@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent;
import org.thingsboard.server.common.data.cf.configuration.ZoneGroupConfiguration;
import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration;
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;
@ -337,8 +337,8 @@ public class GeofencingCalculatedFieldStateTest {
config.setArguments(Map.of("latitude", argument1, "longitude", argument2, "allowedZones", argument3, "restrictedZones", argument4));
List<GeofencingEvent> reportEvents = Arrays.stream(GeofencingEvent.values()).toList();
ZoneGroupConfiguration allowedZoneGroupConfiguration = new ZoneGroupConfiguration("allowedZone", reportEvents);
ZoneGroupConfiguration restrictedZoneGroupConfiguration = new ZoneGroupConfiguration("restrictedZone", reportEvents);
GeofencingZoneGroupConfiguration allowedZoneGroupConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", reportEvents);
GeofencingZoneGroupConfiguration restrictedZoneGroupConfiguration = new GeofencingZoneGroupConfiguration("restrictedZone", reportEvents);
config.setZoneGroupConfigurations(Map.of("allowedZones", allowedZoneGroupConfiguration, "restrictedZones", restrictedZoneGroupConfiguration));
config.setCreateRelationsWithMatchedZones(true);

View File

@ -66,11 +66,9 @@ public interface CalculatedFieldConfiguration {
return false;
}
@JsonIgnore
default void setScheduledUpdateIntervalSec(int scheduledUpdateIntervalSec) {
}
@JsonIgnore
default int getScheduledUpdateIntervalSec() {
return 0;
}

View File

@ -44,7 +44,7 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC
private boolean createRelationsWithMatchedZones;
private String zoneRelationType;
private EntitySearchDirection zoneRelationDirection;
private Map<String, ZoneGroupConfiguration> zoneGroupConfigurations;
private Map<String, GeofencingZoneGroupConfiguration> zoneGroupConfigurations;
@Override
public CalculatedFieldType getType() {
@ -91,7 +91,7 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC
Set<String> usedPrefixes = new HashSet<>();
zoneGroupsArguments.forEach((zoneGroupName, zoneGroupArgument) -> {
ZoneGroupConfiguration config = zoneGroupConfigurations.get(zoneGroupName);
GeofencingZoneGroupConfiguration config = zoneGroupConfigurations.get(zoneGroupName);
if (config == null) {
throw new IllegalArgumentException("Zone group configuration is not configured for '" + zoneGroupName + "' argument!");
}

View File

@ -20,7 +20,7 @@ import lombok.Data;
import java.util.List;
@Data
public class ZoneGroupConfiguration {
public class GeofencingZoneGroupConfiguration {
private final String reportTelemetryPrefix;
private final List<GeofencingEvent> reportEvents;

View File

@ -224,8 +224,8 @@ public class GeofencingCalculatedFieldConfigurationTest {
allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock);
arguments.put("allowedZones", allowedZonesArg);
ZoneGroupConfiguration allowedZoneConfiguration = new ZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values()));
Map<String, ZoneGroupConfiguration> zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration);
GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values()));
Map<String, GeofencingZoneGroupConfiguration> zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration);
cfg.setArguments(arguments);
cfg.setZoneGroupConfigurations(zoneGroupConfigurations);
@ -268,9 +268,9 @@ public class GeofencingCalculatedFieldConfigurationTest {
arguments.put("allowedZones", allowedZonesArg);
arguments.put("restrictedZones", restrictedZonesArg);
ZoneGroupConfiguration allowedZoneConfiguration = new ZoneGroupConfiguration("theSamePrefixTest", Arrays.asList(GeofencingEvent.values()));
ZoneGroupConfiguration restrictedZoneConfiguration = new ZoneGroupConfiguration("theSamePrefixTest", Arrays.asList(GeofencingEvent.values()));
Map<String, ZoneGroupConfiguration> zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration, "restrictedZones", restrictedZoneConfiguration);
GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("theSamePrefixTest", Arrays.asList(GeofencingEvent.values()));
GeofencingZoneGroupConfiguration restrictedZoneConfiguration = new GeofencingZoneGroupConfiguration("theSamePrefixTest", Arrays.asList(GeofencingEvent.values()));
Map<String, GeofencingZoneGroupConfiguration> zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration, "restrictedZones", restrictedZoneConfiguration);
cfg.setArguments(arguments);
cfg.setZoneGroupConfigurations(zoneGroupConfigurations);
@ -292,8 +292,8 @@ public class GeofencingCalculatedFieldConfigurationTest {
allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock);
arguments.put("allowedZones", allowedZonesArg);
ZoneGroupConfiguration allowedZoneConfiguration = new ZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values()));
Map<String, ZoneGroupConfiguration> zoneGroupConfigurations = Map.of("someOtherZones", allowedZoneConfiguration);
GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values()));
Map<String, GeofencingZoneGroupConfiguration> zoneGroupConfigurations = Map.of("someOtherZones", allowedZoneConfiguration);
cfg.setArguments(arguments);
cfg.setZoneGroupConfigurations(zoneGroupConfigurations);
@ -315,8 +315,8 @@ public class GeofencingCalculatedFieldConfigurationTest {
allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock);
arguments.put("allowedZones", allowedZonesArg);
ZoneGroupConfiguration allowedZoneConfiguration = new ZoneGroupConfiguration("allowedZone", null);
Map<String, ZoneGroupConfiguration> zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration);
GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", null);
Map<String, GeofencingZoneGroupConfiguration> zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration);
cfg.setArguments(arguments);
cfg.setZoneGroupConfigurations(zoneGroupConfigurations);
@ -340,8 +340,8 @@ public class GeofencingCalculatedFieldConfigurationTest {
allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock);
arguments.put("allowedZones", allowedZonesArg);
ZoneGroupConfiguration allowedZoneConfiguration = new ZoneGroupConfiguration(reportTelemetryPrefix, Arrays.asList(GeofencingEvent.values()));
Map<String, ZoneGroupConfiguration> zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration);
GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration(reportTelemetryPrefix, Arrays.asList(GeofencingEvent.values()));
Map<String, GeofencingZoneGroupConfiguration> zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration);
cfg.setArguments(arguments);
cfg.setZoneGroupConfigurations(zoneGroupConfigurations);
@ -365,8 +365,8 @@ public class GeofencingCalculatedFieldConfigurationTest {
allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock);
arguments.put("allowedZones", allowedZonesArg);
ZoneGroupConfiguration allowedZoneConfiguration = new ZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values()));
Map<String, ZoneGroupConfiguration> zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration);
GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values()));
Map<String, GeofencingZoneGroupConfiguration> zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration);
cfg.setArguments(arguments);
cfg.setZoneGroupConfigurations(zoneGroupConfigurations);
@ -390,8 +390,8 @@ public class GeofencingCalculatedFieldConfigurationTest {
allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock);
arguments.put("allowedZones", allowedZonesArg);
ZoneGroupConfiguration allowedZoneConfiguration = new ZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values()));
Map<String, ZoneGroupConfiguration> zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration);
GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values()));
Map<String, GeofencingZoneGroupConfiguration> zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration);
cfg.setArguments(arguments);
cfg.setZoneGroupConfigurations(zoneGroupConfigurations);
@ -448,7 +448,7 @@ public class GeofencingCalculatedFieldConfigurationTest {
args.put("allowedZones", allowed);
cfg.setArguments(args);
var zc = new ZoneGroupConfiguration("gf_allowed", Arrays.asList(GeofencingEvent.values()));
var zc = new GeofencingZoneGroupConfiguration("gf_allowed", Arrays.asList(GeofencingEvent.values()));
cfg.setZoneGroupConfigurations(Map.of("allowedZones", zc));
cfg.setCreateRelationsWithMatchedZones(true);

View File

@ -28,18 +28,24 @@ 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.CalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent;
import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration;
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.SimpleCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import java.util.Arrays;
import java.util.Map;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ -51,6 +57,8 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
private CalculatedFieldService calculatedFieldService;
@Autowired
private DeviceService deviceService;
@Autowired
private TbTenantProfileCache tbTenantProfileCache;
private ListeningExecutorService executor;
@ -90,6 +98,187 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
calculatedFieldService.deleteCalculatedField(tenantId, savedCalculatedField.getId());
}
@Test
public void testSaveGeofencingCalculatedField_shouldNotChangeScheduledInterval() {
// Arrange a device
Device device = createTestDevice();
// Build a valid Geofencing configuration
GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration();
// Coordinates: TS_LATEST, no dynamic source
Argument lat = new Argument();
lat.setRefEntityId(device.getId());
lat.setRefEntityKey(new ReferencedEntityKey("latitude", ArgumentType.TS_LATEST, null));
Argument lon = new Argument();
lon.setRefEntityId(device.getId());
lon.setRefEntityKey(new ReferencedEntityKey("longitude", ArgumentType.TS_LATEST, null));
// Zone-group argument (ATTRIBUTE) no DYNAMIC configuration, so no scheduling even if the scheduled interval is set
Argument allowed = new Argument();
lat.setRefEntityId(device.getId());
allowed.setRefEntityKey(new ReferencedEntityKey("allowed", ArgumentType.ATTRIBUTE, null));
cfg.setArguments(Map.of(
GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY, lat,
GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY, lon,
"allowed", allowed
));
// Matching zone-group configuration
var geofencingZoneGroupConfiguration = new GeofencingZoneGroupConfiguration("gf_allowed", Arrays.asList(GeofencingEvent.values()));
cfg.setZoneGroupConfigurations(Map.of("allowed", geofencingZoneGroupConfiguration));
// Set a scheduled interval to some value
cfg.setScheduledUpdateIntervalSec(600);
// Create & save Calculated Field
CalculatedField cf = new CalculatedField();
cf.setTenantId(tenantId);
cf.setEntityId(device.getId());
cf.setType(CalculatedFieldType.GEOFENCING);
cf.setName("GF clamp test");
cf.setConfigurationVersion(0);
cf.setConfiguration(cfg);
CalculatedField saved = calculatedFieldService.save(cf);
// Assert: the interval is saved, but scheduling is not enabled
int savedInterval = saved.getConfiguration().getScheduledUpdateIntervalSec();
boolean scheduledUpdateEnabled = saved.getConfiguration().isScheduledUpdateEnabled();
assertThat(savedInterval).isEqualTo(600);
assertThat(scheduledUpdateEnabled).isFalse();
calculatedFieldService.deleteCalculatedField(tenantId, saved.getId());
}
@Test
public void testSaveGeofencingCalculatedField_shouldClampScheduledIntervalToTenantMin() {
// Arrange a device
Device device = createTestDevice();
// Build a valid Geofencing configuration
GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration();
// Coordinates: TS_LATEST, no dynamic source
Argument lat = new Argument();
lat.setRefEntityId(device.getId());
lat.setRefEntityKey(new ReferencedEntityKey("latitude", ArgumentType.TS_LATEST, null));
Argument lon = new Argument();
lon.setRefEntityId(device.getId());
lon.setRefEntityKey(new ReferencedEntityKey("longitude", ArgumentType.TS_LATEST, null));
// Zone-group argument (ATTRIBUTE) make it DYNAMIC so scheduling is enabled
Argument allowed = new Argument();
allowed.setRefEntityKey(new ReferencedEntityKey("allowed", ArgumentType.ATTRIBUTE, null));
var dynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration();
dynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM);
dynamicSourceConfiguration.setMaxLevel(1);
dynamicSourceConfiguration.setRelationType(EntityRelation.CONTAINS_TYPE);
allowed.setRefDynamicSourceConfiguration(dynamicSourceConfiguration);
cfg.setArguments(Map.of(
GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY, lat,
GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY, lon,
"allowed", allowed
));
// Matching zone-group configuration
var geofencingZoneGroupConfiguration = new GeofencingZoneGroupConfiguration("gf_allowed", Arrays.asList(GeofencingEvent.values()));
cfg.setZoneGroupConfigurations(Map.of("allowed", geofencingZoneGroupConfiguration));
// Enable scheduling with an interval below tenant min
cfg.setScheduledUpdateIntervalSec(600);
// Create & save Calculated Field
CalculatedField cf = new CalculatedField();
cf.setTenantId(tenantId);
cf.setEntityId(device.getId());
cf.setType(CalculatedFieldType.GEOFENCING);
cf.setName("GF clamp test");
cf.setConfigurationVersion(0);
cf.setConfiguration(cfg);
CalculatedField saved = calculatedFieldService.save(cf);
// Assert: the interval is clamped up to tenant profile min
int savedInterval = saved.getConfiguration().getScheduledUpdateIntervalSec();
int min = tbTenantProfileCache.get(tenantId)
.getDefaultProfileConfiguration()
.getMinAllowedScheduledUpdateIntervalInSecForCF();
assertThat(savedInterval).isEqualTo(min);
calculatedFieldService.deleteCalculatedField(tenantId, saved.getId());
}
@Test
public void testSaveGeofencingCalculatedField_shouldUseScheduledIntervalFromConfig() {
// Arrange a device
Device device = createTestDevice();
// Build a valid Geofencing configuration
GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration();
// Coordinates: TS_LATEST, no dynamic source
Argument lat = new Argument();
lat.setRefEntityId(device.getId());
lat.setRefEntityKey(new ReferencedEntityKey("latitude", ArgumentType.TS_LATEST, null));
Argument lon = new Argument();
lon.setRefEntityId(device.getId());
lon.setRefEntityKey(new ReferencedEntityKey("longitude", ArgumentType.TS_LATEST, null));
// Zone-group argument (ATTRIBUTE) make it DYNAMIC so scheduling is enabled
Argument allowed = new Argument();
allowed.setRefEntityKey(new ReferencedEntityKey("allowed", ArgumentType.ATTRIBUTE, null));
var dynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration();
dynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM);
dynamicSourceConfiguration.setMaxLevel(1);
dynamicSourceConfiguration.setRelationType(EntityRelation.CONTAINS_TYPE);
allowed.setRefDynamicSourceConfiguration(dynamicSourceConfiguration);
cfg.setArguments(Map.of(
GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY, lat,
GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY, lon,
"allowed", allowed
));
// Matching zone-group configuration
var geofencingZoneGroupConfiguration = new GeofencingZoneGroupConfiguration("gf_allowed", Arrays.asList(GeofencingEvent.values()));
cfg.setZoneGroupConfigurations(Map.of("allowed", geofencingZoneGroupConfiguration));
// Get tenant profile min.
int min = tbTenantProfileCache.get(tenantId)
.getDefaultProfileConfiguration()
.getMinAllowedScheduledUpdateIntervalInSecForCF();
// Enable scheduling with an interval greater than tenant min
int valueFromConfig = min + 100;
cfg.setScheduledUpdateIntervalSec(valueFromConfig);
// Create & save Calculated Field
CalculatedField cf = new CalculatedField();
cf.setTenantId(tenantId);
cf.setEntityId(device.getId());
cf.setType(CalculatedFieldType.GEOFENCING);
cf.setName("GF no clamp test");
cf.setConfigurationVersion(0);
cf.setConfiguration(cfg);
CalculatedField saved = calculatedFieldService.save(cf);
// Assert: the interval is clamped up to tenant profile min (or stays >= original if already >= min)
int savedInterval = saved.getConfiguration().getScheduledUpdateIntervalSec();
assertThat(savedInterval).isEqualTo(valueFromConfig);
calculatedFieldService.deleteCalculatedField(tenantId, saved.getId());
}
@Test
public void testSaveCalculatedFieldWithExistingName() {
Device device = createTestDevice();