From a0f5b3bc5b6496dcab4248b15a038a39766b45ae Mon Sep 17 00:00:00 2001 From: Oleksandra Matviienko Date: Tue, 9 Jun 2026 09:53:57 +0200 Subject: [PATCH] Move Californium server construction into init try/catch and guard CoAP shutdown Construct CoapServer and the LwM2M bootstrap server inside the init try block so a failure in the constructor or build() is cleaned up by the existing catch. Guard CoAP shutdown() against a null server. Add a DTLS-enabled CoAP test that covers the dtlsSessionsExecutor shutdown branch. --- .../coapserver/DefaultCoapServerService.java | 6 +- .../DefaultCoapServerServiceTest.java | 66 +++++++++++++++++++ .../LwM2MTransportBootstrapService.java | 7 +- 3 files changed, 75 insertions(+), 4 deletions(-) diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java index 081f1a9db5..8ffa489f7b 100644 --- a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java @@ -85,7 +85,9 @@ public class DefaultCoapServerService implements CoapServerService, SmartInitial dtlsSessionsExecutor.shutdownNow(); } log.info("Stopping CoAP server!"); - server.destroy(); + if (server != null) { + server.destroy(); + } log.info("CoAP server stopped!"); } @@ -105,8 +107,8 @@ public class DefaultCoapServerService implements CoapServerService, SmartInitial private CoapServer createCoapServer() throws UnknownHostException { Configuration networkConfig = createNetworkConfiguration(); - server = new CoapServer(networkConfig); try { + server = new CoapServer(networkConfig); CoapEndpoint.Builder noSecCoapEndpointBuilder = new CoapEndpoint.Builder(); InetAddress addr = InetAddress.getByName(coapServerContext.getHost()); InetSocketAddress sockAddr = new InetSocketAddress(addr, coapServerContext.getPort()); diff --git a/common/coap-server/src/test/java/org/thingsboard/server/coapserver/DefaultCoapServerServiceTest.java b/common/coap-server/src/test/java/org/thingsboard/server/coapserver/DefaultCoapServerServiceTest.java index 5606fe18d7..0c9e9fc29d 100644 --- a/common/coap-server/src/test/java/org/thingsboard/server/coapserver/DefaultCoapServerServiceTest.java +++ b/common/coap-server/src/test/java/org/thingsboard/server/coapserver/DefaultCoapServerServiceTest.java @@ -15,20 +15,36 @@ */ package org.thingsboard.server.coapserver; +import org.eclipse.californium.core.CoapServer; +import org.eclipse.californium.core.network.CoapEndpoint; +import org.eclipse.californium.core.server.resources.Resource; +import org.eclipse.californium.scandium.DTLSConnector; +import org.eclipse.californium.scandium.config.DtlsConnectorConfig; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; +import org.mockito.MockedConstruction; +import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.common.util.ThingsBoardExecutors; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.InetSocketAddress; +import java.util.concurrent.ScheduledExecutorService; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -76,4 +92,54 @@ public class DefaultCoapServerServiceTest { assertThat(ReflectionTestUtils.getField(service, "tbDtlsCertificateVerifier")).isNull(); } + @Test + public void whenDtlsEnabledAndStartFails_thenInitShutsDownDtlsExecutorAndReleasesCoapServer() throws Exception { + // DTLS enabled: the DTLS endpoint is created and dtlsSessionsExecutor is scheduled before server.start(). + // This exercises the catch's dtlsSessionsExecutor.shutdownNow() branch, which the plain-bind test does not. + TbCoapDtlsSettings mockDtlsSettings = mock(TbCoapDtlsSettings.class); + when(mockCoapServerContext.getDtlsSettings()).thenReturn(mockDtlsSettings); + + DtlsConnectorConfig mockDtlsConfig = mock(DtlsConnectorConfig.class); + when(mockDtlsConfig.getAddress()).thenReturn(new InetSocketAddress(InetAddress.getByName(HOST), occupiedPort + 1)); + TbCoapDtlsCertificateVerifier mockVerifier = mock(TbCoapDtlsCertificateVerifier.class); + when(mockVerifier.getDtlsSessionReportTimeout()).thenReturn(1800000L); + when(mockDtlsConfig.getAdvancedCertificateVerifier()).thenReturn(mockVerifier); + when(mockDtlsSettings.dtlsConnectorConfig(any())).thenReturn(mockDtlsConfig); + + ScheduledExecutorService mockExecutor = mock(ScheduledExecutorService.class); + Resource mockRoot = mock(Resource.class); + + try (MockedStatic executorsStatic = mockStatic(ThingsBoardExecutors.class); + MockedConstruction serverMock = mockConstruction(CoapServer.class, (server, ctx) -> { + when(server.getRoot()).thenReturn(mockRoot); + doThrow(new IllegalStateException("None of the server endpoints could be started")).when(server).start(); + }); + MockedConstruction dtlsMock = mockConstruction(DTLSConnector.class); + MockedConstruction builderMock = mockConstruction(CoapEndpoint.Builder.class, (builder, ctx) -> { + when(builder.setInetSocketAddress(any())).thenReturn(builder); + when(builder.setConfiguration(any())).thenReturn(builder); + when(builder.setConnector(any(DTLSConnector.class))).thenReturn(builder); + when(builder.build()).thenReturn(mock(CoapEndpoint.class)); + })) { + + executorsStatic.when(() -> ThingsBoardExecutors.newSingleThreadScheduledExecutor(anyString())).thenReturn(mockExecutor); + + assertThatThrownBy(() -> service.init()) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("None of the server endpoints could be started"); + + // DTLS branch was actually entered and the executor was created... + verify(mockDtlsSettings).dtlsConnectorConfig(any()); + // ...and the cleanup branch shut it down and destroyed the server. + verify(mockExecutor).shutdownNow(); + verify(serverMock.constructed().get(0)).destroy(); + } + + assertThat(ReflectionTestUtils.getField(service, "server")).isNull(); + assertThat(ReflectionTestUtils.getField(service, "dtlsSessionsExecutor")).isNull(); + assertThat(ReflectionTestUtils.getField(service, "dtlsConnector")).isNull(); + assertThat(ReflectionTestUtils.getField(service, "dtlsCoapEndpoint")).isNull(); + assertThat(ReflectionTestUtils.getField(service, "tbDtlsCertificateVerifier")).isNull(); + } + } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java index bf2b48dd61..639e0bf74a 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java @@ -82,15 +82,18 @@ public class LwM2MTransportBootstrapService implements SmartInitializingSingleto @PostConstruct public void init() { log.info("Starting LwM2M transport bootstrap server..."); - LeshanBootstrapServer bootstrapServer = getLhBootstrapServer(); + LeshanBootstrapServer bootstrapServer = null; try { + bootstrapServer = getLhBootstrapServer(); this.server = bootstrapServer; bootstrapServer.start(); log.info("Started LwM2M transport bootstrap server."); } catch (RuntimeException e) { log.error("Failed to start LwM2M transport bootstrap server, releasing resources", e); try { - bootstrapServer.destroy(); + if (bootstrapServer != null) { + bootstrapServer.destroy(); + } } catch (Exception suppressed) { e.addSuppressed(suppressed); } finally {