From 43b07c242fb03df5b5732cd6acd3ee9ed8c81164 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Wed, 13 Aug 2025 14:43:59 +0300 Subject: [PATCH] Added service layer test with validation of scheduling config updates --- .../cf/CalculatedFieldIntegrationTest.java | 6 +- .../GeofencingCalculatedFieldStateTest.java | 6 +- .../CalculatedFieldConfiguration.java | 2 - ...eofencingCalculatedFieldConfiguration.java | 4 +- ... => GeofencingZoneGroupConfiguration.java} | 2 +- ...ncingCalculatedFieldConfigurationTest.java | 32 +-- .../service/CalculatedFieldServiceTest.java | 193 +++++++++++++++++- 7 files changed, 216 insertions(+), 29 deletions(-) rename common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/{ZoneGroupConfiguration.java => GeofencingZoneGroupConfiguration.java} (94%) diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index 8a8d5e389e..303515ca61 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -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 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, diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java index 0d7a2971d0..cadb47c8f5 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -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 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); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java index 3459e11c0c..c7b5c6fcaf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java @@ -66,11 +66,9 @@ public interface CalculatedFieldConfiguration { return false; } - @JsonIgnore default void setScheduledUpdateIntervalSec(int scheduledUpdateIntervalSec) { } - @JsonIgnore default int getScheduledUpdateIntervalSec() { return 0; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java index b115f3e334..34b2820cd0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java @@ -44,7 +44,7 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC private boolean createRelationsWithMatchedZones; private String zoneRelationType; private EntitySearchDirection zoneRelationDirection; - private Map zoneGroupConfigurations; + private Map zoneGroupConfigurations; @Override public CalculatedFieldType getType() { @@ -91,7 +91,7 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC Set 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!"); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ZoneGroupConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingZoneGroupConfiguration.java similarity index 94% rename from common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ZoneGroupConfiguration.java rename to common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingZoneGroupConfiguration.java index cc5eb70eea..c82151fc64 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ZoneGroupConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingZoneGroupConfiguration.java @@ -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 reportEvents; diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java index 3d7974df11..44e6365b2e 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java @@ -224,8 +224,8 @@ public class GeofencingCalculatedFieldConfigurationTest { allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); arguments.put("allowedZones", allowedZonesArg); - ZoneGroupConfiguration allowedZoneConfiguration = new ZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values())); - Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); + GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values())); + Map 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 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 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 zoneGroupConfigurations = Map.of("someOtherZones", allowedZoneConfiguration); + GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values())); + Map 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 zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); + GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", null); + Map 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 zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); + GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration(reportTelemetryPrefix, Arrays.asList(GeofencingEvent.values())); + Map 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 zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); + GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values())); + Map 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 zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); + GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values())); + Map 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); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java index 2985aa7620..3c6a30ca5c 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java @@ -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();