Browse Source

Add controller integration tests for OR key filter queries

- Add testCountEntitiesWithOrKeyFiltersOperation: verifies OR count returns entities matching any filter
- Add testFindEntityDataWithOrKeyFiltersOperation: verifies OR data query with different keys
- Add testFindEntityDataWithOrSameKeyFilters: verifies same-key OR ungrouping
- Add testCountEntitiesWithoutKeyFiltersOperation: verifies null defaults to AND
- Add testAlarmDataQueryWithOrKeyFiltersOperation: verifies alarm queries with OR
- Add OrConditionsDisabledEntityQueryControllerTest: verifies OR rejected when toggle disabled
pull/15394/head
Viacheslav Klimov 4 months ago
parent
commit
02ddfd09a9
Failed to extract signature
  1. 325
      application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java
  2. 63
      application/src/test/java/org/thingsboard/server/controller/OrConditionsDisabledEntityQueryControllerTest.java

325
application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java

@ -62,6 +62,7 @@ import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.common.data.query.AliasEntityId;
import org.thingsboard.server.common.data.query.AvailableEntityKeysV2;
import org.thingsboard.server.common.data.query.AvailableEntityKeysV2.KeyInfo;
import org.thingsboard.server.common.data.query.ComplexOperation;
import org.thingsboard.server.common.data.query.DeviceTypeFilter;
import org.thingsboard.server.common.data.query.DynamicValue;
import org.thingsboard.server.common.data.query.DynamicValueSourceType;
@ -1729,4 +1730,328 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
return new EntityDataQuery(filter, new EntityDataPageLink(1, 0, null, null), Collections.emptyList(), null, null);
}
// --- OR conditions tests ---
@Test
public void testCountEntitiesWithOrKeyFiltersOperation() throws Exception {
// Create 3 devices with different temperature attributes
Device deviceA = new Device();
deviceA.setName("OrTestDeviceA");
deviceA.setType("orTestType");
deviceA = doPost("/api/device", deviceA, Device.class);
String payloadA = "{\"temperature\":60}";
doPost("/api/plugins/telemetry/" + deviceA.getId() + "/" + DataConstants.SHARED_SCOPE, payloadA, String.class, status().isOk());
Device deviceB = new Device();
deviceB.setName("OrTestDeviceB");
deviceB.setType("orTestType");
deviceB = doPost("/api/device", deviceB, Device.class);
String payloadB = "{\"temperature\":5}";
doPost("/api/plugins/telemetry/" + deviceB.getId() + "/" + DataConstants.SHARED_SCOPE, payloadB, String.class, status().isOk());
Device deviceC = new Device();
deviceC.setName("OrTestDeviceC");
deviceC.setType("orTestType");
deviceC = doPost("/api/device", deviceC, Device.class);
String payloadC = "{\"temperature\":30}";
doPost("/api/plugins/telemetry/" + deviceC.getId() + "/" + DataConstants.SHARED_SCOPE, payloadC, String.class, status().isOk());
Thread.sleep(1000);
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceTypes(List.of("orTestType"));
filter.setDeviceNameFilter("");
// Filter 1: temperature > 50
KeyFilter tempGt50 = new KeyFilter();
tempGt50.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
tempGt50.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate gt50 = new NumericFilterPredicate();
gt50.setValue(FilterPredicateValue.fromDouble(50));
gt50.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
tempGt50.setPredicate(gt50);
// Filter 2: temperature < 10
KeyFilter tempLt10 = new KeyFilter();
tempLt10.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
tempLt10.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate lt10 = new NumericFilterPredicate();
lt10.setValue(FilterPredicateValue.fromDouble(10));
lt10.setOperation(NumericFilterPredicate.NumericOperation.LESS);
tempLt10.setPredicate(lt10);
List<KeyFilter> keyFilters = List.of(tempGt50, tempLt10);
// OR: deviceA (60>50) and deviceB (5<10) match => count=2
EntityCountQuery orQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.OR);
countByQueryAndCheck(orQuery, 2);
// AND: no device has temperature both >50 AND <10 => count=0
EntityCountQuery andQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.AND);
countByQueryAndCheck(andQuery, 0);
}
@Test
public void testFindEntityDataWithOrKeyFiltersOperation() throws Exception {
// Create devices with different attributes
Device deviceX = new Device();
deviceX.setName("OrDataDeviceX");
deviceX.setType("orDataType");
deviceX = doPost("/api/device", deviceX, Device.class);
doPost("/api/plugins/telemetry/" + deviceX.getId() + "/" + DataConstants.SHARED_SCOPE,
"{\"status\":\"active\"}", String.class, status().isOk());
Device deviceY = new Device();
deviceY.setName("OrDataDeviceY");
deviceY.setType("orDataType");
deviceY = doPost("/api/device", deviceY, Device.class);
doPost("/api/plugins/telemetry/" + deviceY.getId() + "/" + DataConstants.SHARED_SCOPE,
"{\"humidity\":80}", String.class, status().isOk());
Device deviceZ = new Device();
deviceZ.setName("OrDataDeviceZ");
deviceZ.setType("orDataType");
deviceZ = doPost("/api/device", deviceZ, Device.class);
// deviceZ has neither matching attribute
Thread.sleep(1000);
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceTypes(List.of("orDataType"));
filter.setDeviceNameFilter("");
// Filter 1: status = "active"
KeyFilter statusFilter = new KeyFilter();
statusFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "status"));
statusFilter.setValueType(EntityKeyValueType.STRING);
StringFilterPredicate statusPred = new StringFilterPredicate();
statusPred.setValue(FilterPredicateValue.fromString("active"));
statusPred.setOperation(StringFilterPredicate.StringOperation.EQUAL);
statusFilter.setPredicate(statusPred);
// Filter 2: humidity > 70
KeyFilter humidityFilter = new KeyFilter();
humidityFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "humidity"));
humidityFilter.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate humidityPred = new NumericFilterPredicate();
humidityPred.setValue(FilterPredicateValue.fromDouble(70));
humidityPred.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
humidityFilter.setPredicate(humidityPred);
List<KeyFilter> keyFilters = List.of(statusFilter, humidityFilter);
EntityDataSortOrder sortOrder = new EntityDataSortOrder(
new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), EntityDataSortOrder.Direction.ASC
);
EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder);
List<EntityKey> entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"));
// OR: deviceX matches status=active, deviceY matches humidity>70
EntityDataQuery orQuery = new EntityDataQuery(filter, pageLink, entityFields, null, keyFilters, ComplexOperation.OR);
PageData<EntityData> result = findByQueryAndCheck(orQuery, 2);
List<String> names = result.getData().stream()
.map(e -> e.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue())
.collect(Collectors.toList());
assertThat(names).containsExactlyInAnyOrder("OrDataDeviceX", "OrDataDeviceY");
}
@Test
public void testFindEntityDataWithOrSameKeyFilters() throws Exception {
// Create devices with temperature values that test same-key OR ungrouping
Device deviceA = new Device();
deviceA.setName("OrSameKeyDeviceA");
deviceA.setType("orSameKeyType");
deviceA = doPost("/api/device", deviceA, Device.class);
doPost("/api/plugins/telemetry/" + deviceA.getId() + "/" + DataConstants.SHARED_SCOPE,
"{\"temperature\":60}", String.class, status().isOk());
Device deviceB = new Device();
deviceB.setName("OrSameKeyDeviceB");
deviceB.setType("orSameKeyType");
deviceB = doPost("/api/device", deviceB, Device.class);
doPost("/api/plugins/telemetry/" + deviceB.getId() + "/" + DataConstants.SHARED_SCOPE,
"{\"temperature\":5}", String.class, status().isOk());
Thread.sleep(1000);
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceTypes(List.of("orSameKeyType"));
filter.setDeviceNameFilter("");
// Filter 1: temperature > 50
KeyFilter tempGt50 = new KeyFilter();
tempGt50.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
tempGt50.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate gt50 = new NumericFilterPredicate();
gt50.setValue(FilterPredicateValue.fromDouble(50));
gt50.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
tempGt50.setPredicate(gt50);
// Filter 2: temperature < 10 (same key, different predicate)
KeyFilter tempLt10 = new KeyFilter();
tempLt10.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
tempLt10.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate lt10 = new NumericFilterPredicate();
lt10.setValue(FilterPredicateValue.fromDouble(10));
lt10.setOperation(NumericFilterPredicate.NumericOperation.LESS);
tempLt10.setPredicate(lt10);
List<KeyFilter> keyFilters = List.of(tempGt50, tempLt10);
EntityDataSortOrder sortOrder = new EntityDataSortOrder(
new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), EntityDataSortOrder.Direction.ASC
);
EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder);
List<EntityKey> entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"));
// OR on same key: deviceA (60>50) and deviceB (5<10) should both be returned
EntityDataQuery orQuery = new EntityDataQuery(filter, pageLink, entityFields, null, keyFilters, ComplexOperation.OR);
PageData<EntityData> result = findByQueryAndCheck(orQuery, 2);
List<String> names = result.getData().stream()
.map(e -> e.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue())
.collect(Collectors.toList());
assertThat(names).containsExactlyInAnyOrder("OrSameKeyDeviceA", "OrSameKeyDeviceB");
}
@Test
public void testCountEntitiesWithoutKeyFiltersOperation() throws Exception {
// Create 2 devices with temperature attributes
Device deviceA = new Device();
deviceA.setName("BackCompatDeviceA");
deviceA.setType("backCompatType");
deviceA = doPost("/api/device", deviceA, Device.class);
doPost("/api/plugins/telemetry/" + deviceA.getId() + "/" + DataConstants.SHARED_SCOPE,
"{\"temperature\":60}", String.class, status().isOk());
Device deviceB = new Device();
deviceB.setName("BackCompatDeviceB");
deviceB.setType("backCompatType");
deviceB = doPost("/api/device", deviceB, Device.class);
doPost("/api/plugins/telemetry/" + deviceB.getId() + "/" + DataConstants.SHARED_SCOPE,
"{\"temperature\":5}", String.class, status().isOk());
Thread.sleep(1000);
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceTypes(List.of("backCompatType"));
filter.setDeviceNameFilter("");
// Filter 1: temperature > 50
KeyFilter tempGt50 = new KeyFilter();
tempGt50.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
tempGt50.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate gt50 = new NumericFilterPredicate();
gt50.setValue(FilterPredicateValue.fromDouble(50));
gt50.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
tempGt50.setPredicate(gt50);
// Filter 2: temperature < 10
KeyFilter tempLt10 = new KeyFilter();
tempLt10.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
tempLt10.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate lt10 = new NumericFilterPredicate();
lt10.setValue(FilterPredicateValue.fromDouble(10));
lt10.setOperation(NumericFilterPredicate.NumericOperation.LESS);
tempLt10.setPredicate(lt10);
List<KeyFilter> keyFilters = List.of(tempGt50, tempLt10);
// Query without keyFiltersOperation (null) -- should behave as AND
EntityCountQuery nullOpQuery = new EntityCountQuery(filter, keyFilters);
Long nullResult = countByQueryAndCheck(nullOpQuery, 0);
// Query with explicit AND -- should produce the same result
EntityCountQuery andOpQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.AND);
Long andResult = countByQueryAndCheck(andOpQuery, 0);
Assert.assertEquals(nullResult, andResult);
}
@Test
public void testAlarmDataQueryWithOrKeyFiltersOperation() throws Exception {
loginTenantAdmin();
// Create devices with different temperatures and alarms
Device deviceHot = new Device();
deviceHot.setName("OrAlarmDeviceHot");
deviceHot.setType("orAlarmType");
deviceHot = doPost("/api/device", deviceHot, Device.class);
doPost("/api/plugins/telemetry/" + deviceHot.getId() + "/" + DataConstants.SHARED_SCOPE,
"{\"temperature\":60}", String.class, status().isOk());
Device deviceCold = new Device();
deviceCold.setName("OrAlarmDeviceCold");
deviceCold.setType("orAlarmType");
deviceCold = doPost("/api/device", deviceCold, Device.class);
doPost("/api/plugins/telemetry/" + deviceCold.getId() + "/" + DataConstants.SHARED_SCOPE,
"{\"temperature\":5}", String.class, status().isOk());
Device deviceMid = new Device();
deviceMid.setName("OrAlarmDeviceMid");
deviceMid.setType("orAlarmType");
deviceMid = doPost("/api/device", deviceMid, Device.class);
doPost("/api/plugins/telemetry/" + deviceMid.getId() + "/" + DataConstants.SHARED_SCOPE,
"{\"temperature\":30}", String.class, status().isOk());
Thread.sleep(500);
// Create alarms for each device
Alarm alarmHot = new Alarm();
alarmHot.setOriginator(deviceHot.getId());
alarmHot.setType("highTemp");
alarmHot.setSeverity(AlarmSeverity.CRITICAL);
doPost("/api/alarm", alarmHot, Alarm.class);
Alarm alarmCold = new Alarm();
alarmCold.setOriginator(deviceCold.getId());
alarmCold.setType("lowTemp");
alarmCold.setSeverity(AlarmSeverity.WARNING);
doPost("/api/alarm", alarmCold, Alarm.class);
Alarm alarmMid = new Alarm();
alarmMid.setOriginator(deviceMid.getId());
alarmMid.setType("normalTemp");
alarmMid.setSeverity(AlarmSeverity.WARNING);
doPost("/api/alarm", alarmMid, Alarm.class);
Thread.sleep(500);
// Filter 1: temperature > 50
KeyFilter tempGt50 = new KeyFilter();
tempGt50.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
tempGt50.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate gt50 = new NumericFilterPredicate();
gt50.setValue(FilterPredicateValue.fromDouble(50));
gt50.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
tempGt50.setPredicate(gt50);
// Filter 2: temperature < 10
KeyFilter tempLt10 = new KeyFilter();
tempLt10.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
tempLt10.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate lt10 = new NumericFilterPredicate();
lt10.setValue(FilterPredicateValue.fromDouble(10));
lt10.setOperation(NumericFilterPredicate.NumericOperation.LESS);
tempLt10.setPredicate(lt10);
List<KeyFilter> keyFilters = List.of(tempGt50, tempLt10);
DeviceTypeFilter entityFilter = new DeviceTypeFilter();
entityFilter.setDeviceTypes(List.of("orAlarmType"));
entityFilter.setDeviceNameFilter("");
AlarmDataPageLink pageLink = new AlarmDataPageLink();
pageLink.setPage(0);
pageLink.setPageSize(100);
pageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.ALARM_FIELD, "createdTime")));
List<EntityKey> alarmFields = List.of(new EntityKey(EntityKeyType.ALARM_FIELD, "type"));
// OR query: should return alarms for deviceHot (60>50) and deviceCold (5<10) = 2 alarms
AlarmDataQuery orAlarmQuery = new AlarmDataQuery(entityFilter, pageLink, null, null, keyFilters, alarmFields, ComplexOperation.OR);
PageData<AlarmData> alarmResult = findAlarmsByQueryAndCheck(orAlarmQuery, 2);
List<String> alarmTypes = alarmResult.getData().stream().map(AlarmData::getType).collect(Collectors.toList());
assertThat(alarmTypes).containsExactlyInAnyOrder("highTemp", "lowTemp");
}
}

