Browse Source

added validation for config and tests

pull/14253/head
IrynaMatveieva 11 months ago
parent
commit
40e0d4a92f
  1. 84
      application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java
  2. 57
      application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java
  3. 32
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfiguration.java
  4. 2
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggInterval.java
  5. 24
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/BaseAggInterval.java
  6. 128
      common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfigurationTest.java
  7. 29
      common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggIntervalTest.java

84
application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java

@ -93,41 +93,71 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest
} }
@Test @Test
public void testCreateCf_checkAggregation() throws Exception { public void testCreateCfAndNoTelemetryDuringInterval_checkAggregation() throws Exception {
Device device = createDevice("Device", "1234567890111"); Device device = createDevice("Device", "1234567890111");
CustomInterval customInterval = new CustomInterval("Europe/Kyiv", 0L, 30L); CustomInterval customInterval = new CustomInterval("Europe/Kyiv", 0L, 5L);
long intervalEndTs = customInterval.getCurrentIntervalEndTs();
CalculatedField totalConsumptionCF = createTotalConsumptionCF(device.getId(), customInterval, null);
long interval = customInterval.getCurrentIntervalDurationMillis();
await().alias("create CF and no telemetry during interval -> save metric with default value")
.atMost(2 * interval, TimeUnit.MILLISECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode result = getLatestTelemetry(device.getId(), "consumption");
assertThat(result).isNotNull();
assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("9999");
});
}
@Test
public void testCreateCfWithoutWatermark_checkAggregation() throws Exception {
Device device = createDevice("Device", "1234567890111");
CustomInterval customInterval = new CustomInterval("Europe/Kyiv", 0L, 5L);
long currentIntervalStartTs = customInterval.getCurrentIntervalStartTs(); long currentIntervalStartTs = customInterval.getCurrentIntervalStartTs();
long currentIntervalEndTs = customInterval.getCurrentIntervalEndTs(); long currentIntervalEndTs = customInterval.getCurrentIntervalEndTs();
long tsBeforeInterval = currentIntervalStartTs - 1000L; long tsBeforeInterval = currentIntervalStartTs - 1000;
long tsInInterval_1 = currentIntervalStartTs + 1000L; long tsInInterval_1 = currentIntervalStartTs + 1000;
long tsInInterval_2 = currentIntervalStartTs + 500L; long tsInInterval_2 = currentIntervalStartTs + 500;
long tsInInterval_3 = currentIntervalStartTs + 200L; long tsInInterval_3 = currentIntervalStartTs + 200;
postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsBeforeInterval)); postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsBeforeInterval));
postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":100}}", tsInInterval_1)); postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":100}}", tsInInterval_1));
postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":180}}", tsInInterval_2)); postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":180}}", tsInInterval_2));
postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsInInterval_3)); postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsInInterval_3));
long interval = customInterval.getCurrentIntervalDurationMillis(); long interval = customInterval.getCurrentIntervalDurationMillis();
Watermark watermark = new Watermark(60); CalculatedField totalConsumptionCF = createTotalConsumptionCF(device.getId(), customInterval, null);
CalculatedField totalConsumptionCF = createTotalConsumptionCF(device.getId(), customInterval, watermark);
await().alias("create CF -> perform aggregation after interval end")
.atMost(2 * interval, TimeUnit.MILLISECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode result = getLatestTelemetry(device.getId(), "consumption");
assertThat(result).isNotNull();
assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("400");
});
postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":500}}", tsInInterval_1));
await().alias("create CF and perform aggregation after interval end") await().alias("update telemetry that belongs to previous interval -> no aggregation since watermark is not set ")
.atMost(2 * interval, TimeUnit.MILLISECONDS) .atMost(2 * interval, TimeUnit.MILLISECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> { .untilAsserted(() -> {
ObjectNode result = getLatestTelemetry(device.getId(), "consumptionPerMin"); ObjectNode result = getLatestTelemetry(device.getId(), "consumption");
assertThat(result).isNotNull(); assertThat(result).isNotNull();
assertThat(result.get("consumptionPerMin").get(0).get("value").asText()).isEqualTo("400"); assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("400");
}); });
} }
@Test @Test
public void testCreateCf_checkAggregationDuringWatermark() throws Exception { public void testCreateCfWithWatermark_checkAggregationDuringWatermark() throws Exception {
Device device = createDevice("Device", "1234567890111"); Device device = createDevice("Device", "1234567890111");
CustomInterval customInterval = new CustomInterval("Europe/Kyiv", 0L, 30L); CustomInterval customInterval = new CustomInterval("Europe/Kyiv", 0L, 5L);
long currentIntervalStartTs = customInterval.getCurrentIntervalStartTs(); long currentIntervalStartTs = customInterval.getCurrentIntervalStartTs();
long currentIntervalEndTs = customInterval.getCurrentIntervalEndTs(); long currentIntervalEndTs = customInterval.getCurrentIntervalEndTs();
@ -141,27 +171,27 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest
postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsInInterval_3)); postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsInInterval_3));
long interval = customInterval.getCurrentIntervalDurationMillis(); long interval = customInterval.getCurrentIntervalDurationMillis();
Watermark watermark = new Watermark(60); Watermark watermark = new Watermark(10);
CalculatedField totalConsumptionCF = createTotalConsumptionCF(device.getId(), customInterval, watermark); CalculatedField totalConsumptionCF = createTotalConsumptionCF(device.getId(), customInterval, watermark);
await().alias("create CF and perform aggregation after interval end") await().alias("create CF -> perform aggregation after interval end")
.atMost(2 * interval, TimeUnit.MILLISECONDS) .atMost(2 * interval, TimeUnit.MILLISECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> { .untilAsserted(() -> {
ObjectNode result = getLatestTelemetry(device.getId(), "consumptionPerMin"); ObjectNode result = getLatestTelemetry(device.getId(), "consumption");
assertThat(result).isNotNull(); assertThat(result).isNotNull();
assertThat(result.get("consumptionPerMin").get(0).get("value").asText()).isEqualTo("400"); assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("400");
}); });
postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":300}}", tsInInterval_1)); postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":300}}", tsInInterval_1));
await().alias("create CF and perform aggregation after interval end") await().alias("update telemetry during watermark -> perform aggregation")
.atMost(2 * 10, TimeUnit.SECONDS) .atMost(2 * 10, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> { .untilAsserted(() -> {
ObjectNode result = getLatestTelemetry(device.getId(), "consumptionPerMin"); ObjectNode result = getLatestTelemetry(device.getId(), "consumption");
assertThat(result).isNotNull(); assertThat(result).isNotNull();
assertThat(result.get("consumptionPerMin").get(0).get("value").asText()).isEqualTo("600"); assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("600");
}); });
} }
@ -169,15 +199,15 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest
Map<String, Argument> arguments = new HashMap<>(); Map<String, Argument> arguments = new HashMap<>();
Argument argument = new Argument(); Argument argument = new Argument();
argument.setRefEntityKey(new ReferencedEntityKey("energy", ArgumentType.TS_LATEST, null)); argument.setRefEntityKey(new ReferencedEntityKey("energy", ArgumentType.TS_LATEST, null));
argument.setLimit(100);
arguments.put("en", argument); arguments.put("en", argument);
Map<String, AggMetric> aggMetrics = new HashMap<>(); Map<String, AggMetric> aggMetrics = new HashMap<>();
AggMetric consumptionPerMin = new AggMetric(); AggMetric consumption = new AggMetric();
consumptionPerMin.setFunction(AggFunction.SUM); consumption.setFunction(AggFunction.SUM);
consumptionPerMin.setInput(new AggKeyInput("en")); consumption.setInput(new AggKeyInput("en"));
aggMetrics.put("consumptionPerMin", consumptionPerMin); consumption.setDefaultValue(9999L);
aggMetrics.put("consumption", consumption);
Output output = new Output(); Output output = new Output();
output.setType(OutputType.TIME_SERIES); output.setType(OutputType.TIME_SERIES);
@ -208,7 +238,9 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest
configuration.setArguments(inputs); configuration.setArguments(inputs);
configuration.setMetrics(metrics); configuration.setMetrics(metrics);
configuration.setInterval(aggInterval); configuration.setInterval(aggInterval);
configuration.setWatermark(watermark); if (watermark != null) {
configuration.setWatermark(watermark);
}
configuration.setOutput(output); configuration.setOutput(output);
calculatedField.setConfiguration(configuration); calculatedField.setConfiguration(configuration);

