45 changed files with 1870 additions and 157 deletions
@ -0,0 +1,143 @@ |
|||
/** |
|||
* 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.coapserver; |
|||
|
|||
import org.eclipse.californium.core.CoapServer; |
|||
import org.eclipse.californium.core.network.CoapEndpoint; |
|||
import org.eclipse.californium.scandium.DTLSConnector; |
|||
import org.junit.jupiter.api.BeforeEach; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.api.extension.ExtendWith; |
|||
import org.mockito.ArgumentCaptor; |
|||
import org.mockito.Mock; |
|||
import org.mockito.junit.jupiter.MockitoExtension; |
|||
import org.mockito.junit.jupiter.MockitoSettings; |
|||
import org.mockito.quality.Strictness; |
|||
import org.springframework.test.util.ReflectionTestUtils; |
|||
|
|||
import java.util.List; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
import static org.mockito.ArgumentMatchers.any; |
|||
import static org.mockito.Mockito.doAnswer; |
|||
import static org.mockito.Mockito.mock; |
|||
import static org.mockito.Mockito.never; |
|||
import static org.mockito.Mockito.verify; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
@ExtendWith(MockitoExtension.class) |
|||
@MockitoSettings(strictness = Strictness.LENIENT) |
|||
public class CoapDtlsCertificateReloadTest { |
|||
|
|||
@Mock |
|||
private CoapServerContext mockCoapServerContext; |
|||
|
|||
@Mock |
|||
private TbCoapDtlsSettings mockDtlsSettings; |
|||
|
|||
@Mock |
|||
private CoapServer mockCoapServer; |
|||
|
|||
@Mock |
|||
private CoapEndpoint mockDtlsEndpoint; |
|||
|
|||
@Mock |
|||
private DTLSConnector mockDtlsConnector; |
|||
|
|||
private DefaultCoapServerService coapServerService; |
|||
|
|||
@BeforeEach |
|||
public void setup() { |
|||
coapServerService = new DefaultCoapServerService(); |
|||
ReflectionTestUtils.setField(coapServerService, "coapServerContext", mockCoapServerContext); |
|||
|
|||
when(mockCoapServerContext.getHost()).thenReturn("localhost"); |
|||
when(mockCoapServerContext.getPort()).thenReturn(5683); |
|||
doAnswer(invocation -> { |
|||
invocation.getArgument(0); |
|||
return null; |
|||
}).when(mockDtlsSettings).registerReloadCallback(any()); |
|||
} |
|||
|
|||
@Test |
|||
public void givenDtlsEnabled_whenRegisterCertificateReloadCallback_thenShouldRegisterCallback() { |
|||
when(mockCoapServerContext.getDtlsSettings()).thenReturn(mockDtlsSettings); |
|||
|
|||
ReflectionTestUtils.setField(coapServerService, "server", mockCoapServer); |
|||
|
|||
ReflectionTestUtils.invokeMethod(coapServerService, "afterSingletonsInstantiated"); |
|||
|
|||
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
verify(mockDtlsSettings).registerReloadCallback(callbackCaptor.capture()); |
|||
assertThat(callbackCaptor.getValue()).isNotNull(); |
|||
} |
|||
|
|||
@Test |
|||
public void givenDtlsNotEnabled_whenRegisterCertificateReloadCallback_thenShouldNotRegisterCallback() { |
|||
when(mockCoapServerContext.getDtlsSettings()).thenReturn(null); |
|||
|
|||
ReflectionTestUtils.invokeMethod(coapServerService, "afterSingletonsInstantiated"); |
|||
|
|||
verify(mockDtlsSettings, never()).registerReloadCallback(any()); |
|||
} |
|||
|
|||
@Test |
|||
public void givenReloadCallbackInvoked_whenDtlsEndpointExists_thenShouldRecreateDtlsEndpoint() { |
|||
when(mockCoapServerContext.getDtlsSettings()).thenReturn(mockDtlsSettings); |
|||
|
|||
ReflectionTestUtils.setField(coapServerService, "server", mockCoapServer); |
|||
ReflectionTestUtils.setField(coapServerService, "dtlsCoapEndpoint", mockDtlsEndpoint); |
|||
ReflectionTestUtils.setField(coapServerService, "dtlsConnector", mockDtlsConnector); |
|||
|
|||
when(mockCoapServer.getEndpoints()).thenReturn(mock(List.class)); |
|||
|
|||
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
ReflectionTestUtils.invokeMethod(coapServerService, "afterSingletonsInstantiated"); |
|||
verify(mockDtlsSettings).registerReloadCallback(callbackCaptor.capture()); |
|||
|
|||
Runnable reloadCallback = callbackCaptor.getValue(); |
|||
assertThat(reloadCallback).isNotNull(); |
|||
} |
|||
|
|||
@Test |
|||
public void givenDtlsEnabled_whenInit_thenShouldRegisterCallback() { |
|||
when(mockCoapServerContext.getDtlsSettings()).thenReturn(mockDtlsSettings); |
|||
when(mockCoapServerContext.getHost()).thenReturn("localhost"); |
|||
when(mockCoapServerContext.getPort()).thenReturn(5683); |
|||
|
|||
ReflectionTestUtils.setField(coapServerService, "server", mockCoapServer); |
|||
ReflectionTestUtils.invokeMethod(coapServerService, "afterSingletonsInstantiated"); |
|||
|
|||
verify(mockDtlsSettings).registerReloadCallback(any(Runnable.class)); |
|||
} |
|||
|
|||
@Test |
|||
public void givenReloadCallback_whenInvokedMultipleTimes_thenShouldHandleGracefully() { |
|||
when(mockCoapServerContext.getDtlsSettings()).thenReturn(mockDtlsSettings); |
|||
ReflectionTestUtils.setField(coapServerService, "server", mockCoapServer); |
|||
ReflectionTestUtils.setField(coapServerService, "dtlsCoapEndpoint", mockDtlsEndpoint); |
|||
ReflectionTestUtils.setField(coapServerService, "dtlsConnector", mockDtlsConnector); |
|||
|
|||
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
ReflectionTestUtils.invokeMethod(coapServerService, "afterSingletonsInstantiated"); |
|||
verify(mockDtlsSettings).registerReloadCallback(callbackCaptor.capture()); |
|||
|
|||
Runnable reloadCallback = callbackCaptor.getValue(); |
|||
|
|||
assertThat(reloadCallback).isNotNull(); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,207 @@ |
|||
/** |
|||
* 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.transport.lwm2m.bootstrap; |
|||
|
|||
import org.eclipse.leshan.server.bootstrap.LeshanBootstrapServer; |
|||
import org.junit.jupiter.api.BeforeEach; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.api.extension.ExtendWith; |
|||
import org.mockito.ArgumentCaptor; |
|||
import org.mockito.Mock; |
|||
import org.mockito.junit.jupiter.MockitoExtension; |
|||
import org.mockito.junit.jupiter.MockitoSettings; |
|||
import org.mockito.quality.Strictness; |
|||
import org.springframework.test.util.ReflectionTestUtils; |
|||
import org.thingsboard.server.common.transport.TransportService; |
|||
import org.thingsboard.server.common.transport.config.ssl.SslCredentials; |
|||
import org.thingsboard.server.transport.lwm2m.bootstrap.secure.TbLwM2MDtlsBootstrapCertificateVerifier; |
|||
import org.thingsboard.server.transport.lwm2m.bootstrap.store.LwM2MBootstrapSecurityStore; |
|||
import org.thingsboard.server.transport.lwm2m.bootstrap.store.LwM2MInMemoryBootstrapConfigStore; |
|||
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportBootstrapConfig; |
|||
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
import static org.mockito.Mockito.mock; |
|||
import static org.mockito.Mockito.times; |
|||
import static org.mockito.Mockito.verify; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
@ExtendWith(MockitoExtension.class) |
|||
@MockitoSettings(strictness = Strictness.LENIENT) |
|||
public class LwM2mBootstrapCertificateReloadTest { |
|||
|
|||
@Mock |
|||
private LwM2MTransportServerConfig mockServerConfig; |
|||
|
|||
@Mock |
|||
private LwM2MTransportBootstrapConfig mockBootstrapConfig; |
|||
|
|||
@Mock |
|||
private LwM2MBootstrapSecurityStore mockSecurityStore; |
|||
|
|||
@Mock |
|||
private LwM2MInMemoryBootstrapConfigStore mockConfigStore; |
|||
|
|||
@Mock |
|||
private TransportService mockTransportService; |
|||
|
|||
@Mock |
|||
private TbLwM2MDtlsBootstrapCertificateVerifier mockCertificateVerifier; |
|||
|
|||
@Mock |
|||
private LeshanBootstrapServer mockBootstrapServer; |
|||
|
|||
@Mock |
|||
private SslCredentials mockSslCredentials; |
|||
|
|||
private LwM2MTransportBootstrapService bootstrapService; |
|||
|
|||
@BeforeEach |
|||
public void setup() { |
|||
bootstrapService = new LwM2MTransportBootstrapService( |
|||
mockServerConfig, |
|||
mockBootstrapConfig, |
|||
mockSecurityStore, |
|||
mockConfigStore, |
|||
mockTransportService, |
|||
mockCertificateVerifier |
|||
); |
|||
|
|||
when(mockBootstrapConfig.getHost()).thenReturn("localhost"); |
|||
when(mockBootstrapConfig.getPort()).thenReturn(5687); |
|||
when(mockBootstrapConfig.getSecureHost()).thenReturn("localhost"); |
|||
when(mockBootstrapConfig.getSecurePort()).thenReturn(5688); |
|||
when(mockBootstrapConfig.getSslCredentials()).thenReturn(mockSslCredentials); |
|||
when(mockServerConfig.getDtlsRetransmissionTimeout()).thenReturn(9000); |
|||
} |
|||
|
|||
@Test |
|||
public void givenInit_whenCalled_thenShouldRegisterCertificateReloadCallback() { |
|||
ReflectionTestUtils.setField(bootstrapService, "server", mockBootstrapServer); |
|||
|
|||
bootstrapService.afterSingletonsInstantiated(); |
|||
|
|||
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
verify(mockBootstrapConfig).registerServerReloadCallback(callbackCaptor.capture()); |
|||
|
|||
assertThat(callbackCaptor.getValue()).isNotNull(); |
|||
} |
|||
|
|||
@Test |
|||
public void givenReloadCallback_whenInvoked_thenShouldRecreateBootstrapServer() { |
|||
LeshanBootstrapServer firstServer = mock(LeshanBootstrapServer.class); |
|||
LeshanBootstrapServer secondServer = mock(LeshanBootstrapServer.class); |
|||
|
|||
ReflectionTestUtils.setField(bootstrapService, "server", firstServer); |
|||
|
|||
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
bootstrapService.afterSingletonsInstantiated(); |
|||
verify(mockBootstrapConfig).registerServerReloadCallback(callbackCaptor.capture()); |
|||
|
|||
Runnable reloadCallback = callbackCaptor.getValue(); |
|||
assertThat(reloadCallback).isNotNull(); |
|||
|
|||
reloadCallback.run(); |
|||
|
|||
verify(firstServer).destroy(); |
|||
verify(secondServer, times(0)).destroy(); |
|||
} |
|||
|
|||
@Test |
|||
public void givenBootstrapServerExists_whenRecreate_thenShouldDestroyOldServer() { |
|||
LeshanBootstrapServer oldServer = mock(LeshanBootstrapServer.class); |
|||
ReflectionTestUtils.setField(bootstrapService, "server", oldServer); |
|||
|
|||
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
bootstrapService.afterSingletonsInstantiated(); |
|||
verify(mockBootstrapConfig).registerServerReloadCallback(callbackCaptor.capture()); |
|||
|
|||
Runnable reloadCallback = callbackCaptor.getValue(); |
|||
reloadCallback.run(); |
|||
|
|||
verify(oldServer).destroy(); |
|||
} |
|||
|
|||
@Test |
|||
public void givenMultipleReloads_whenInvoked_thenShouldHandleSequentially() { |
|||
LeshanBootstrapServer firstServer = mock(LeshanBootstrapServer.class); |
|||
LeshanBootstrapServer secondServer = mock(LeshanBootstrapServer.class); |
|||
LeshanBootstrapServer thirdServer = mock(LeshanBootstrapServer.class); |
|||
|
|||
ReflectionTestUtils.setField(bootstrapService, "server", firstServer); |
|||
|
|||
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
bootstrapService.afterSingletonsInstantiated(); |
|||
verify(mockBootstrapConfig).registerServerReloadCallback(callbackCaptor.capture()); |
|||
|
|||
Runnable reloadCallback = callbackCaptor.getValue(); |
|||
|
|||
reloadCallback.run(); |
|||
verify(firstServer, times(1)).destroy(); |
|||
|
|||
ReflectionTestUtils.setField(bootstrapService, "server", secondServer); |
|||
reloadCallback.run(); |
|||
verify(secondServer, times(1)).destroy(); |
|||
|
|||
ReflectionTestUtils.setField(bootstrapService, "server", thirdServer); |
|||
reloadCallback.run(); |
|||
verify(thirdServer, times(1)).destroy(); |
|||
} |
|||
|
|||
@Test |
|||
public void givenNullServer_whenRecreate_thenShouldHandleGracefully() { |
|||
ReflectionTestUtils.setField(bootstrapService, "server", null); |
|||
|
|||
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
bootstrapService.afterSingletonsInstantiated(); |
|||
verify(mockBootstrapConfig).registerServerReloadCallback(callbackCaptor.capture()); |
|||
|
|||
Runnable reloadCallback = callbackCaptor.getValue(); |
|||
|
|||
assertThat(reloadCallback).isNotNull(); |
|||
reloadCallback.run(); |
|||
} |
|||
|
|||
@Test |
|||
public void givenCertificateUpdate_whenRecreate_thenShouldUseNewCredentials() { |
|||
SslCredentials oldCredentials = mockSslCredentials; |
|||
SslCredentials newCredentials = mock(SslCredentials.class); |
|||
|
|||
when(mockBootstrapConfig.getSslCredentials()).thenReturn(oldCredentials).thenReturn(newCredentials); |
|||
|
|||
SslCredentials firstCall = mockBootstrapConfig.getSslCredentials(); |
|||
assertThat(firstCall).isEqualTo(oldCredentials); |
|||
|
|||
SslCredentials secondCall = mockBootstrapConfig.getSslCredentials(); |
|||
assertThat(secondCall).isEqualTo(newCredentials); |
|||
|
|||
verify(mockBootstrapConfig, times(2)).getSslCredentials(); |
|||
} |
|||
|
|||
@Test |
|||
public void givenReloadCallback_whenInvokedWithException_thenShouldLogError() { |
|||
LeshanBootstrapServer faultyServer = mock(LeshanBootstrapServer.class); |
|||
ReflectionTestUtils.setField(bootstrapService, "server", faultyServer); |
|||
|
|||
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
bootstrapService.afterSingletonsInstantiated(); |
|||
verify(mockBootstrapConfig).registerServerReloadCallback(callbackCaptor.capture()); |
|||
|
|||
Runnable reloadCallback = callbackCaptor.getValue(); |
|||
assertThat(reloadCallback).isNotNull(); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,183 @@ |
|||
/** |
|||
* 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.transport.lwm2m.server; |
|||
|
|||
import org.eclipse.leshan.server.LeshanServer; |
|||
import org.eclipse.leshan.server.observation.ObservationService; |
|||
import org.eclipse.leshan.server.registration.RegistrationService; |
|||
import org.eclipse.leshan.server.registration.RegistrationStore; |
|||
import org.eclipse.leshan.server.send.SendService; |
|||
import org.junit.jupiter.api.BeforeEach; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.api.extension.ExtendWith; |
|||
import org.mockito.ArgumentCaptor; |
|||
import org.mockito.Mock; |
|||
import org.mockito.junit.jupiter.MockitoExtension; |
|||
import org.mockito.junit.jupiter.MockitoSettings; |
|||
import org.mockito.quality.Strictness; |
|||
import org.springframework.test.util.ReflectionTestUtils; |
|||
import org.thingsboard.server.cache.ota.OtaPackageDataCache; |
|||
import org.thingsboard.server.common.transport.config.ssl.SslCredentials; |
|||
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.LwM2mUplinkMsgHandler; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
import static org.mockito.Mockito.times; |
|||
import static org.mockito.Mockito.verify; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
@ExtendWith(MockitoExtension.class) |
|||
@MockitoSettings(strictness = Strictness.LENIENT) |
|||
public class LwM2mServerCertificateReloadTest { |
|||
|
|||
@Mock |
|||
private LwM2mTransportContext mockContext; |
|||
|
|||
@Mock |
|||
private LwM2MTransportServerConfig mockConfig; |
|||
|
|||
@Mock |
|||
private OtaPackageDataCache mockOtaCache; |
|||
|
|||
@Mock |
|||
private LwM2mUplinkMsgHandler mockHandler; |
|||
|
|||
@Mock |
|||
private RegistrationStore mockRegistrationStore; |
|||
|
|||
@Mock |
|||
private TbSecurityStore mockSecurityStore; |
|||
|
|||
@Mock |
|||
private TbLwM2MDtlsCertificateVerifier mockCertificateVerifier; |
|||
|
|||
@Mock |
|||
private TbLwM2MAuthorizer mockAuthorizer; |
|||
|
|||
@Mock |
|||
private LwM2mVersionedModelProvider mockModelProvider; |
|||
|
|||
@Mock |
|||
private LeshanServer mockLeshanServer; |
|||
|
|||
@Mock |
|||
private RegistrationService mockRegistrationService; |
|||
|
|||
@Mock |
|||
private ObservationService mockObservationService; |
|||
|
|||
@Mock |
|||
private SendService mockSendService; |
|||
|
|||
@Mock |
|||
private SslCredentials mockSslCredentials; |
|||
|
|||
private DefaultLwM2mTransportService lwm2mTransportService; |
|||
|
|||
@BeforeEach |
|||
public void setup() { |
|||
lwm2mTransportService = new DefaultLwM2mTransportService( |
|||
mockContext, |
|||
mockConfig, |
|||
mockOtaCache, |
|||
mockHandler, |
|||
mockRegistrationStore, |
|||
mockSecurityStore, |
|||
mockCertificateVerifier, |
|||
mockAuthorizer, |
|||
mockModelProvider |
|||
); |
|||
|
|||
when(mockConfig.getHost()).thenReturn("localhost"); |
|||
when(mockConfig.getPort()).thenReturn(5683); |
|||
when(mockConfig.getSecureHost()).thenReturn("localhost"); |
|||
when(mockConfig.getSecurePort()).thenReturn(5684); |
|||
when(mockConfig.getSslCredentials()).thenReturn(mockSslCredentials); |
|||
|
|||
when(mockLeshanServer.getRegistrationService()).thenReturn(mockRegistrationService); |
|||
when(mockLeshanServer.getObservationService()).thenReturn(mockObservationService); |
|||
when(mockLeshanServer.getSendService()).thenReturn(mockSendService); |
|||
} |
|||
|
|||
@Test |
|||
public void givenRegisterCertificateReloadCallback_whenInvoked_thenShouldRegisterCallback() { |
|||
lwm2mTransportService.afterSingletonsInstantiated(); |
|||
|
|||
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
verify(mockConfig).registerServerReloadCallback(callbackCaptor.capture()); |
|||
assertThat(callbackCaptor.getValue()).isNotNull(); |
|||
} |
|||
|
|||
@Test |
|||
public void givenReloadCallback_whenInvoked_thenShouldTriggerServerRecreation() { |
|||
lwm2mTransportService.afterSingletonsInstantiated(); |
|||
|
|||
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
verify(mockConfig).registerServerReloadCallback(callbackCaptor.capture()); |
|||
Runnable reloadCallback = callbackCaptor.getValue(); |
|||
|
|||
ReflectionTestUtils.setField(lwm2mTransportService, "server", mockLeshanServer); |
|||
|
|||
assertThat(reloadCallback).isNotNull(); |
|||
} |
|||
|
|||
@Test |
|||
public void givenServerWithListeners_whenRecreate_thenShouldRemoveOldListeners() { |
|||
lwm2mTransportService.afterSingletonsInstantiated(); |
|||
|
|||
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
verify(mockConfig).registerServerReloadCallback(callbackCaptor.capture()); |
|||
|
|||
ReflectionTestUtils.setField(lwm2mTransportService, "server", mockLeshanServer); |
|||
|
|||
LwM2mServerListener serverListener = new LwM2mServerListener(mockHandler); |
|||
ReflectionTestUtils.setField(lwm2mTransportService, "serverListener", serverListener); |
|||
|
|||
Runnable reloadCallback = callbackCaptor.getValue(); |
|||
assertThat(reloadCallback).isNotNull(); |
|||
} |
|||
|
|||
@Test |
|||
public void givenMultipleReloadCallbacks_whenInvoked_thenShouldHandleGracefully() { |
|||
lwm2mTransportService.afterSingletonsInstantiated(); |
|||
|
|||
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
verify(mockConfig, times(1)).registerServerReloadCallback(callbackCaptor.capture()); |
|||
|
|||
Runnable reloadCallback = callbackCaptor.getValue(); |
|||
ReflectionTestUtils.setField(lwm2mTransportService, "server", mockLeshanServer); |
|||
|
|||
assertThat(reloadCallback).isNotNull(); |
|||
} |
|||
|
|||
@Test |
|||
public void givenCertificateReload_whenServerNull_thenShouldHandleGracefully() { |
|||
lwm2mTransportService.afterSingletonsInstantiated(); |
|||
|
|||
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
verify(mockConfig).registerServerReloadCallback(callbackCaptor.capture()); |
|||
|
|||
ReflectionTestUtils.setField(lwm2mTransportService, "server", null); |
|||
|
|||
Runnable reloadCallback = callbackCaptor.getValue(); |
|||
|
|||
assertThat(reloadCallback).isNotNull(); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,197 @@ |
|||
/** |
|||
* 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.transport.mqtt; |
|||
|
|||
import io.netty.handler.ssl.SslHandler; |
|||
import org.junit.jupiter.api.BeforeEach; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.api.extension.ExtendWith; |
|||
import org.mockito.ArgumentCaptor; |
|||
import org.mockito.Mock; |
|||
import org.mockito.junit.jupiter.MockitoExtension; |
|||
import org.springframework.test.util.ReflectionTestUtils; |
|||
import org.thingsboard.server.common.transport.TransportService; |
|||
import org.thingsboard.server.common.transport.config.ssl.SslCredentials; |
|||
import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig; |
|||
|
|||
import javax.net.ssl.KeyManager; |
|||
import javax.net.ssl.KeyManagerFactory; |
|||
import javax.net.ssl.SSLContext; |
|||
import javax.net.ssl.TrustManager; |
|||
import javax.net.ssl.TrustManagerFactory; |
|||
import javax.net.ssl.X509TrustManager; |
|||
import java.util.concurrent.CountDownLatch; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
import static org.mockito.Mockito.mock; |
|||
import static org.mockito.Mockito.verify; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
@ExtendWith(MockitoExtension.class) |
|||
public class MqttSslHandlerProviderTest { |
|||
|
|||
@Mock |
|||
private SslCredentialsConfig mockCredentialsConfig; |
|||
|
|||
@Mock |
|||
private SslCredentials mockCredentials; |
|||
|
|||
@Mock |
|||
private TransportService mockTransportService; |
|||
|
|||
private MqttSslHandlerProvider sslHandlerProvider; |
|||
|
|||
@BeforeEach |
|||
public void setup() throws Exception { |
|||
sslHandlerProvider = new MqttSslHandlerProvider(); |
|||
ReflectionTestUtils.setField(sslHandlerProvider, "mqttSslCredentialsConfig", mockCredentialsConfig); |
|||
ReflectionTestUtils.setField(sslHandlerProvider, "transportService", mockTransportService); |
|||
ReflectionTestUtils.setField(sslHandlerProvider, "sslProtocol", "TLSv1.2"); |
|||
|
|||
KeyManagerFactory mockKmf = mock(KeyManagerFactory.class); |
|||
TrustManagerFactory mockTmf = mock(TrustManagerFactory.class); |
|||
X509TrustManager mockTrustManager = mock(X509TrustManager.class); |
|||
|
|||
when(mockCredentialsConfig.getCredentials()).thenReturn(mockCredentials); |
|||
when(mockCredentials.createKeyManagerFactory()).thenReturn(mockKmf); |
|||
when(mockCredentials.createTrustManagerFactory()).thenReturn(mockTmf); |
|||
when(mockKmf.getKeyManagers()).thenReturn(new KeyManager[0]); |
|||
when(mockTmf.getTrustManagers()).thenReturn(new TrustManager[]{mockTrustManager}); |
|||
} |
|||
|
|||
@Test |
|||
public void givenInitialized_whenGetSslHandler_thenShouldCreateSSLContext() { |
|||
sslHandlerProvider.afterSingletonsInstantiated(); |
|||
|
|||
SslHandler handler1 = sslHandlerProvider.getSslHandler(); |
|||
SslHandler handler2 = sslHandlerProvider.getSslHandler(); |
|||
|
|||
assertThat(handler1).isNotNull(); |
|||
assertThat(handler2).isNotNull(); |
|||
assertThat(handler1).isNotSameAs(handler2); |
|||
|
|||
SSLContext context = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); |
|||
assertThat(context).isNotNull(); |
|||
} |
|||
|
|||
@Test |
|||
public void givenCertificatesReloaded_whenGetSslHandler_thenShouldRecreateSSLContext() { |
|||
sslHandlerProvider.afterSingletonsInstantiated(); |
|||
|
|||
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); |
|||
Runnable reloadCallback = callbackCaptor.getValue(); |
|||
|
|||
SslHandler handler1 = sslHandlerProvider.getSslHandler(); |
|||
SSLContext initialContext = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); |
|||
assertThat(initialContext).isNotNull(); |
|||
|
|||
reloadCallback.run(); |
|||
|
|||
assertThat(handler1).isNotNull(); |
|||
SSLContext contextAfterReload = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); |
|||
assertThat(contextAfterReload).isNull(); |
|||
|
|||
SslHandler handler2 = sslHandlerProvider.getSslHandler(); |
|||
SSLContext newContext = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); |
|||
|
|||
assertThat(handler2).isNotNull(); |
|||
assertThat(newContext).isNotNull(); |
|||
assertThat(newContext).isNotSameAs(initialContext); |
|||
} |
|||
|
|||
@Test |
|||
public void givenConcurrentGetSslHandlerCalls_whenSSLContextNull_thenShouldCreateOnlyOnce() throws Exception { |
|||
sslHandlerProvider.afterSingletonsInstantiated(); |
|||
|
|||
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); |
|||
callbackCaptor.getValue().run(); |
|||
|
|||
CountDownLatch startLatch = new CountDownLatch(1); |
|||
CountDownLatch doneLatch = new CountDownLatch(5); |
|||
|
|||
for (int i = 0; i < 5; i++) { |
|||
new Thread(() -> { |
|||
try { |
|||
startLatch.await(); |
|||
SslHandler handler = sslHandlerProvider.getSslHandler(); |
|||
assertThat(handler).isNotNull(); |
|||
} catch (Exception e) { |
|||
throw new RuntimeException(e); |
|||
} finally { |
|||
doneLatch.countDown(); |
|||
} |
|||
}).start(); |
|||
} |
|||
|
|||
startLatch.countDown(); |
|||
boolean completed = doneLatch.await(5, TimeUnit.SECONDS); |
|||
|
|||
assertThat(completed).isTrue(); |
|||
SSLContext context = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); |
|||
assertThat(context).isNotNull(); |
|||
} |
|||
|
|||
@Test |
|||
public void givenReloadCallback_whenInvoked_thenShouldInvalidateSSLContext() { |
|||
sslHandlerProvider.afterSingletonsInstantiated(); |
|||
|
|||
sslHandlerProvider.getSslHandler(); |
|||
SSLContext initialContext = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); |
|||
assertThat(initialContext).isNotNull(); |
|||
|
|||
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); |
|||
|
|||
callbackCaptor.getValue().run(); |
|||
|
|||
SSLContext contextAfterReload = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); |
|||
assertThat(contextAfterReload).isNull(); |
|||
} |
|||
|
|||
@Test |
|||
public void givenMultipleReloads_whenGetSslHandler_thenShouldRecreateEachTime() { |
|||
sslHandlerProvider.afterSingletonsInstantiated(); |
|||
|
|||
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); |
|||
Runnable reloadCallback = callbackCaptor.getValue(); |
|||
|
|||
SSLContext context1; |
|||
SSLContext context2; |
|||
SSLContext context3; |
|||
|
|||
sslHandlerProvider.getSslHandler(); |
|||
context1 = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); |
|||
assertThat(context1).isNotNull(); |
|||
|
|||
reloadCallback.run(); |
|||
sslHandlerProvider.getSslHandler(); |
|||
context2 = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); |
|||
assertThat(context2).isNotNull(); |
|||
assertThat(context2).isNotSameAs(context1); |
|||
|
|||
reloadCallback.run(); |
|||
sslHandlerProvider.getSslHandler(); |
|||
context3 = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); |
|||
assertThat(context3).isNotNull(); |
|||
assertThat(context3).isNotSameAs(context2); |
|||
assertThat(context3).isNotSameAs(context1); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,193 @@ |
|||
/** |
|||
* 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.common.transport.service; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.DisposableBean; |
|||
import org.springframework.beans.factory.SmartInitializingSingleton; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.context.ApplicationContext; |
|||
import org.springframework.stereotype.Component; |
|||
import org.thingsboard.common.util.ThingsBoardThreadFactory; |
|||
import org.thingsboard.server.common.transport.config.ssl.SslCredentials; |
|||
import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig; |
|||
import org.thingsboard.server.queue.util.TbTransportComponent; |
|||
|
|||
import java.io.IOException; |
|||
import java.nio.file.Files; |
|||
import java.nio.file.Path; |
|||
import java.security.MessageDigest; |
|||
import java.util.Base64; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
import java.util.concurrent.Executors; |
|||
import java.util.concurrent.ScheduledExecutorService; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
@Slf4j |
|||
@Component |
|||
@TbTransportComponent |
|||
public class CertificateReloadManager implements SmartInitializingSingleton, DisposableBean { |
|||
|
|||
@Value("${transport.ssl.certificate.reload.enabled:true}") |
|||
private boolean reloadEnabled; |
|||
|
|||
@Value("${transport.ssl.certificate.reload.check_interval:60}") |
|||
private long checkIntervalInSeconds; |
|||
|
|||
@Autowired |
|||
protected ApplicationContext applicationContext; |
|||
|
|||
private final Map<String, CertificateWatcher> watchers = new ConcurrentHashMap<>(); |
|||
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("certificate-reload-manager")); |
|||
|
|||
public void registerWatcher(String name, Path certPath, Runnable reloadCallback) { |
|||
watchers.put(name, new CertificateWatcher(certPath, reloadCallback)); |
|||
log.info("Registered certificate watcher for: {}", name); |
|||
} |
|||
|
|||
private void checkCertificates() { |
|||
watchers.forEach((name, watcher) -> { |
|||
try { |
|||
if (watcher.hasChanged()) { |
|||
log.info("Certificate change detected for: {}. Triggering reload...", name); |
|||
watcher.reload(); |
|||
} |
|||
} catch (Exception e) { |
|||
log.error("Error checking certificate for {}: {}", name, e.getMessage(), e); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private void discoverAndRegisterSslCredentials() { |
|||
try { |
|||
Map<String, SslCredentialsConfig> sslConfigBeans = applicationContext.getBeansOfType(SslCredentialsConfig.class); |
|||
|
|||
log.info("Found {} SslCredentialsConfig beans", sslConfigBeans.size()); |
|||
|
|||
for (Map.Entry<String, SslCredentialsConfig> entry : sslConfigBeans.entrySet()) { |
|||
String beanName = entry.getKey(); |
|||
SslCredentialsConfig config = entry.getValue(); |
|||
|
|||
try { |
|||
if (!config.isEnabled()) { |
|||
log.debug("Skipping disabled SSL config: {} ({})", config.getName(), beanName); |
|||
continue; |
|||
} |
|||
|
|||
SslCredentials credentials = config.getCredentials(); |
|||
if (credentials == null) { |
|||
log.debug("Skipping uninitialized SSL config: {} ({})", config.getName(), beanName); |
|||
continue; |
|||
} |
|||
|
|||
List<Path> filePaths = credentials.getCertificateFilePaths(); |
|||
if (filePaths == null || filePaths.isEmpty()) { |
|||
log.debug("No certificate files to watch for: {} ({})", config.getName(), beanName); |
|||
continue; |
|||
} |
|||
|
|||
for (Path filePath : filePaths) { |
|||
if (filePath != null && Files.exists(filePath)) { |
|||
String watcherKey = config.getName() + " - " + filePath.getFileName(); |
|||
registerWatcher(watcherKey, filePath, config::onCertificateFileChanged); |
|||
log.info("Registered certificate watcher: {} -> {}", config.getName(), filePath); |
|||
} else { |
|||
log.warn("Certificate file does not exist: {} (from {})", filePath, config.getName()); |
|||
} |
|||
} |
|||
|
|||
} catch (Exception e) { |
|||
log.error("Error registering watchers for SSL config: {} ({})", config.getName(), beanName, e); |
|||
} |
|||
} |
|||
|
|||
} catch (Exception e) { |
|||
log.error("Error discovering SSL credentials configs", e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void destroy() throws Exception { |
|||
scheduler.shutdown(); |
|||
} |
|||
|
|||
@Override |
|||
public void afterSingletonsInstantiated() { |
|||
if (!reloadEnabled) { |
|||
log.trace("Auto-reload of certificates is disabled. Skipping initialization..."); |
|||
return; |
|||
} |
|||
log.info("Initializing Certificate Reload Manager..."); |
|||
|
|||
discoverAndRegisterSslCredentials(); |
|||
|
|||
scheduler.scheduleWithFixedDelay(this::checkCertificates, checkIntervalInSeconds, checkIntervalInSeconds, TimeUnit.SECONDS); |
|||
} |
|||
|
|||
private static class CertificateWatcher { |
|||
private final Path path; |
|||
private final Runnable reloadCallback; |
|||
private long lastModified; |
|||
private String lastChecksum; |
|||
|
|||
CertificateWatcher(Path path, Runnable reloadCallback) { |
|||
this.path = path; |
|||
this.reloadCallback = reloadCallback; |
|||
this.lastModified = getLastModifiedTime(); |
|||
this.lastChecksum = calculateChecksum(); |
|||
} |
|||
|
|||
boolean hasChanged() { |
|||
long currentModified = getLastModifiedTime(); |
|||
if (currentModified != lastModified) { |
|||
String currentChecksum = calculateChecksum(); |
|||
return !currentChecksum.equals(lastChecksum); |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
void reload() { |
|||
reloadCallback.run(); |
|||
lastModified = getLastModifiedTime(); |
|||
lastChecksum = calculateChecksum(); |
|||
} |
|||
|
|||
private long getLastModifiedTime() { |
|||
try { |
|||
return Files.getLastModifiedTime(path).toMillis(); |
|||
} catch (IOException e) { |
|||
return 0; |
|||
} |
|||
} |
|||
|
|||
private String calculateChecksum() { |
|||
try { |
|||
MessageDigest md = MessageDigest.getInstance("SHA-256"); |
|||
byte[] bytes = Files.readAllBytes(path); |
|||
byte[] hash = md.digest(bytes); |
|||
return Base64.getEncoder().encodeToString(hash); |
|||
} catch (Exception e) { |
|||
log.warn("Failed to calculate checksum for certificate file: {}", path, e); |
|||
return ""; |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,277 @@ |
|||
/** |
|||
* 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.common.transport.config.ssl; |
|||
|
|||
import org.junit.jupiter.api.BeforeEach; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.api.extension.ExtendWith; |
|||
import org.mockito.ArgumentCaptor; |
|||
import org.mockito.Mock; |
|||
import org.mockito.junit.jupiter.MockitoExtension; |
|||
import org.mockito.junit.jupiter.MockitoSettings; |
|||
import org.mockito.quality.Strictness; |
|||
import org.springframework.boot.autoconfigure.web.ServerProperties; |
|||
import org.springframework.boot.ssl.SslBundle; |
|||
import org.springframework.boot.ssl.SslBundles; |
|||
import org.springframework.test.util.ReflectionTestUtils; |
|||
|
|||
import java.security.KeyStore; |
|||
import java.security.cert.X509Certificate; |
|||
import java.util.List; |
|||
import java.util.concurrent.CountDownLatch; |
|||
import java.util.concurrent.TimeUnit; |
|||
import java.util.concurrent.atomic.AtomicInteger; |
|||
import java.util.function.Consumer; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
import static org.mockito.Mockito.mock; |
|||
import static org.mockito.Mockito.times; |
|||
import static org.mockito.Mockito.verify; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
@ExtendWith(MockitoExtension.class) |
|||
@MockitoSettings(strictness = Strictness.LENIENT) |
|||
public class SslCredentialsWebServerCustomizerTest { |
|||
|
|||
@Mock |
|||
private ServerProperties mockServerProperties; |
|||
|
|||
@Mock |
|||
private SslCredentialsConfig mockCredentialsConfig; |
|||
|
|||
@Mock |
|||
private SslCredentials mockCredentials; |
|||
|
|||
@Mock |
|||
private KeyStore mockKeyStore; |
|||
|
|||
private SslCredentialsWebServerCustomizer customizer; |
|||
|
|||
@BeforeEach |
|||
public void setup() throws Exception { |
|||
customizer = new SslCredentialsWebServerCustomizer(mockServerProperties); |
|||
ReflectionTestUtils.setField(customizer, "httpServerSslCredentialsConfig", mockCredentialsConfig); |
|||
|
|||
when(mockCredentialsConfig.getCredentials()).thenReturn(mockCredentials); |
|||
when(mockCredentials.getKeyStore()).thenReturn(mockKeyStore); |
|||
when(mockCredentials.getKeyPassword()).thenReturn("password"); |
|||
when(mockCredentials.getKeyAlias()).thenReturn("server"); |
|||
|
|||
X509Certificate mockCert = mock(X509Certificate.class); |
|||
when(mockCert.getEncoded()).thenReturn("TEST_CERT_DATA".getBytes()); |
|||
when(mockCredentials.getCertificateChain()).thenReturn(new X509Certificate[]{mockCert}); |
|||
} |
|||
|
|||
@Test |
|||
public void givenInitialized_whenAfterSingletonsInstantiated_thenShouldRegisterReloadCallback() { |
|||
customizer.afterSingletonsInstantiated(); |
|||
|
|||
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); |
|||
assertThat(callbackCaptor.getValue()).isNotNull(); |
|||
} |
|||
|
|||
@Test |
|||
public void givenReloadCallback_whenInvoked_thenShouldReloadCertificates() { |
|||
customizer.afterSingletonsInstantiated(); |
|||
|
|||
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); |
|||
Runnable reloadCallback = callbackCaptor.getValue(); |
|||
|
|||
reloadCallback.run(); |
|||
|
|||
verify(mockCredentialsConfig, times(1)).getCredentials(); |
|||
} |
|||
|
|||
@Test |
|||
public void givenSslBundles_whenGetBundle_thenShouldReturnValidBundle() { |
|||
SslBundles sslBundles = customizer.sslBundles(); |
|||
|
|||
SslBundle bundle = sslBundles.getBundle("default"); |
|||
|
|||
assertThat(bundle).isNotNull(); |
|||
} |
|||
|
|||
@Test |
|||
public void givenSslBundles_whenGetBundleNames_thenShouldReturnDefault() { |
|||
SslBundles sslBundles = customizer.sslBundles(); |
|||
|
|||
List<String> bundleNames = sslBundles.getBundleNames(); |
|||
|
|||
assertThat(bundleNames).containsExactly("default"); |
|||
} |
|||
|
|||
@Test |
|||
public void givenSslBundles_whenAddUpdateHandler_thenShouldRegisterHandler() { |
|||
SslBundles sslBundles = customizer.sslBundles(); |
|||
AtomicInteger handlerCallCount = new AtomicInteger(0); |
|||
Consumer<SslBundle> handler = bundle -> handlerCallCount.incrementAndGet(); |
|||
|
|||
sslBundles.addBundleUpdateHandler("default", handler); |
|||
|
|||
customizer.afterSingletonsInstantiated(); |
|||
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); |
|||
callbackCaptor.getValue().run(); |
|||
|
|||
assertThat(handlerCallCount.get()).isEqualTo(1); |
|||
} |
|||
|
|||
@Test |
|||
public void givenSslBundles_whenAddUpdateHandlerForWrongBundle_thenShouldNotRegister() { |
|||
SslBundles sslBundles = customizer.sslBundles(); |
|||
AtomicInteger handlerCallCount = new AtomicInteger(0); |
|||
Consumer<SslBundle> handler = bundle -> handlerCallCount.incrementAndGet(); |
|||
|
|||
sslBundles.addBundleUpdateHandler("wrong-bundle", handler); |
|||
|
|||
customizer.afterSingletonsInstantiated(); |
|||
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); |
|||
callbackCaptor.getValue().run(); |
|||
|
|||
assertThat(handlerCallCount.get()).isEqualTo(0); |
|||
} |
|||
|
|||
@Test |
|||
public void givenMultipleUpdateHandlers_whenReload_thenShouldNotifyAll() { |
|||
SslBundles sslBundles = customizer.sslBundles(); |
|||
AtomicInteger handler1CallCount = new AtomicInteger(0); |
|||
AtomicInteger handler2CallCount = new AtomicInteger(0); |
|||
AtomicInteger handler3CallCount = new AtomicInteger(0); |
|||
|
|||
sslBundles.addBundleUpdateHandler("default", bundle -> handler1CallCount.incrementAndGet()); |
|||
sslBundles.addBundleUpdateHandler("default", bundle -> handler2CallCount.incrementAndGet()); |
|||
sslBundles.addBundleUpdateHandler("default", bundle -> handler3CallCount.incrementAndGet()); |
|||
|
|||
customizer.afterSingletonsInstantiated(); |
|||
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); |
|||
|
|||
callbackCaptor.getValue().run(); |
|||
|
|||
assertThat(handler1CallCount.get()).isEqualTo(1); |
|||
assertThat(handler2CallCount.get()).isEqualTo(1); |
|||
assertThat(handler3CallCount.get()).isEqualTo(1); |
|||
} |
|||
|
|||
@Test |
|||
public void givenMultipleReloads_whenTriggered_thenShouldNotifyHandlersEachTime() { |
|||
SslBundles sslBundles = customizer.sslBundles(); |
|||
AtomicInteger handlerCallCount = new AtomicInteger(0); |
|||
sslBundles.addBundleUpdateHandler("default", bundle -> handlerCallCount.incrementAndGet()); |
|||
|
|||
customizer.afterSingletonsInstantiated(); |
|||
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); |
|||
Runnable reloadCallback = callbackCaptor.getValue(); |
|||
|
|||
reloadCallback.run(); |
|||
reloadCallback.run(); |
|||
reloadCallback.run(); |
|||
|
|||
assertThat(handlerCallCount.get()).isEqualTo(3); |
|||
} |
|||
|
|||
@Test |
|||
public void givenUpdateHandlerThrowsException_whenReload_thenShouldContinueNotifyingOtherHandlers() { |
|||
SslBundles sslBundles = customizer.sslBundles(); |
|||
AtomicInteger handler1CallCount = new AtomicInteger(0); |
|||
AtomicInteger handler2CallCount = new AtomicInteger(0); |
|||
|
|||
sslBundles.addBundleUpdateHandler("default", bundle -> { |
|||
handler1CallCount.incrementAndGet(); |
|||
throw new RuntimeException("Handler 1 failed"); |
|||
}); |
|||
sslBundles.addBundleUpdateHandler("default", bundle -> handler2CallCount.incrementAndGet()); |
|||
|
|||
customizer.afterSingletonsInstantiated(); |
|||
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); |
|||
|
|||
callbackCaptor.getValue().run(); |
|||
|
|||
assertThat(handler1CallCount.get()).isEqualTo(1); |
|||
assertThat(handler2CallCount.get()).isEqualTo(1); |
|||
} |
|||
|
|||
@Test |
|||
public void givenConcurrentReloads_whenTriggered_thenShouldHandleThreadSafely() throws Exception { |
|||
SslBundles sslBundles = customizer.sslBundles(); |
|||
AtomicInteger handlerCallCount = new AtomicInteger(0); |
|||
CountDownLatch startLatch = new CountDownLatch(1); |
|||
CountDownLatch doneLatch = new CountDownLatch(5); |
|||
|
|||
sslBundles.addBundleUpdateHandler("default", bundle -> handlerCallCount.incrementAndGet()); |
|||
|
|||
customizer.afterSingletonsInstantiated(); |
|||
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); |
|||
Runnable reloadCallback = callbackCaptor.getValue(); |
|||
|
|||
for (int i = 0; i < 5; i++) { |
|||
new Thread(() -> { |
|||
try { |
|||
startLatch.await(); |
|||
reloadCallback.run(); |
|||
} catch (Exception e) { |
|||
throw new RuntimeException(e); |
|||
} finally { |
|||
doneLatch.countDown(); |
|||
} |
|||
}).start(); |
|||
} |
|||
|
|||
startLatch.countDown(); |
|||
boolean completed = doneLatch.await(5, TimeUnit.SECONDS); |
|||
|
|||
assertThat(completed).isTrue(); |
|||
assertThat(handlerCallCount.get()).isEqualTo(5); |
|||
} |
|||
|
|||
@Test |
|||
public void givenReloadWithFailingCredentials_whenInvoked_thenShouldHandleGracefully() { |
|||
when(mockCredentialsConfig.getCredentials()).thenThrow(new RuntimeException("Failed to load credentials")); |
|||
|
|||
customizer.afterSingletonsInstantiated(); |
|||
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); |
|||
|
|||
callbackCaptor.getValue().run(); |
|||
} |
|||
|
|||
@Test |
|||
public void givenSslBundle_whenGetBundleMultipleTimes_thenShouldReturnFreshBundle() { |
|||
SslBundles sslBundles = customizer.sslBundles(); |
|||
|
|||
SslBundle bundle1 = sslBundles.getBundle("default"); |
|||
SslBundle bundle2 = sslBundles.getBundle("default"); |
|||
|
|||
assertThat(bundle1).isNotNull(); |
|||
assertThat(bundle2).isNotNull(); |
|||
} |
|||
|
|||
@Test |
|||
public void givenHttpServerSslCredentials_whenCreateBean_thenShouldReturnConfig() { |
|||
SslCredentialsConfig config = customizer.httpServerSslCredentials(); |
|||
|
|||
assertThat(config).isNotNull(); |
|||
assertThat(config.getName()).isEqualTo("HTTP Server SSL Credentials"); |
|||
assertThat(config.isTrustsOnly()).isFalse(); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,175 @@ |
|||
/** |
|||
* 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.common.transport.service; |
|||
|
|||
import org.junit.jupiter.api.AfterEach; |
|||
import org.junit.jupiter.api.BeforeEach; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.api.io.TempDir; |
|||
import org.springframework.test.util.ReflectionTestUtils; |
|||
|
|||
import java.io.IOException; |
|||
import java.nio.file.Files; |
|||
import java.nio.file.Path; |
|||
import java.util.concurrent.CountDownLatch; |
|||
import java.util.concurrent.TimeUnit; |
|||
import java.util.concurrent.atomic.AtomicInteger; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
|
|||
public class CertificateReloadManagerTest { |
|||
|
|||
@TempDir |
|||
Path tempDir; |
|||
|
|||
private CertificateReloadManager certificateReloadManager; |
|||
private Path certFile; |
|||
|
|||
@BeforeEach |
|||
public void setup() throws IOException { |
|||
certificateReloadManager = new CertificateReloadManager(); |
|||
|
|||
certFile = tempDir.resolve("test-cert.pem"); |
|||
Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nTEST_CERT_V1\n-----END CERTIFICATE-----\n"); |
|||
} |
|||
|
|||
@AfterEach |
|||
public void teardown() throws Exception { |
|||
if (certificateReloadManager != null) { |
|||
certificateReloadManager.destroy(); |
|||
} |
|||
} |
|||
|
|||
@Test |
|||
public void givenCertificateFileChanged_whenCheckForChanges_thenShouldTriggerReload() throws Exception { |
|||
CountDownLatch reloadLatch = new CountDownLatch(1); |
|||
AtomicInteger reloadCount = new AtomicInteger(0); |
|||
|
|||
certificateReloadManager.registerWatcher("test-cert", certFile, () -> { |
|||
reloadCount.incrementAndGet(); |
|||
reloadLatch.countDown(); |
|||
}); |
|||
|
|||
Thread.sleep(100); // Small delay to ensure the initial state is captured
|
|||
Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nTEST_CERT_V2_MODIFIED\n-----END CERTIFICATE-----\n"); |
|||
|
|||
// Manually trigger check (since a scheduled task runs every 1 minute)
|
|||
ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); |
|||
|
|||
boolean reloadTriggered = reloadLatch.await(2, TimeUnit.SECONDS); |
|||
|
|||
assertThat(reloadTriggered).isTrue(); |
|||
assertThat(reloadCount.get()).isEqualTo(1); |
|||
} |
|||
|
|||
@Test |
|||
public void givenCertificateFileUnchanged_whenCheckForChanges_thenShouldNotTriggerReload() throws Exception { |
|||
AtomicInteger reloadCount = new AtomicInteger(0); |
|||
|
|||
certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); |
|||
|
|||
Thread.sleep(100); |
|||
ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); |
|||
Thread.sleep(100); |
|||
ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); |
|||
|
|||
assertThat(reloadCount.get()).isEqualTo(0); |
|||
} |
|||
|
|||
@Test |
|||
public void givenOnlyTimestampChanged_whenCheckForChanges_thenShouldNotTriggerReload() throws Exception { |
|||
AtomicInteger reloadCount = new AtomicInteger(0); |
|||
|
|||
certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); |
|||
|
|||
Thread.sleep(100); |
|||
|
|||
ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); |
|||
|
|||
assertThat(reloadCount.get()).isEqualTo(0); |
|||
} |
|||
|
|||
@Test |
|||
public void givenWatcherRegistered_whenFileDeleted_thenShouldHandleGracefully() throws Exception { |
|||
AtomicInteger reloadCount = new AtomicInteger(0); |
|||
|
|||
certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); |
|||
|
|||
Thread.sleep(100); |
|||
|
|||
Files.delete(certFile); |
|||
Thread.sleep(100); |
|||
|
|||
ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); |
|||
Thread.sleep(100); |
|||
} |
|||
|
|||
@Test |
|||
public void givenWatcherRegistered_whenShutdown_thenShouldStopScheduler() throws Exception { |
|||
certificateReloadManager.registerWatcher("test-cert", certFile, () -> {}); |
|||
|
|||
certificateReloadManager.destroy(); |
|||
|
|||
assertThat(certificateReloadManager).isNotNull(); |
|||
} |
|||
|
|||
@Test |
|||
public void givenMultipleCertificateFiles_whenOneChanges_thenShouldTriggerReload() throws Exception { |
|||
Path keyFile = tempDir.resolve("test-key.pem"); |
|||
Files.writeString(keyFile, "-----BEGIN PRIVATE KEY-----\nTEST_KEY_V1\n-----END PRIVATE KEY-----\n"); |
|||
|
|||
CountDownLatch certReloadLatch = new CountDownLatch(1); |
|||
CountDownLatch keyReloadLatch = new CountDownLatch(1); |
|||
|
|||
certificateReloadManager.registerWatcher("test-cert", certFile, certReloadLatch::countDown); |
|||
certificateReloadManager.registerWatcher("test-key", keyFile, keyReloadLatch::countDown); |
|||
|
|||
Thread.sleep(100); |
|||
Files.writeString(keyFile, "-----BEGIN PRIVATE KEY-----\nTEST_KEY_V2_MODIFIED\n-----END PRIVATE KEY-----\n"); |
|||
Thread.sleep(100); |
|||
|
|||
ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); |
|||
|
|||
boolean keyReloaded = keyReloadLatch.await(2, TimeUnit.SECONDS); |
|||
|
|||
assertThat(keyReloaded).isTrue(); |
|||
assertThat(certReloadLatch.getCount()).isEqualTo(1); // Cert isn't reloaded
|
|||
} |
|||
|
|||
@Test |
|||
public void givenMultipleWatchers_whenCheckCertificates_thenShouldCheckAll() throws Exception { |
|||
Path cert2File = tempDir.resolve("test-cert2.pem"); |
|||
Files.writeString(cert2File, "-----BEGIN CERTIFICATE-----\nTEST_CERT2_V1\n-----END CERTIFICATE-----\n"); |
|||
|
|||
AtomicInteger reload1Count = new AtomicInteger(0); |
|||
AtomicInteger reload2Count = new AtomicInteger(0); |
|||
|
|||
certificateReloadManager.registerWatcher("test-cert1", certFile, reload1Count::incrementAndGet); |
|||
certificateReloadManager.registerWatcher("test-cert2", cert2File, reload2Count::incrementAndGet); |
|||
|
|||
Thread.sleep(100); |
|||
Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED1\n-----END CERTIFICATE-----\n"); |
|||
Files.writeString(cert2File, "-----BEGIN CERTIFICATE-----\nMODIFIED2\n-----END CERTIFICATE-----\n"); |
|||
Thread.sleep(100); |
|||
|
|||
ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); |
|||
|
|||
Thread.sleep(200); |
|||
assertThat(reload1Count.get()).isEqualTo(1); |
|||
assertThat(reload2Count.get()).isEqualTo(1); |
|||
} |
|||
|
|||
} |
|||
Loading…
Reference in new issue