From 8352be035bf9dc1a02ac36ded2eb8a57fbfe8f60 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Fri, 10 Mar 2023 11:40:31 +0100 Subject: [PATCH 1/8] LWM2M bean lifecycle dependency fix to avoid executors shutdown before server stop. Dependencies refactored: implementations replaced with interfaces. --- .../lwm2m/AbstractLwM2MIntegrationTest.java | 4 +- .../lwm2m/client/LwM2MTestClient.java | 6 +-- .../server/DefaultLwM2mTransportService.java | 6 ++- .../server/client/LwM2mClientContextImpl.java | 4 +- .../DefaultLwM2mDownlinkMsgHandler.java | 3 +- .../ota/DefaultLwM2MOtaUpdateService.java | 1 + .../uplink/DefaultLwM2mUplinkMsgHandler.java | 48 +++++-------------- .../server/uplink/LwM2mUplinkMsgHandler.java | 6 +++ .../lwm2m/utils/LwM2MTransportUtil.java | 16 +++---- 9 files changed, 40 insertions(+), 54 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index ef00a3050e..c92369e3c2 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -70,7 +70,7 @@ import org.thingsboard.server.service.telemetry.cmd.v2.LatestValueCmd; import org.thingsboard.server.transport.AbstractTransportIntegrationTest; import org.thingsboard.server.transport.lwm2m.client.LwM2MTestClient; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; -import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler; +import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; import java.io.IOException; import java.net.ServerSocket; @@ -108,7 +108,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfil public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportIntegrationTest { @SpyBean - DefaultLwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest; + LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest; @Autowired private LwM2mClientContext clientContextTest; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java index 7cb5f88733..449ac4c2ae 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java @@ -45,7 +45,7 @@ import org.junit.Assert; import org.mockito.Mockito; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; -import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler; +import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; import java.io.IOException; @@ -109,12 +109,12 @@ public class LwM2MTestClient { private LwM2MLocationParams locationParams; private LwM2mTemperatureSensor lwM2MTemperatureSensor; private Set clientStates; - private DefaultLwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest; + private LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest; private LwM2mClientContext clientContext; public void init(Security security, Configuration coapConfig, int port, boolean isRpc, boolean isBootstrap, int shortServerId, int shortServerIdBs, Security securityBs, - DefaultLwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler, + LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler, LwM2mClientContext clientContext) throws InvalidDDFFileException, IOException { Assert.assertNull("client already initialized", leshanClient); this.defaultLwM2mUplinkMsgHandlerTest = defaultLwM2mUplinkMsgHandler; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java index f4b52a0f5f..3eb096cd38 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java @@ -25,6 +25,7 @@ import org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder; import org.eclipse.leshan.server.californium.LeshanServer; import org.eclipse.leshan.server.californium.LeshanServerBuilder; import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore; +import org.springframework.context.annotation.DependsOn; import org.springframework.stereotype.Component; import org.thingsboard.server.cache.ota.OtaPackageDataCache; import org.thingsboard.server.common.data.DataConstants; @@ -35,7 +36,7 @@ import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MAuthorizer; import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MDtlsCertificateVerifier; import org.thingsboard.server.transport.lwm2m.server.store.TbSecurityStore; -import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler; +import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; import javax.annotation.PreDestroy; @@ -56,6 +57,7 @@ import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaU @Slf4j @Component +@DependsOn({"lwM2mDownlinkMsgHandler", "lwM2mUplinkMsgHandler"}) @TbLwM2mTransportComponent @RequiredArgsConstructor public class DefaultLwM2mTransportService implements LwM2MTransportService { @@ -66,7 +68,7 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { private final LwM2mTransportContext context; private final LwM2MTransportServerConfig config; private final OtaPackageDataCache otaPackageDataCache; - private final DefaultLwM2mUplinkMsgHandler handler; + private final LwM2mUplinkMsgHandler handler; private final CaliforniumRegistrationStore registrationStore; private final TbSecurityStore securityStore; private final TbLwM2MDtlsCertificateVerifier certificateVerifier; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java index 49a77d1eea..d624badc1a 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java @@ -42,7 +42,7 @@ import org.thingsboard.server.transport.lwm2m.server.ota.LwM2MOtaUpdateService; import org.thingsboard.server.transport.lwm2m.server.session.LwM2MSessionManager; import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2MClientStore; import org.thingsboard.server.transport.lwm2m.server.store.TbMainSecurityStore; -import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler; +import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; import org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil; import java.util.Arrays; @@ -75,7 +75,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { @Autowired @Lazy - private DefaultLwM2mUplinkMsgHandler defaultLwM2MUplinkMsgHandler; + private LwM2mUplinkMsgHandler defaultLwM2MUplinkMsgHandler; @Autowired @Lazy private LwM2MOtaUpdateService otaUpdateService; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java index 735fa36e1e..580d59c11d 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java @@ -103,7 +103,7 @@ import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.ge import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.validateVersionedId; @Slf4j -@Service +@Service("lwM2mDownlinkMsgHandler") @TbLwM2mTransportComponent @RequiredArgsConstructor public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService implements LwM2mDownlinkMsgHandler { @@ -124,6 +124,7 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im @PreDestroy public void destroy() { + log.trace("Destroying {}", getClass().getSimpleName()); super.destroy(); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java index 153e3894fb..355656759d 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java @@ -146,6 +146,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl @PreDestroy public void destroy() { + log.info("Destroying {}", getClass().getSimpleName()); super.destroy(); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java index c790d2495c..25e3a6b591 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java @@ -20,6 +20,8 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.reflect.TypeToken; +import lombok.Getter; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.model.ObjectModel; @@ -32,6 +34,7 @@ import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mResource; import org.eclipse.leshan.core.node.LwM2mResourceInstance; import org.eclipse.leshan.core.node.LwM2mSingleResource; +import org.eclipse.leshan.core.node.codec.LwM2mValueConverter; import org.eclipse.leshan.core.observation.Observation; import org.eclipse.leshan.core.request.*; import org.eclipse.leshan.core.request.WriteRequest.Mode; @@ -122,69 +125,41 @@ import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fr @Slf4j -@Service +@Service("lwM2mUplinkMsgHandler") @TbLwM2mTransportComponent +@RequiredArgsConstructor public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService implements LwM2mUplinkMsgHandler { - public LwM2mValueConverterImpl converter; + @Getter + private final LwM2mValueConverter converter = LwM2mValueConverterImpl.getInstance();; private final TransportService transportService; private final LwM2mTransportContext context; + @Lazy private final LwM2MAttributesService attributesService; private final LwM2MSessionManager sessionManager; + @Lazy private final LwM2MOtaUpdateService otaService; private final LwM2MTransportServerConfig config; private final LwM2MTelemetryLogService logService; private final LwM2mTransportServerHelper helper; private final TbLwM2MDtlsSessionStore sessionStore; private final LwM2mClientContext clientContext; - private final LwM2mDownlinkMsgHandler defaultLwM2MDownlinkMsgHandler; + private final LwM2mDownlinkMsgHandler defaultLwM2MDownlinkMsgHandler; //Do not use Lazy because we need live executor to handle msgs private final LwM2mVersionedModelProvider modelProvider; private final RegistrationStore registrationStore; private final TbLwM2mSecurityStore securityStore; private final LwM2MModelConfigService modelConfigService; - public DefaultLwM2mUplinkMsgHandler(TransportService transportService, - LwM2MTransportServerConfig config, - LwM2mTransportServerHelper helper, - LwM2mClientContext clientContext, - LwM2MTelemetryLogService logService, - LwM2MSessionManager sessionManager, - @Lazy LwM2MOtaUpdateService otaService, - @Lazy LwM2MAttributesService attributesService, - @Lazy LwM2mDownlinkMsgHandler defaultLwM2MDownlinkMsgHandler, - LwM2mTransportContext context, - TbLwM2MDtlsSessionStore sessionStore, - LwM2mVersionedModelProvider modelProvider, - RegistrationStore registrationStore, - TbLwM2mSecurityStore securityStore, - LwM2MModelConfigService modelConfigService) { - this.transportService = transportService; - this.sessionManager = sessionManager; - this.attributesService = attributesService; - this.otaService = otaService; - this.config = config; - this.helper = helper; - this.clientContext = clientContext; - this.logService = logService; - this.defaultLwM2MDownlinkMsgHandler = defaultLwM2MDownlinkMsgHandler; - this.context = context; - this.sessionStore = sessionStore; - this.modelProvider = modelProvider; - this.registrationStore = registrationStore; - this.securityStore = securityStore; - this.modelConfigService = modelConfigService; - } - @PostConstruct public void init() { super.init(); this.context.getScheduler().scheduleAtFixedRate(this::reportActivity, new Random().nextInt((int) config.getSessionReportTimeout()), config.getSessionReportTimeout(), TimeUnit.MILLISECONDS); - this.converter = LwM2mValueConverterImpl.getInstance(); } @PreDestroy public void destroy() { + log.trace("Destroying {}", getClass().getSimpleName()); super.destroy(); } @@ -955,6 +930,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl * * @param lwM2MClient - LwM2M Client */ + @Override public void initAttributes(LwM2mClient lwM2MClient, boolean logFailedUpdateOfNonChangedValue) { Map keyNamesMap = this.getNamesFromProfileForSharedAttributes(lwM2MClient); if (!keyNamesMap.isEmpty()) { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java index 0b2d819fa8..9beebe5279 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.transport.lwm2m.server.uplink; +import org.eclipse.leshan.core.node.codec.LwM2mValueConverter; import org.eclipse.leshan.core.observation.Observation; import org.eclipse.leshan.core.request.CreateRequest; import org.eclipse.leshan.core.request.SendRequest; @@ -69,5 +70,10 @@ public interface LwM2mUplinkMsgHandler { void onToTransportUpdateCredentials(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.ToTransportUpdateCredentialsProto updateCredentials); + void initAttributes(LwM2mClient lwM2MClient, boolean logFailedUpdateOfNonChangedValue); + LwM2MTransportServerConfig getConfig(); + + LwM2mValueConverter getConverter(); + } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java index f207e7d05a..861261d951 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java @@ -52,7 +52,7 @@ import org.thingsboard.server.transport.lwm2m.server.ota.firmware.FirmwareUpdate import org.thingsboard.server.transport.lwm2m.server.ota.firmware.FirmwareUpdateState; import org.thingsboard.server.transport.lwm2m.server.ota.software.SoftwareUpdateResult; import org.thingsboard.server.transport.lwm2m.server.ota.software.SoftwareUpdateState; -import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler; +import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; import java.util.ArrayList; import java.util.Arrays; @@ -289,12 +289,12 @@ public class LwM2MTransportUtil { * Attribute pmax = new Attribute(MAXIMUM_PERIOD, "60"); * Attribute [] attrs = {gt, st}; */ - public static SimpleDownlinkRequest createWriteAttributeRequest(String target, Object params, DefaultLwM2mUplinkMsgHandler serviceImpl) { + public static SimpleDownlinkRequest createWriteAttributeRequest(String target, Object params, LwM2mUplinkMsgHandler serviceImpl) { AttributeSet attrSet = new AttributeSet(createWriteAttributes(params, serviceImpl, target)); return attrSet.getAttributes().size() > 0 ? new WriteAttributesRequest(target, attrSet) : null; } - private static Attribute[] createWriteAttributes(Object params, DefaultLwM2mUplinkMsgHandler serviceImpl, String target) { + private static Attribute[] createWriteAttributes(Object params, LwM2mUplinkMsgHandler serviceImpl, String target) { List attributeLists = new ArrayList<>(); Map map = JacksonUtil.convertValue(params, new TypeReference<>() { }); @@ -366,19 +366,19 @@ public class LwM2MTransportUtil { return newValues; } - public static Object convertWriteAttributes(String type, Object value, DefaultLwM2mUplinkMsgHandler serviceImpl, String target) { + public static Object convertWriteAttributes(String type, Object value, LwM2mUplinkMsgHandler serviceImpl, String target) { switch (type) { /** Integer [0:255]; */ case DIMENSION: - Long dim = (Long) serviceImpl.converter.convertValue(value, equalsResourceTypeGetSimpleName(value), INTEGER, new LwM2mPath(target)); + Long dim = (Long) serviceImpl.getConverter().convertValue(value, equalsResourceTypeGetSimpleName(value), INTEGER, new LwM2mPath(target)); return dim >= 0 && dim <= 255 ? dim : null; /**String;*/ case OBJECT_VERSION: - return serviceImpl.converter.convertValue(value, equalsResourceTypeGetSimpleName(value), STRING, new LwM2mPath(target)); + return serviceImpl.getConverter().convertValue(value, equalsResourceTypeGetSimpleName(value), STRING, new LwM2mPath(target)); /**INTEGER */ case MINIMUM_PERIOD: case MAXIMUM_PERIOD: - return serviceImpl.converter.convertValue(value, equalsResourceTypeGetSimpleName(value), INTEGER, new LwM2mPath(target)); + return serviceImpl.getConverter().convertValue(value, equalsResourceTypeGetSimpleName(value), INTEGER, new LwM2mPath(target)); /**Float; */ case GREATER_THAN: case LESSER_THAN: @@ -386,7 +386,7 @@ public class LwM2MTransportUtil { if (value.getClass().getSimpleName().equals("String")) { value = Double.valueOf((String) value); } - return serviceImpl.converter.convertValue(value, equalsResourceTypeGetSimpleName(value), FLOAT, new LwM2mPath(target)); + return serviceImpl.getConverter().convertValue(value, equalsResourceTypeGetSimpleName(value), FLOAT, new LwM2mPath(target)); default: return null; } From 817487b74d64c999e8cdac8d5286ddf4887704ad Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Fri, 10 Mar 2023 11:44:13 +0100 Subject: [PATCH 2/8] DefaultLwM2mDownlinkMsgHandler - wrapped any exceptions on handleDownlinkError that may fire and prevent graceful server shutdown --- .../DefaultLwM2mDownlinkMsgHandler.java | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java index 580d59c11d..ecc57a7458 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java @@ -85,6 +85,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.RejectedExecutionException; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -522,16 +523,22 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im private , T extends LwM2mResponse> void handleDownlinkError(LwM2mClient client, R request, DownlinkRequestCallback callback, Exception e) { log.trace("[{}] Received downlink error: {}.", client.getEndpoint(), e); - client.updateLastUplinkTime(); - executor.submit(() -> { - if (e instanceof TimeoutException || e instanceof ClientSleepingException) { - log.trace("[{}] Received {}, client is probably sleeping", client.getEndpoint(), e.getClass().getSimpleName()); - clientContext.asleep(client); - } else { - log.trace("[{}] Received {}", client.getEndpoint(), e.getClass().getSimpleName()); - } - callback.onError(toString(request), e); - }); + try { + client.updateLastUplinkTime(); + executor.submit(() -> { + if (e instanceof TimeoutException || e instanceof ClientSleepingException) { + log.trace("[{}] Received {}, client is probably sleeping", client.getEndpoint(), e.getClass().getSimpleName()); + clientContext.asleep(client); + } else { + log.trace("[{}] Received {}", client.getEndpoint(), e.getClass().getSimpleName()); + } + callback.onError(toString(request), e); + }); + } catch (RejectedExecutionException ree) { + log.warn("[{}] Can not handle downlink error. Executor already down", client.getEndpoint(), ree); + } catch (Exception exception) { + log.warn("[{}] Can not handle downlink error", client.getEndpoint(), exception); + } } private WriteRequest getWriteRequestSingleResource(ResourceModel.Type type, ContentFormat contentFormat, int objectId, int instanceId, int resourceId, Object value) { From fed47f699cda3c37219e4e1a867d72225b6453fd Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Fri, 10 Mar 2023 12:16:41 +0100 Subject: [PATCH 3/8] LwM2m lifecycle debug for the test scope --- application/src/test/resources/logback-test.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/application/src/test/resources/logback-test.xml b/application/src/test/resources/logback-test.xml index 23e6d8c2f6..64c101cec4 100644 --- a/application/src/test/resources/logback-test.xml +++ b/application/src/test/resources/logback-test.xml @@ -21,6 +21,11 @@ + + + + + From 7b32286e4c7dae1cf7a3e3d14297a281775e7b22 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Fri, 10 Mar 2023 12:18:47 +0100 Subject: [PATCH 4/8] log trace on destroy for DefaultLwM2MOtaUpdateService --- .../lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java index 355656759d..f59643cbeb 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java @@ -146,7 +146,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl @PreDestroy public void destroy() { - log.info("Destroying {}", getClass().getSimpleName()); + log.trace("Destroying {}", getClass().getSimpleName()); super.destroy(); } From 287999d46a6fa7db79efb742a980b0bb0a9e910b Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Fri, 10 Mar 2023 12:37:10 +0100 Subject: [PATCH 5/8] test scope: logs for lwm2m.server --- application/src/test/resources/logback-test.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/application/src/test/resources/logback-test.xml b/application/src/test/resources/logback-test.xml index 64c101cec4..953b7094a4 100644 --- a/application/src/test/resources/logback-test.xml +++ b/application/src/test/resources/logback-test.xml @@ -25,6 +25,8 @@ + + From 0e01f1c74d267aa1da0c9361631c5ae9565682fb Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Fri, 10 Mar 2023 15:53:47 +0200 Subject: [PATCH 6/8] sendUpdateSharedAttributeToCloudAndValidateDeviceSubscription - timeout increased for run test stable on slow (busy) environments --- .../java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java index 98508e72ce..303e91ee80 100644 --- a/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java @@ -690,7 +690,7 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest { edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); Assert.assertTrue(edgeImitator.waitForResponses()); - Assert.assertTrue(onUpdateCallback.getSubscribeLatch().await(5, TimeUnit.SECONDS)); + Assert.assertTrue(onUpdateCallback.getSubscribeLatch().await(30, TimeUnit.SECONDS)); Assert.assertEquals(JacksonUtil.OBJECT_MAPPER.createObjectNode().put(attrKey, attrValue), JacksonUtil.fromBytes(onUpdateCallback.getPayloadBytes())); From 5cb1b7d275cc04e35df197b682e72bf7a0225f3f Mon Sep 17 00:00:00 2001 From: Oleksandra Matviienko Date: Mon, 13 Mar 2023 22:09:16 +0100 Subject: [PATCH 7/8] BaseOtaPackageServiceTest: Rule ExpectedException refactored with AssertJ Exception Assertions --- .../service/BaseOtaPackageServiceTest.java | 134 +++++++++--------- 1 file changed, 66 insertions(+), 68 deletions(-) diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java index c89e7c4b69..c5c217a486 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java @@ -19,9 +19,7 @@ import com.datastax.oss.driver.api.core.uuid.Uuids; import org.junit.After; import org.junit.Assert; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; @@ -44,6 +42,7 @@ import java.util.Collections; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE; public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { @@ -78,10 +77,6 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { deviceProfileId = savedDeviceProfile.getId(); } - @SuppressWarnings("deprecation") - @Rule - public ExpectedException thrown = ExpectedException.none(); - @After public void after() { tenantService.deleteTenant(tenantId); @@ -99,9 +94,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { createAndSaveFirmware(tenantId, "1"); Assert.assertEquals(1, otaPackageService.sumDataSizeByTenantId(tenantId)); - thrown.expect(DataValidationException.class); - thrown.expectMessage(String.format("Failed to create the ota package, files size limit is exhausted %d bytes!", DATA_SIZE)); - createAndSaveFirmware(tenantId, "2"); + assertThatThrownBy(() -> createAndSaveFirmware(tenantId, "2")) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("Failed to create the ota package, files size limit is exhausted %d bytes!", DATA_SIZE); } @Test @@ -243,9 +238,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmware.setChecksum(CHECKSUM); firmware.setData(DATA); - thrown.expect(DataValidationException.class); - thrown.expectMessage("OtaPackage should be assigned to tenant!"); - otaPackageService.saveOtaPackage(firmware); + assertThatThrownBy(() -> otaPackageService.saveOtaPackage(firmware)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("OtaPackage should be assigned to tenant!"); } @Test @@ -261,9 +256,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmware.setChecksum(CHECKSUM); firmware.setData(DATA); - thrown.expect(DataValidationException.class); - thrown.expectMessage("Type should be specified!"); - otaPackageService.saveOtaPackage(firmware); + assertThatThrownBy(() -> otaPackageService.saveOtaPackage(firmware)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("Type should be specified!"); } @Test @@ -279,9 +274,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmware.setChecksum(CHECKSUM); firmware.setData(DATA); - thrown.expect(DataValidationException.class); - thrown.expectMessage("OtaPackage title should be specified!"); - otaPackageService.saveOtaPackage(firmware); + assertThatThrownBy(() -> otaPackageService.saveOtaPackage(firmware)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("OtaPackage title should be specified!"); } @Test @@ -297,9 +292,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmware.setChecksum(CHECKSUM); firmware.setData(DATA); - thrown.expect(DataValidationException.class); - thrown.expectMessage("OtaPackage file name should be specified!"); - otaPackageService.saveOtaPackage(firmware); + assertThatThrownBy(() -> otaPackageService.saveOtaPackage(firmware)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("OtaPackage file name should be specified!"); } @Test @@ -315,9 +310,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmware.setChecksum(CHECKSUM); firmware.setData(DATA); - thrown.expect(DataValidationException.class); - thrown.expectMessage("OtaPackage content type should be specified!"); - otaPackageService.saveOtaPackage(firmware); + assertThatThrownBy(() -> otaPackageService.saveOtaPackage(firmware)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("OtaPackage content type should be specified!"); } @Test @@ -333,9 +328,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM); firmware.setChecksum(CHECKSUM); - thrown.expect(DataValidationException.class); - thrown.expectMessage("OtaPackage data should be specified!"); - otaPackageService.saveOtaPackage(firmware); + assertThatThrownBy(() -> otaPackageService.saveOtaPackage(firmware)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("OtaPackage data should be specified!"); } @Test @@ -352,9 +347,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmware.setChecksum(CHECKSUM); firmware.setData(DATA); - thrown.expect(DataValidationException.class); - thrown.expectMessage("OtaPackage is referencing to non-existent tenant!"); - otaPackageService.saveOtaPackage(firmware); + assertThatThrownBy(() -> otaPackageService.saveOtaPackage(firmware)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("OtaPackage is referencing to non-existent tenant!"); } @Test @@ -371,9 +366,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmware.setChecksum(CHECKSUM); firmware.setData(DATA); - thrown.expect(DataValidationException.class); - thrown.expectMessage("OtaPackage is referencing to non-existent device profile!"); - otaPackageService.saveOtaPackage(firmware); + assertThatThrownBy(() -> otaPackageService.saveOtaPackage(firmware)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("OtaPackage is referencing to non-existent device profile!"); } @Test @@ -389,9 +384,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM); firmware.setData(DATA); - thrown.expect(DataValidationException.class); - thrown.expectMessage("OtaPackage checksum should be specified!"); - otaPackageService.saveOtaPackage(firmware); + assertThatThrownBy(() -> otaPackageService.saveOtaPackage(firmware)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("OtaPackage checksum should be specified!"); } @Test @@ -411,17 +406,17 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { newFirmwareInfo.setTitle(TITLE); newFirmwareInfo.setVersion(VERSION); - thrown.expect(DataValidationException.class); - thrown.expectMessage("OtaPackage with such title and version already exists!"); - otaPackageService.saveOtaPackageInfo(newFirmwareInfo, false); + assertThatThrownBy(() -> otaPackageService.saveOtaPackageInfo(newFirmwareInfo, false)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("OtaPackage with such title and version already exists!"); } @Test public void testSaveFirmwareWithExistingTitleAndVersion() { createAndSaveFirmware(tenantId, VERSION); - thrown.expect(DataValidationException.class); - thrown.expectMessage("OtaPackage with such title and version already exists!"); - createAndSaveFirmware(tenantId, VERSION); + assertThatThrownBy(() -> createAndSaveFirmware(tenantId, VERSION)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("OtaPackage with such title and version already exists!"); } @Test @@ -436,9 +431,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { Device savedDevice = deviceService.saveDevice(device); try { - thrown.expect(DataValidationException.class); - thrown.expectMessage("The otaPackage referenced by the devices cannot be deleted!"); - otaPackageService.deleteOtaPackage(tenantId, savedFirmware.getId()); + assertThatThrownBy(() -> otaPackageService.deleteOtaPackage(tenantId, savedFirmware.getId())) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("The otaPackage referenced by the devices cannot be deleted!"); } finally { deviceService.deleteDevice(tenantId, savedDevice.getId()); otaPackageService.deleteOtaPackage(tenantId, savedFirmware.getId()); @@ -448,12 +443,12 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { @Test public void testUpdateDeviceProfileId() { OtaPackage savedFirmware = createAndSaveFirmware(tenantId, VERSION); + savedFirmware.setDeviceProfileId(null); try { - thrown.expect(DataValidationException.class); - thrown.expectMessage("Updating otaPackage deviceProfile is prohibited!"); - savedFirmware.setDeviceProfileId(null); - otaPackageService.saveOtaPackage(savedFirmware); + assertThatThrownBy(() -> otaPackageService.saveOtaPackage(savedFirmware)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("Updating otaPackage deviceProfile is prohibited!"); } finally { otaPackageService.deleteOtaPackage(tenantId, savedFirmware.getId()); } @@ -482,9 +477,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { deviceProfileService.saveDeviceProfile(savedDeviceProfile); try { - thrown.expect(DataValidationException.class); - thrown.expectMessage("The otaPackage referenced by the device profile cannot be deleted!"); - otaPackageService.deleteOtaPackage(tenantId, savedFirmware.getId()); + assertThatThrownBy(() -> otaPackageService.deleteOtaPackage(tenantId, savedFirmware.getId())) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("The otaPackage referenced by the device profile cannot be deleted!"); } finally { deviceProfileService.deleteDeviceProfile(tenantId, savedDeviceProfile.getId()); } @@ -636,12 +631,18 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmwareInfo.setType(FIRMWARE); firmwareInfo.setTitle(TITLE); firmwareInfo.setVersion(VERSION); + firmwareInfo.setUrl(" "); - thrown.expect(DataValidationException.class); - thrown.expectMessage("Ota package URL should be specified!"); - otaPackageService.saveOtaPackageInfo(firmwareInfo, true); + assertThatThrownBy(() -> otaPackageService.saveOtaPackageInfo(firmwareInfo, true)) + .as("firmwareInfo url set whitespaces") + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("Ota package URL should be specified!"); + firmwareInfo.setUrl(""); - otaPackageService.saveOtaPackageInfo(firmwareInfo, true); + assertThatThrownBy(() -> otaPackageService.saveOtaPackageInfo(firmwareInfo, true)) + .as("firmwareInfo url is empty") + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("Ota package URL should be specified!"); } @Test @@ -655,12 +656,10 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmwareInfo.setTenantId(tenantId); OtaPackageInfo savedFirmwareInfo = otaPackageService.saveOtaPackageInfo(firmwareInfo, true); - - thrown.expect(DataValidationException.class); - thrown.expectMessage("Updating otaPackage URL is prohibited!"); - savedFirmwareInfo.setUrl("https://newurl.com"); - otaPackageService.saveOtaPackageInfo(savedFirmwareInfo, true); + assertThatThrownBy(() -> otaPackageService.saveOtaPackageInfo(savedFirmwareInfo, true)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("Updating otaPackage URL is prohibited!"); } @Test @@ -673,10 +672,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmwareInfo.setUrl(URL); firmwareInfo.setTenantId(tenantId); - thrown.expect(DataValidationException.class); - thrown.expectMessage("title length must be equal or less than 255"); - - otaPackageService.saveOtaPackageInfo(firmwareInfo, true); + assertThatThrownBy(() -> otaPackageService.saveOtaPackageInfo(firmwareInfo, true)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("title length must be equal or less than 255"); } @Test @@ -687,11 +685,11 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmwareInfo.setUrl(URL); firmwareInfo.setTenantId(tenantId); firmwareInfo.setTitle(TITLE); - firmwareInfo.setVersion(StringUtils.random(257)); - thrown.expectMessage("version length must be equal or less than 255"); - otaPackageService.saveOtaPackageInfo(firmwareInfo, true); + assertThatThrownBy(() -> otaPackageService.saveOtaPackageInfo(firmwareInfo, true)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("version length must be equal or less than 255"); } private OtaPackage createAndSaveFirmware(TenantId tenantId, String version) { From 3841deae247eebd3f6b491449508b73bb21f1943 Mon Sep 17 00:00:00 2001 From: Oleksandra Matviienko Date: Mon, 13 Mar 2023 23:08:19 +0100 Subject: [PATCH 8/8] BaseTbResourceServiceTest, BaseDeviceServiceTest: Rule ExpectedException refactored with AssertJ Exception Assertions --- .../resource/sql/BaseTbResourceServiceTest.java | 13 ++++--------- .../server/dao/service/BaseDeviceServiceTest.java | 13 ++++--------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java index b0282832bc..e2c5985553 100644 --- a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java @@ -19,10 +19,8 @@ import com.datastax.oss.driver.api.core.uuid.Uuids; import org.junit.After; import org.junit.Assert; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.jupiter.api.Assertions; -import org.junit.rules.ExpectedException; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.ResourceType; @@ -46,6 +44,7 @@ import java.util.Base64; import java.util.Collections; import java.util.List; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @DaoSqlTest @@ -116,10 +115,6 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { .andExpect(status().isOk()); } - @SuppressWarnings("deprecation") - @Rule - public ExpectedException thrown = ExpectedException.none(); - @Test public void testSaveResourceWithMaxSumDataSizeOutOfLimit() throws Exception { loginSysAdmin(); @@ -138,9 +133,9 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { Assert.assertEquals(1, resourceService.sumDataSizeByTenantId(tenantId)); try { - thrown.expect(DataValidationException.class); - thrown.expectMessage(String.format("Failed to create the tb resource, files size limit is exhausted %d bytes!", limit)); - createResource("test1", 1 + DEFAULT_FILE_NAME); + assertThatThrownBy(() -> createResource("test1", 1 + DEFAULT_FILE_NAME)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("Failed to create the tb resource, files size limit is exhausted %d bytes!", limit); } finally { defaultTenantProfile.getProfileData().setConfiguration(DefaultTenantProfileConfiguration.builder().maxResourcesInBytes(0).build()); loginSysAdmin(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java index 010f56235f..900eea8a6d 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java @@ -19,10 +19,8 @@ import com.datastax.oss.driver.api.core.uuid.Uuids; import org.junit.After; import org.junit.Assert; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.jupiter.api.Assertions; -import org.junit.rules.ExpectedException; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceInfo; @@ -47,6 +45,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; @@ -72,10 +71,6 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { tenantProfileService.deleteTenantProfiles(anotherTenantId); } - @SuppressWarnings("deprecation") - @Rule - public ExpectedException thrown = ExpectedException.none(); - @Test public void testSaveDevicesWithoutMaxDeviceLimit() { Device device = this.saveDevice(tenantId, "My device"); @@ -244,9 +239,9 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { savedDevice.setFirmwareId(savedFirmware.getId()); - thrown.expect(DataValidationException.class); - thrown.expectMessage("Can't assign firmware with different deviceProfile!"); - deviceService.saveDevice(savedDevice); + assertThatThrownBy(() -> deviceService.saveDevice(savedDevice)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("Can't assign firmware with different deviceProfile!"); } @Test