57
application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java

@ -32,6 +32,11 @@ import org.thingsboard.server.common.data.cf.configuration.PropagationCalculated
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput;
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric;
import org.thingsboard.server.common.data.cf.configuration.aggregation.single.EntityAggregationCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.HourInterval;
import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.Watermark;
import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration;
@ -45,6 +50,7 @@ import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
@ -166,6 +172,34 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
.andExpect(status().isOk()); .andExpect(status().isOk());
} }
@Test
public void testSaveEntityAggregationCalculatedField() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
CalculatedField calculatedField = getCalculatedField(testDevice.getId(), CalculatedFieldType.ENTITY_AGGREGATION);
CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
assertThat(savedCalculatedField).isNotNull();
assertThat(savedCalculatedField.getId()).isNotNull();
assertThat(savedCalculatedField.getCreatedTime()).isGreaterThan(0);
assertThat(savedCalculatedField.getTenantId()).isEqualTo(savedTenant.getId());
assertThat(savedCalculatedField.getEntityId()).isEqualTo(calculatedField.getEntityId());
assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType());
assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName());
assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getEntityAggregationCalculatedFieldConfig());
assertThat(savedCalculatedField.getVersion()).isEqualTo(1L);
savedCalculatedField.setName("Test CF");
CalculatedField updatedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class);
assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName());
assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1);
doDelete("/api/calculatedField/" + savedCalculatedField.getId().getId().toString())
.andExpect(status().isOk());
}
@Test @Test
public void testSavePropagationCalculatedFieldWithNullArguments() throws Exception { public void testSavePropagationCalculatedFieldWithNullArguments() throws Exception {
Device testDevice = createDevice("Test device", "1234567890"); Device testDevice = createDevice("Test device", "1234567890");
@ -237,6 +271,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
case SIMPLE -> calculatedField.setConfiguration(getSimpleCalculatedFieldConfig()); case SIMPLE -> calculatedField.setConfiguration(getSimpleCalculatedFieldConfig());
case GEOFENCING -> calculatedField.setConfiguration(getGeofencingCalculatedFieldConfig()); case GEOFENCING -> calculatedField.setConfiguration(getGeofencingCalculatedFieldConfig());
case PROPAGATION -> calculatedField.setConfiguration(getPropagationCalculatedFieldConfig()); case PROPAGATION -> calculatedField.setConfiguration(getPropagationCalculatedFieldConfig());
case ENTITY_AGGREGATION -> calculatedField.setConfiguration(getEntityAggregationCalculatedFieldConfig());
} }
calculatedField.setVersion(1L); calculatedField.setVersion(1L);
return calculatedField; return calculatedField;
@ -287,6 +322,28 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
return config; return config;
} }
private CalculatedFieldConfiguration getEntityAggregationCalculatedFieldConfig() {
var config = new EntityAggregationCalculatedFieldConfiguration();
Argument energyArgument = new Argument();
energyArgument.setRefEntityKey(new ReferencedEntityKey("energy", ArgumentType.TS_LATEST, null));
config.setArguments(Map.of("en", energyArgument));
AggMetric metric = new AggMetric();
metric.setInput(new AggKeyInput("en"));
metric.setDefaultValue(9999L);
config.setMetrics(Map.of("consumption", metric));
config.setWatermark(new Watermark(TimeUnit.DAYS.toSeconds(1)));
config.setInterval(new HourInterval("Europe/Kiev", TimeUnit.MINUTES.toSeconds(15)));
Output output = new Output();
output.setType(OutputType.TIME_SERIES);
config.setOutput(output);
return config;
}
private CalculatedFieldConfiguration getSimpleCalculatedFieldConfig() { private CalculatedFieldConfiguration getSimpleCalculatedFieldConfig() {
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();

32
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfiguration.java

@ -24,6 +24,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.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput;
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric;
import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.AggInterval; import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.AggInterval;
import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.Watermark; import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.Watermark;
@ -53,6 +54,12 @@ public class EntityAggregationCalculatedFieldConfiguration implements ArgumentsB
@Override @Override
public void validate() { public void validate() {
validateArguments();
validateMetrics();
validateInterval();
}
private void validateArguments() {
if (arguments.containsKey("ctx")) { if (arguments.containsKey("ctx")) {
throw new IllegalArgumentException("Argument name 'ctx' is reserved and cannot be used."); throw new IllegalArgumentException("Argument name 'ctx' is reserved and cannot be used.");
} }
@ -61,4 +68,29 @@ public class EntityAggregationCalculatedFieldConfiguration implements ArgumentsB
} }
} }
private void validateMetrics() {
if (metrics == null || metrics.isEmpty()) {
throw new IllegalArgumentException("Metrics map cannot be empty.");
}
for (AggMetric metric : metrics.values()) {
if (metric.getInput() instanceof AggKeyInput aggKeyInput) {
if (!arguments.containsKey(aggKeyInput.getKey())) {
throw new IllegalArgumentException(
"Metric references unknown argument: '" + aggKeyInput.getKey() + "'."
);
}
} else {
throw new IllegalArgumentException("Metric key can only refer to argument.");
}
}
}
private void validateInterval() {
if (interval == null) {
throw new IllegalArgumentException("Interval must be defined.");
}
interval.validate();
}
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggInterval.java

