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 50051c08d5..8a62795863 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 @@ -33,10 +33,12 @@ import org.eclipse.leshan.server.registration.Registration; import org.junit.After; import org.junit.Assert; import org.junit.Before; +import org.junit.jupiter.api.TestInstance; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.http.HttpStatus; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; @@ -120,11 +122,13 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfil import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; import static org.thingsboard.server.transport.lwm2m.ota.AbstractOtaLwM2MIntegrationTest.CLIENT_LWM2M_SETTINGS_19; -@TestPropertySource(properties = { - "transport.lwm2m.enabled=true", -}) @Slf4j @DaoSqlTest +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@TestPropertySource(properties = { + "transport.lwm2m.enabled=true" +}) public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportIntegrationTest { @SpyBean @@ -317,9 +321,16 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte @After public void after() throws Exception { this.clientDestroy(true); + if (executor != null && !executor.isShutdown()) { executor.shutdownNow(); + if (!executor.awaitTermination(2, TimeUnit.SECONDS)) { + log.warn("⚠️ Executor did not terminate cleanly, forcing GC"); + } } + Thread.sleep(300); + System.gc(); + log.info("✅ Test teardown completed: {}", this.getClass().getSimpleName()); } private void init() throws Exception { 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 4d6265c393..c368dbfe54 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 @@ -69,6 +69,7 @@ import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.Field; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.HashMap; @@ -153,29 +154,30 @@ public class LwM2MTestClient { if (securityBs != null && security != null) { // SECURITIES - securityBs.setId(0); - security.setId(1); + forceNullSecurityId(securityBs); + forceNullSecurityId(security); LwM2mInstanceEnabler[] instances = new LwM2mInstanceEnabler[]{securityBs, security}; initializer.setInstancesForObject(SECURITY, instances); + log.warn("Security BS section: securityBsId [{}] Security Lwm2m section: securityLwm2mId [{}] ", securityBs.getId(), security.getId()); // SERVER Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60)); - lwm2mServer.setId(0); instances = new LwM2mInstanceEnabler[]{lwm2mServer}; - initializer.setInstancesForObject(SERVER, instances); } else if (securityBs != null) { // SECURITY - initializer.setClassForObject(SERVER, Server.class); +; forceNullSecurityId(securityBs); initializer.setInstancesForObject(SECURITY, securityBs); - log.warn("Security section: securityBsId [{}] ", securityBs.getId()); + // SERVER + initializer.setClassForObject(SERVER, Server.class); + log.warn("Security BS section: securityBsId [{}] ", securityBs.getId()); } else { // SECURITY - security.setId(0); + forceNullSecurityId(security); initializer.setInstancesForObject(SECURITY, security); // SERVER Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60)); - lwm2mServer.setId(0); initializer.setInstancesForObject(SERVER, lwm2mServer); + log.warn("Security Lwm2m section: securityLwm2mId [{}] Server Lwm2m section: securityLwm2mId [{}] ", security.getId(), lwm2mServer.getId()); } initializer.setInstancesForObject(DEVICE, lwM2MDevice = new SimpleLwM2MDevice(executor, value3_0_9)); @@ -494,5 +496,29 @@ public class LwM2MTestClient { LwM2mModel model = new StaticModel(models); return new ObjectsInitializer(model); } + + private void forceNullSecurityId(Security securityBs) { + if (securityBs == null) { + return; + } + try { + Field field = securityBs.getClass().getDeclaredField("id"); + field.setAccessible(true); + field.set(securityBs, null); + log.info("[forceNullSecurityId] Set id=null for {}", securityBs); + } catch (NoSuchFieldException e) { + try { + // Якщо поле в батьківському класі (наприклад SecurityObjectInstance) + Field field = securityBs.getClass().getSuperclass().getDeclaredField("id"); + field.setAccessible(true); + field.set(securityBs, null); + log.info("[forceNullSecurityId] Set id=null for {} (via superclass)", securityBs); + } catch (Exception ex) { + log.error("[forceNullSecurityId] Field 'id' not found for {}", securityBs.getClass(), ex); + } + } catch (Exception e) { + log.error("[forceNullSecurityId] Failed to set id=null for {}", securityBs.getClass(), e); + } + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/Lwm2mServerIdentifier.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/Lwm2mServerIdentifier.java new file mode 100644 index 0000000000..a9f81ab655 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/Lwm2mServerIdentifier.java @@ -0,0 +1,137 @@ +/** + * 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.device.credentials.lwm2m; + +/** + * Enum representing predefined LwM2M Short Server Identifiers. + *
+ * See OMA Lightweight M2M Specification for details about the server identifier space.
+ */
+public enum Lwm2mServerIdentifier {
+
+ /**
+ * Bootstrap Short Server ID (0).
+ * Reserved for the Bootstrap Server — used exclusively during the bootstrap phase.
+ */
+ BOOTSTRAP(0, "Bootstrap Short Server ID", true),
+
+ /**
+ * Primary LwM2M Server Short Server ID (1).
+ * Upper boundary for valid LwM2M Server Identifiers (1–65534).
+ */
+ PRIMARY_LWM2M_SERVER(1, "LwM2M Server Short Server ID", false),
+
+ /**
+ * Maximum valid LwM2M Server ID (65534).
+ * Upper boundary for valid LwM2M Server Identifiers (1–65534).
+ */
+ LWM2M_SERVER_MAX(65534, "LwM2M Server Short Server ID", false),
+
+ /**
+ * Not used for identifying an LwM2M Server (65535).
+ * Reserved sentinel value representing "no server associated" or "invalid ID".
+ * MUST NOT be assigned to any LwM2M Server according to OMA-TS-LightweightM2M-Core, §6.2.1.
+ * OMA LwM2M Core / v1.2: Server / Short Server ID): «MAX_ID 65535 is a reserved value and MUST NOT be used for identifying an Object»
+ */
+ NOT_USED_IDENTIFYING_LWM2M_SERVER(65535, "Reserved sentinel value (no active server)", false);
+
+ private final int id;
+ private final String description;
+ private final boolean isBootstrap;
+
+ Lwm2mServerIdentifier(int id, String description, boolean isBootstrap) {
+ this.id = id;
+ this.description = description;
+ this.isBootstrap = isBootstrap;
+ }
+
+ /**
+ * @return the integer value of this Short Server ID.
+ */
+ public int getId() {
+ return id;
+ }
+
+ /**
+ * @return a human-readable description of this Server ID.
+ */
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * @return true if this ID represents a Bootstrap Server.
+ */
+ public boolean isBootstrap() {
+ return isBootstrap;
+ }
+
+ /**
+ * Checks whether a given numeric ID belongs to the Bootstrap Server (0).
+ * OMA Spec (LwM2M v1.0 / v1.1):
+ * Short Server ID Resource (Resource ID: 0)
+ * The Short Server ID identifies a Server Object Instance.
+ * The value 0 is reserved for the Bootstrap Server.
+ * A value between 1 and 65534 identifies a LwM2M Server.
+ * The value 65535 MUST NOT be used.
+ * @param id Short Server ID value.
+ * @return true if id == 0.
+ */
+ public static boolean isBootstrap(int id) {
+ return id == BOOTSTRAP.id;
+ }
+
+ /**
+ * Checks whether a given ID represents a valid LwM2M Server (1–65534).
+ *
+ * @param id Short Server ID value.
+ * @return true if the ID belongs to a standard LwM2M Server.
+ */
+ public static boolean isLwm2mServer(int id) {
+ return id >= PRIMARY_LWM2M_SERVER.id && id <= LWM2M_SERVER_MAX.id;
+ }
+
+ /**
+ * Checks whether the provided ID is within the valid LwM2M range [0–65535].
+ *
+ * @param id ID to check.
+ * @return true if valid, false otherwise.
+ */
+ public static boolean isValid(int id) {
+ return id >= 0 && id <= 65535;
+ }
+
+ /**
+ * Returns a {@link Lwm2mServerIdentifier} instance matching the given ID.
+ *
+ * @param id numeric ID.
+ * @return corresponding enum constant.
+ * @throws IllegalArgumentException if no constant matches the given ID.
+ */
+ public static Lwm2mServerIdentifier fromId(int id) {
+ for (Lwm2mServerIdentifier s : values()) {
+ if (s.id == id) {
+ return s;
+ }
+ }
+ throw new IllegalArgumentException("Unknown Lwm2mServerIdentifier: " + id);
+ }
+
+ @Override
+ public String toString() {
+ return name() + "(" + id + ") - " + description;
+ }
+}
diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java
index 8e5c1cf4bf..33d1fb207c 100644
--- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java
+++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java
@@ -48,9 +48,9 @@ import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL;
import static org.eclipse.leshan.core.LwM2mId.SECURITY;
import static org.eclipse.leshan.core.LwM2mId.SERVER;
import static org.eclipse.leshan.server.bootstrap.BootstrapUtil.toWriteRequest;
-import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.BOOTSTRAP_DEFAULT_SHORT_ID_0;
-import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.LWM2M_DEFAULT_SHORT_ID_1;
-import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.LWM2M_DEFAULT_SHORT_ID_65534;
+import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.BOOTSTRAP;
+import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.LWM2M_SERVER_MAX;
+import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.PRIMARY_LWM2M_SERVER;
@Slf4j
public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTaskProvider {
@@ -147,7 +147,7 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask
log.error("Invalid lwm2mSecurityInstance [{}] by short server id [{}]", path.getObjectInstanceId(), lwm2mShortServerId);
}
} else {
- this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().putIfAbsent(BOOTSTRAP_DEFAULT_SHORT_ID_0, path.getObjectInstanceId());
+ this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().putIfAbsent(BOOTSTRAP.getId(), path.getObjectInstanceId());
}
} else if (path.getObjectId() == 1) {
if (link.getAttributes().get("ssid") != null) {
@@ -192,7 +192,7 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask
* SECURITY = 0; InstanceId = 0
* SERVER = 1; InstanceId = 0
* 2) Both
- * - Short Server ID == 0 or 65535 bs)
+ * - Short Server ID == 0 bs)
* SECURITY = 0; InstanceId = 0
* SERVER = 1; InstanceId = null
* - Short Server ID == 1 - 65534 lwm2m)
@@ -202,8 +202,8 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask
public List