63
application/src/test/java/org/thingsboard/server/controller/OrConditionsDisabledEntityQueryControllerTest.java

@ -0,0 +1,63 @@
/**
* 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 org.junit.Test;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.server.common.data.query.ComplexOperation;
import org.thingsboard.server.common.data.query.DeviceTypeFilter;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.Collections;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
@DirtiesContext
@TestPropertySource(properties = {
"queue.edqs.sync.enabled=true",
"queue.edqs.api.supported=false",
"sql.query.key-filters-or-conditions.enabled=false"
})
public class OrConditionsDisabledEntityQueryControllerTest extends EntityQueryControllerTest {
@Test
public void testOrKeyFiltersOperationRejectedWhenDisabled() throws Exception {
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceTypes(List.of("default"));
filter.setDeviceNameFilter("");
// POST a query with OR operation -- should be rejected with 400
EntityCountQuery orQuery = new EntityCountQuery(filter, Collections.emptyList(), ComplexOperation.OR);
String errorMessage = getErrorMessage(
doPost("/api/entitiesQuery/count", orQuery).andExpect(status().isBadRequest())
);
assertThat(errorMessage).contains("OR conditions between key filters are disabled");
// POST a query without keyFiltersOperation (null/AND) -- should still succeed
EntityCountQuery andQuery = new EntityCountQuery(filter, Collections.emptyList());
doPost("/api/entitiesQuery/count", andQuery).andExpect(status().isOk());
// POST a query with explicit AND -- should also succeed
EntityCountQuery explicitAndQuery = new EntityCountQuery(filter, Collections.emptyList(), ComplexOperation.AND);
doPost("/api/entitiesQuery/count", explicitAndQuery).andExpect(status().isOk());
}
}
Loading…
Cancel
Save