@ -62,4 +62,6 @@ public interface AggInterval {
ZonedDateTime getNextIntervalStart(ZonedDateTime currentStart); ZonedDateTime getNextIntervalStart(ZonedDateTime currentStart);
void validate();
} }

24
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/BaseAggInterval.java

@ -23,6 +23,7 @@ import lombok.NoArgsConstructor;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.concurrent.TimeUnit;
@Data @Data
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
@ -39,7 +40,7 @@ public abstract class BaseAggInterval implements AggInterval {
return ZoneId.of(tz); return ZoneId.of(tz);
} }
protected long getOffset() { protected long getOffsetSafe() {
return offsetSec != null ? offsetSec : 0L; return offsetSec != null ? offsetSec : 0L;
} }
@ -57,7 +58,7 @@ public abstract class BaseAggInterval implements AggInterval {
@Override @Override
public long getDateTimeIntervalStartTs(ZonedDateTime dateTime) { public long getDateTimeIntervalStartTs(ZonedDateTime dateTime) {
long offset = getOffset(); long offset = getOffsetSafe();
ZonedDateTime shiftedNow = dateTime.minusSeconds(offset); ZonedDateTime shiftedNow = dateTime.minusSeconds(offset);
ZonedDateTime alignedStart = getAlignedBoundary(shiftedNow, false); ZonedDateTime alignedStart = getAlignedBoundary(shiftedNow, false);
ZonedDateTime actualStart = alignedStart.plusSeconds(offset); ZonedDateTime actualStart = alignedStart.plusSeconds(offset);
@ -73,7 +74,7 @@ public abstract class BaseAggInterval implements AggInterval {
@Override @Override
public long getDateTimeIntervalEndTs(ZonedDateTime dateTime) { public long getDateTimeIntervalEndTs(ZonedDateTime dateTime) {
long offset = getOffset(); long offset = getOffsetSafe();
ZonedDateTime shiftedNow = dateTime.minusSeconds(offset); ZonedDateTime shiftedNow = dateTime.minusSeconds(offset);
ZonedDateTime alignedEnd = getAlignedBoundary(shiftedNow, true); ZonedDateTime alignedEnd = getAlignedBoundary(shiftedNow, true);
ZonedDateTime actualEnd = alignedEnd.plusSeconds(offset); ZonedDateTime actualEnd = alignedEnd.plusSeconds(offset);
@ -82,4 +83,21 @@ public abstract class BaseAggInterval implements AggInterval {
protected abstract ZonedDateTime getAlignedBoundary(ZonedDateTime reference, boolean next); protected abstract ZonedDateTime getAlignedBoundary(ZonedDateTime reference, boolean next);
@Override
public void validate() {
try {
getZoneId();
} catch (Exception ex) {
throw new IllegalArgumentException("Invalid timezone in interval: " + ex.getMessage());
}
if (offsetSec != null) {
if (offsetSec < 0) {
throw new IllegalArgumentException("Offset cannot be negative.");
}
if (TimeUnit.SECONDS.toMillis(offsetSec) >= getCurrentIntervalDurationMillis()) {
throw new IllegalArgumentException("Offset must be greater than interval duration.");
}
}
}
} }

128
common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfigurationTest.java

@ -0,0 +1,128 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.cf.configuration.aggregation.single;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
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.Output;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunctionInput;
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput;
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric;
import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.HourInterval;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class EntityAggregationCalculatedFieldConfigurationTest {
@Test
void typeShouldBeEntityAggregation() {
var cfg = new EntityAggregationCalculatedFieldConfiguration();
assertThat(cfg.getType()).isEqualTo(CalculatedFieldType.ENTITY_AGGREGATION);
}
@ParameterizedTest
@ValueSource(strings = {"ATTRIBUTE", "TS_ROLLING"})
void validateShouldThrowWhenNotTsLatestArgumentUsed(String argumentType) {
var cfg = new EntityAggregationCalculatedFieldConfiguration();
cfg.setArguments(Map.of("k", validArgument(ArgumentType.valueOf(argumentType))));
assertThatThrownBy(cfg::validate)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Calculated field with type: '" + cfg.getType() + "' support only TS_LATEST arguments.");
}
@Test
void validateShouldThrowWhenMetricMapIsEmpty() {
var cfg = new EntityAggregationCalculatedFieldConfiguration();
cfg.setArguments(Map.of("k", validArgument(ArgumentType.TS_LATEST)));
cfg.setMetrics(Map.of());
assertThatThrownBy(cfg::validate)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Metrics map cannot be empty.");
}
@Test
void validateShouldThrowWhenMetricInputIsNotAggKeyInput() {
var cfg = new EntityAggregationCalculatedFieldConfiguration();
cfg.setArguments(Map.of("k", validArgument(ArgumentType.TS_LATEST)));
AggMetric metric = new AggMetric();
metric.setInput(new AggFunctionInput()); // cannot be function
cfg.setMetrics(Map.of("m", metric));
cfg.setInterval(new HourInterval("Europe/Kiev", null));
cfg.setOutput(new Output());
assertThatThrownBy(cfg::validate)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Metric key can only refer to argument.");
}
@Test
void validateShouldThrowWhenMetricReferencesUnknownArgument() {
var cfg = new EntityAggregationCalculatedFieldConfiguration();
cfg.setArguments(Map.of("k", validArgument(ArgumentType.TS_LATEST)));
AggMetric metric = new AggMetric();
metric.setInput(new AggKeyInput("unknown"));
cfg.setMetrics(Map.of("m", metric));
cfg.setInterval(new HourInterval("Europe/Kiev", null));
cfg.setOutput(new Output());
assertThatThrownBy(cfg::validate)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Metric references unknown argument: 'unknown'.");
}
@Test
void validateShouldThrowWhenIntervalIsNull() {
var cfg = new EntityAggregationCalculatedFieldConfiguration();
cfg.setArguments(Map.of("k", validArgument(ArgumentType.TS_LATEST)));
cfg.setMetrics(Map.of("m", validMetric()));
cfg.setInterval(null);
cfg.setOutput(new Output());
assertThatThrownBy(cfg::validate)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Interval must be defined.");
}
private Argument validArgument(ArgumentType type) {
Argument a = new Argument();
a.setRefEntityKey(new ReferencedEntityKey("key", type, null));
return a;
}
private AggMetric validMetric() {
AggMetric metric = new AggMetric();
metric.setInput(new AggKeyInput("k"));
return metric;
}
}

29
common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggIntervalTest.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval; package org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
@ -29,11 +30,39 @@ import java.util.function.LongFunction;
import java.util.stream.Stream; import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class AggIntervalTest { public class AggIntervalTest {
private static final String TZ = "Europe/Kiev"; private static final String TZ = "Europe/Kiev";
@Test
void validateShouldThrowWhenInvalidTimZone() {
AggInterval interval = new HourInterval("TimeZone", null);
assertThatThrownBy(interval::validate)
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Invalid timezone in interval: ");
}
@Test
void validateShouldThrowWhenOffsetIsNegative() {
AggInterval interval = new CustomInterval(TZ, -100L, TimeUnit.HOURS.toSeconds(2));
assertThatThrownBy(interval::validate)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Offset cannot be negative.");
}
@Test
void validateShouldThrowWhenOffsetGreaterThanIntervalDuration() {
AggInterval interval = new CustomInterval(TZ, TimeUnit.HOURS.toSeconds(2), TimeUnit.HOURS.toSeconds(2));
assertThatThrownBy(interval::validate)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Offset must be greater than interval duration.");
}
@ParameterizedTest @ParameterizedTest
@MethodSource("intervals") @MethodSource("intervals")
void testGetStartAndEndWithoutOffset(LongFunction<AggInterval> intervalCreator) { void testGetStartAndEndWithoutOffset(LongFunction<AggInterval> intervalCreator) {

Loading…
Cancel
Save