Browse Source
Move test script execution logic from controllers to TbCalculatedFieldService, eliminating the dependency of AlarmRuleController on CalculatedFieldController. Simplify entity type filtering in getAlarmRules. Add AlarmRuleControllerTest covering all endpoints.pull/15326/head
5 changed files with 533 additions and 159 deletions
@ -0,0 +1,420 @@ |
|||
/** |
|||
* Copyright © 2016-2026 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.controller; |
|||
|
|||
import com.fasterxml.jackson.core.type.TypeReference; |
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import org.junit.After; |
|||
import org.junit.Before; |
|||
import org.junit.Test; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.server.common.data.Device; |
|||
import org.thingsboard.server.common.data.DeviceProfile; |
|||
import org.thingsboard.server.common.data.EntityType; |
|||
import org.thingsboard.server.common.data.Tenant; |
|||
import org.thingsboard.server.common.data.User; |
|||
import org.thingsboard.server.common.data.alarm.AlarmSeverity; |
|||
import org.thingsboard.server.common.data.alarm.rule.AlarmRule; |
|||
import org.thingsboard.server.common.data.alarm.rule.condition.SimpleAlarmCondition; |
|||
import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression; |
|||
import org.thingsboard.server.common.data.cf.AlarmRuleDefinition; |
|||
import org.thingsboard.server.common.data.cf.AlarmRuleDefinitionInfo; |
|||
import org.thingsboard.server.common.data.cf.CalculatedField; |
|||
import org.thingsboard.server.common.data.cf.CalculatedFieldType; |
|||
import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; |
|||
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.ReferencedEntityKey; |
|||
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; |
|||
import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.page.PageData; |
|||
import org.thingsboard.server.common.data.page.PageLink; |
|||
import org.thingsboard.server.common.data.page.SortOrder; |
|||
import org.thingsboard.server.common.data.security.Authority; |
|||
import org.thingsboard.server.dao.service.DaoSqlTest; |
|||
|
|||
import java.util.Comparator; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.UUID; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
|||
|
|||
@DaoSqlTest |
|||
public class AlarmRuleControllerTest extends AbstractControllerTest { |
|||
|
|||
private Tenant savedTenant; |
|||
|
|||
@Before |
|||
public void beforeTest() throws Exception { |
|||
loginSysAdmin(); |
|||
|
|||
Tenant tenant = new Tenant(); |
|||
tenant.setTitle("My tenant"); |
|||
savedTenant = saveTenant(tenant); |
|||
assertThat(savedTenant).isNotNull(); |
|||
|
|||
User tenantAdmin = new User(); |
|||
tenantAdmin.setAuthority(Authority.TENANT_ADMIN); |
|||
tenantAdmin.setTenantId(savedTenant.getId()); |
|||
tenantAdmin.setEmail("tenant2@thingsboard.org"); |
|||
tenantAdmin.setFirstName("Joe"); |
|||
tenantAdmin.setLastName("Downs"); |
|||
|
|||
createUserAndLogin(tenantAdmin, "testPassword1"); |
|||
} |
|||
|
|||
@After |
|||
public void afterTest() throws Exception { |
|||
loginSysAdmin(); |
|||
deleteTenant(savedTenant.getId()); |
|||
} |
|||
|
|||
@Test |
|||
public void testSaveAlarmRule() throws Exception { |
|||
Device testDevice = createDevice("Test device", "1234567890"); |
|||
AlarmRuleDefinition alarmRule = createTestAlarmRule(testDevice.getId(), "High Temperature"); |
|||
|
|||
AlarmRuleDefinition saved = saveAlarmRule(alarmRule); |
|||
|
|||
assertThat(saved).isNotNull(); |
|||
assertThat(saved.getId()).isNotNull(); |
|||
assertThat(saved.getCreatedTime()).isGreaterThan(0); |
|||
assertThat(saved.getTenantId()).isEqualTo(savedTenant.getId()); |
|||
assertThat(saved.getEntityId()).isEqualTo(testDevice.getId()); |
|||
assertThat(saved.getName()).isEqualTo("High Temperature"); |
|||
assertThat(saved.getConfiguration()).isNotNull(); |
|||
assertThat(saved.getConfiguration().getCreateRules()).containsKey(AlarmSeverity.CRITICAL); |
|||
|
|||
saved.setName("Updated Alarm Rule"); |
|||
AlarmRuleDefinition updated = saveAlarmRule(saved); |
|||
|
|||
assertThat(updated.getName()).isEqualTo("Updated Alarm Rule"); |
|||
assertThat(updated.getVersion()).isEqualTo(saved.getVersion() + 1); |
|||
|
|||
doDelete("/api/alarm/rule/" + saved.getId().getId()) |
|||
.andExpect(status().isOk()); |
|||
} |
|||
|
|||
@Test |
|||
public void testGetAlarmRuleById() throws Exception { |
|||
Device testDevice = createDevice("Test device", "1234567890"); |
|||
AlarmRuleDefinition alarmRule = createTestAlarmRule(testDevice.getId(), "Test Alarm"); |
|||
|
|||
AlarmRuleDefinition saved = saveAlarmRule(alarmRule); |
|||
AlarmRuleDefinition fetched = doGet("/api/alarm/rule/" + saved.getId().getId(), AlarmRuleDefinition.class); |
|||
|
|||
assertThat(fetched).isNotNull(); |
|||
assertThat(fetched).isEqualTo(saved); |
|||
|
|||
doDelete("/api/alarm/rule/" + saved.getId().getId()) |
|||
.andExpect(status().isOk()); |
|||
} |
|||
|
|||
@Test |
|||
public void testGetAlarmRuleById_notFound() throws Exception { |
|||
doGet("/api/alarm/rule/" + UUID.randomUUID()) |
|||
.andExpect(status().isNotFound()); |
|||
} |
|||
|
|||
@Test |
|||
public void testGetAlarmRuleById_calculatedFieldNotAlarm() throws Exception { |
|||
Device testDevice = createDevice("Test device", "1234567890"); |
|||
CalculatedField cf = createSimpleCalculatedField(testDevice.getId()); |
|||
CalculatedField savedCf = doPost("/api/calculatedField", cf, CalculatedField.class); |
|||
|
|||
doGet("/api/alarm/rule/" + savedCf.getId().getId()) |
|||
.andExpect(status().isNotFound()); |
|||
|
|||
doDelete("/api/calculatedField/" + savedCf.getId().getId()) |
|||
.andExpect(status().isOk()); |
|||
} |
|||
|
|||
@Test |
|||
public void testGetAlarmRulesByEntityId() throws Exception { |
|||
Device device1 = createDevice("Device 1", "1234567890"); |
|||
Device device2 = createDevice("Device 2", "0987654321"); |
|||
AlarmRuleDefinition rule1 = saveAlarmRule(createTestAlarmRule(device1.getId(), "Rule 1")); |
|||
saveAlarmRule(createTestAlarmRule(device2.getId(), "Rule 2")); |
|||
|
|||
PageData<AlarmRuleDefinition> result = doGetTypedWithPageLink( |
|||
"/api/alarm/rules/" + EntityType.DEVICE + "/" + device1.getUuidId() + "?", |
|||
new TypeReference<>() {}, new PageLink(10)); |
|||
|
|||
assertThat(result.getData()).hasSize(1); |
|||
assertThat(result.getData().get(0).getId()).isEqualTo(rule1.getId()); |
|||
assertThat(result.getData().get(0).getName()).isEqualTo("Rule 1"); |
|||
} |
|||
|
|||
@Test |
|||
public void testGetAlarmRules() throws Exception { |
|||
Device device = createDevice("Device A", "1234567890"); |
|||
AlarmRuleDefinition deviceRule = saveAlarmRule(createTestAlarmRule(device.getId(), "Device Alarm")); |
|||
|
|||
DeviceProfile profile = doPost("/api/deviceProfile", createDeviceProfile("Profile A"), DeviceProfile.class); |
|||
AlarmRuleDefinition profileRule = saveAlarmRule(createTestAlarmRule(profile.getId(), "Profile Alarm")); |
|||
|
|||
// All alarm rules
|
|||
List<AlarmRuleDefinitionInfo> all = getAlarmRules(null, null); |
|||
assertThat(all).extracting(AlarmRuleDefinition::getName) |
|||
.contains("Device Alarm", "Profile Alarm"); |
|||
|
|||
// Filter by entity type: DEVICE
|
|||
List<AlarmRuleDefinitionInfo> deviceRules = getAlarmRules(EntityType.DEVICE, null); |
|||
assertThat(deviceRules).extracting(AlarmRuleDefinition::getName) |
|||
.containsOnly("Device Alarm"); |
|||
|
|||
// Filter by entity type: DEVICE_PROFILE
|
|||
List<AlarmRuleDefinitionInfo> profileRules = getAlarmRules(EntityType.DEVICE_PROFILE, null); |
|||
assertThat(profileRules).extracting(AlarmRuleDefinition::getName) |
|||
.containsOnly("Profile Alarm"); |
|||
|
|||
// Filter by specific entity IDs
|
|||
List<AlarmRuleDefinitionInfo> specificRules = getAlarmRules(EntityType.DEVICE, List.of(device.getUuidId())); |
|||
assertThat(specificRules).extracting(AlarmRuleDefinition::getName) |
|||
.containsOnly("Device Alarm"); |
|||
|
|||
// Verify entity names are populated
|
|||
AlarmRuleDefinitionInfo deviceInfo = all.stream() |
|||
.filter(r -> r.getName().equals("Device Alarm")).findFirst().orElseThrow(); |
|||
assertThat(deviceInfo.getEntityName()).isEqualTo("Device A"); |
|||
|
|||
AlarmRuleDefinitionInfo profileInfo = all.stream() |
|||
.filter(r -> r.getName().equals("Profile Alarm")).findFirst().orElseThrow(); |
|||
assertThat(profileInfo.getEntityName()).isEqualTo("Profile A"); |
|||
} |
|||
|
|||
@Test |
|||
public void testGetAlarmRules_textSearch() throws Exception { |
|||
Device device = createDevice("Device A", "1234567890"); |
|||
saveAlarmRule(createTestAlarmRule(device.getId(), "Temperature Alarm")); |
|||
saveAlarmRule(createTestAlarmRule(device.getId(), "Humidity Alarm")); |
|||
|
|||
PageData<AlarmRuleDefinitionInfo> result = doGetTypedWithPageLink( |
|||
"/api/alarm/rules?textSearch=Temp&", |
|||
new TypeReference<>() {}, new PageLink(10)); |
|||
|
|||
assertThat(result.getData()).hasSize(1); |
|||
assertThat(result.getData().get(0).getName()).isEqualTo("Temperature Alarm"); |
|||
} |
|||
|
|||
@Test |
|||
public void testGetAlarmRuleNames() throws Exception { |
|||
Device device = createDevice("Device A", "1234567890"); |
|||
saveAlarmRule(createTestAlarmRule(device.getId(), "Alpha Alarm")); |
|||
saveAlarmRule(createTestAlarmRule(device.getId(), "Beta Alarm")); |
|||
|
|||
PageData<String> names = getAlarmRuleNames(new PageLink(10, 0, |
|||
null, new SortOrder("", SortOrder.Direction.ASC))); |
|||
assertThat(names.getTotalElements()).isEqualTo(2); |
|||
assertThat(names.getData()).isSortedAccordingTo(Comparator.naturalOrder()); |
|||
assertThat(names.getData()).contains("Alpha Alarm", "Beta Alarm"); |
|||
|
|||
names = getAlarmRuleNames(new PageLink(10, 0, |
|||
null, new SortOrder("", SortOrder.Direction.DESC))); |
|||
assertThat(names.getData()).isSortedAccordingTo(Comparator.reverseOrder()); |
|||
|
|||
names = getAlarmRuleNames(new PageLink(10, 0, |
|||
"Alpha", new SortOrder("", SortOrder.Direction.ASC))); |
|||
assertThat(names.getTotalElements()).isEqualTo(1); |
|||
assertThat(names.getData()).containsOnly("Alpha Alarm"); |
|||
} |
|||
|
|||
@Test |
|||
public void testDeleteAlarmRule() throws Exception { |
|||
Device testDevice = createDevice("Test device", "1234567890"); |
|||
AlarmRuleDefinition saved = saveAlarmRule(createTestAlarmRule(testDevice.getId(), "To Delete")); |
|||
|
|||
assertThat(saved).isNotNull(); |
|||
|
|||
doDelete("/api/alarm/rule/" + saved.getId().getId()) |
|||
.andExpect(status().isOk()); |
|||
doGet("/api/alarm/rule/" + saved.getId().getId()) |
|||
.andExpect(status().isNotFound()); |
|||
} |
|||
|
|||
@Test |
|||
public void testDeleteAlarmRule_notFound() throws Exception { |
|||
doDelete("/api/alarm/rule/" + UUID.randomUUID()) |
|||
.andExpect(status().isNotFound()); |
|||
} |
|||
|
|||
@Test |
|||
public void testDeleteAlarmRule_calculatedFieldNotAlarm() throws Exception { |
|||
Device testDevice = createDevice("Test device", "1234567890"); |
|||
CalculatedField cf = createSimpleCalculatedField(testDevice.getId()); |
|||
CalculatedField savedCf = doPost("/api/calculatedField", cf, CalculatedField.class); |
|||
|
|||
doDelete("/api/alarm/rule/" + savedCf.getId().getId()) |
|||
.andExpect(status().isNotFound()); |
|||
|
|||
doDelete("/api/calculatedField/" + savedCf.getId().getId()) |
|||
.andExpect(status().isOk()); |
|||
} |
|||
|
|||
@Test |
|||
public void testGetLatestAlarmRuleDebugEvent() throws Exception { |
|||
Device testDevice = createDevice("Test device", "1234567890"); |
|||
AlarmRuleDefinition saved = saveAlarmRule(createTestAlarmRule(testDevice.getId(), "Debug Test")); |
|||
|
|||
JsonNode result = doGet("/api/alarm/rule/" + saved.getId().getId() + "/debug", JsonNode.class); |
|||
assertThat(result).isNull(); |
|||
|
|||
doDelete("/api/alarm/rule/" + saved.getId().getId()) |
|||
.andExpect(status().isOk()); |
|||
} |
|||
|
|||
@Test |
|||
public void testGetLatestAlarmRuleDebugEvent_notFound() throws Exception { |
|||
doGet("/api/alarm/rule/" + UUID.randomUUID() + "/debug") |
|||
.andExpect(status().isNotFound()); |
|||
} |
|||
|
|||
@Test |
|||
public void testTestAlarmRuleScript() throws Exception { |
|||
JsonNode request = JacksonUtil.toJsonNode(""" |
|||
{ |
|||
"expression": "return temperature > 50;", |
|||
"arguments": { |
|||
"temperature": { "type": "SINGLE_VALUE", "ts": 1739776478057, "value": 55 } |
|||
} |
|||
} |
|||
"""); |
|||
|
|||
JsonNode result = doPost("/api/alarm/rule/testScript", request, JsonNode.class); |
|||
|
|||
assertThat(result).isNotNull(); |
|||
assertThat(result.has("output")).isTrue(); |
|||
assertThat(result.has("error")).isTrue(); |
|||
assertThat(result.get("error").asText()).isEmpty(); |
|||
assertThat(result.get("output").asText()).isEqualTo("true"); |
|||
} |
|||
|
|||
@Test |
|||
public void testTestAlarmRuleScript_returnsFalse() throws Exception { |
|||
JsonNode request = JacksonUtil.toJsonNode(""" |
|||
{ |
|||
"expression": "return temperature > 50;", |
|||
"arguments": { |
|||
"temperature": { "type": "SINGLE_VALUE", "ts": 1739776478057, "value": 30 } |
|||
} |
|||
} |
|||
"""); |
|||
|
|||
JsonNode result = doPost("/api/alarm/rule/testScript", request, JsonNode.class); |
|||
|
|||
assertThat(result).isNotNull(); |
|||
assertThat(result.get("error").asText()).isEmpty(); |
|||
assertThat(result.get("output").asText()).isEqualTo("false"); |
|||
} |
|||
|
|||
@Test |
|||
public void testTestAlarmRuleScript_missingExpression() throws Exception { |
|||
JsonNode request = JacksonUtil.toJsonNode(""" |
|||
{ |
|||
"arguments": {} |
|||
} |
|||
"""); |
|||
|
|||
doPost("/api/alarm/rule/testScript", request) |
|||
.andExpect(status().isBadRequest()); |
|||
} |
|||
|
|||
@Test |
|||
public void testTestAlarmRuleScript_invalidExpression() throws Exception { |
|||
JsonNode request = JacksonUtil.toJsonNode(""" |
|||
{ |
|||
"expression": "invalid syntax {{{{", |
|||
"arguments": {} |
|||
} |
|||
"""); |
|||
|
|||
JsonNode result = doPost("/api/alarm/rule/testScript", request, JsonNode.class); |
|||
|
|||
assertThat(result).isNotNull(); |
|||
assertThat(result.get("error").asText()).isNotEmpty(); |
|||
} |
|||
|
|||
// --- Helper methods ---
|
|||
|
|||
private AlarmRuleDefinition createTestAlarmRule(EntityId entityId, String name) { |
|||
AlarmRuleDefinition alarmRule = new AlarmRuleDefinition(); |
|||
alarmRule.setEntityId(entityId); |
|||
alarmRule.setName(name); |
|||
alarmRule.setConfigurationVersion(1); |
|||
alarmRule.setAdditionalInfo(JacksonUtil.newObjectNode()); |
|||
|
|||
AlarmCalculatedFieldConfiguration configuration = new AlarmCalculatedFieldConfiguration(); |
|||
|
|||
Argument argument = new Argument(); |
|||
argument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); |
|||
argument.setDefaultValue("0"); |
|||
configuration.setArguments(Map.of("temperature", argument)); |
|||
|
|||
AlarmRule rule = new AlarmRule(); |
|||
TbelAlarmConditionExpression expression = new TbelAlarmConditionExpression(); |
|||
expression.setExpression("return temperature >= 50;"); |
|||
SimpleAlarmCondition condition = new SimpleAlarmCondition(); |
|||
condition.setExpression(expression); |
|||
rule.setCondition(condition); |
|||
configuration.setCreateRules(Map.of(AlarmSeverity.CRITICAL, rule)); |
|||
|
|||
alarmRule.setConfiguration(configuration); |
|||
return alarmRule; |
|||
} |
|||
|
|||
private CalculatedField createSimpleCalculatedField(EntityId entityId) { |
|||
CalculatedField cf = new CalculatedField(); |
|||
cf.setEntityId(entityId); |
|||
cf.setType(CalculatedFieldType.SIMPLE); |
|||
cf.setName("Simple CF"); |
|||
cf.setConfigurationVersion(1); |
|||
cf.setAdditionalInfo(JacksonUtil.newObjectNode()); |
|||
|
|||
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); |
|||
Argument arg = new Argument(); |
|||
arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); |
|||
config.setArguments(Map.of("T", arg)); |
|||
config.setExpression("T * 2"); |
|||
TimeSeriesOutput output = new TimeSeriesOutput(); |
|||
output.setName("result"); |
|||
config.setOutput(output); |
|||
cf.setConfiguration(config); |
|||
|
|||
return cf; |
|||
} |
|||
|
|||
private List<AlarmRuleDefinitionInfo> getAlarmRules(EntityType entityType, List<UUID> entities) throws Exception { |
|||
StringBuilder url = new StringBuilder("/api/alarm/rules?"); |
|||
if (entityType != null) { |
|||
url.append("entityType=").append(entityType).append("&"); |
|||
} |
|||
if (entities != null) { |
|||
url.append("entities=").append(String.join(",", |
|||
entities.stream().map(UUID::toString).toList())).append("&"); |
|||
} |
|||
return doGetTypedWithPageLink(url.toString(), |
|||
new TypeReference<PageData<AlarmRuleDefinitionInfo>>() {}, new PageLink(10)).getData(); |
|||
} |
|||
|
|||
private PageData<String> getAlarmRuleNames(PageLink pageLink) throws Exception { |
|||
return doGetTypedWithPageLink("/api/alarm/rules/names?", |
|||
new TypeReference<PageData<String>>() {}, pageLink); |
|||
} |
|||
|
|||
} |
|||
Loading…
Reference in new issue