diff --git a/application/src/main/java/org/thingsboard/server/config/ApiKeyHandshakeInterceptor.java b/application/src/main/java/org/thingsboard/server/config/ApiKeyHandshakeInterceptor.java new file mode 100644 index 0000000000..d6d550b2c0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/config/ApiKeyHandshakeInterceptor.java @@ -0,0 +1,69 @@ +/** + * 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.config; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.server.HandshakeInterceptor; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.auth.pat.ApiKeyAuthenticationProvider; +import org.thingsboard.server.service.security.model.SecurityUser; + +import java.util.Map; + +@Slf4j +@Component +@TbCoreComponent +@RequiredArgsConstructor +public class ApiKeyHandshakeInterceptor implements HandshakeInterceptor { + + public static final String API_KEY_HEADER = "X-API-Key"; + public static final String API_KEY_SECURITY_CTX_ATTR = "apiKeySecurityCtx"; + + private final ApiKeyAuthenticationProvider apiKeyAuthenticationProvider; + + @Override + public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) { + String apiKey = request.getHeaders().getFirst(API_KEY_HEADER); + if (apiKey != null) { + if (apiKey.isEmpty()) { + log.debug("Empty API key provided during WS handshake"); + response.setStatusCode(HttpStatus.UNAUTHORIZED); + return false; + } + try { + SecurityUser securityUser = apiKeyAuthenticationProvider.authenticate(apiKey); + attributes.put(API_KEY_SECURITY_CTX_ATTR, securityUser); + } catch (Exception e) { + log.debug("API key authentication failed during WS handshake: {}", e.getMessage()); + response.setStatusCode(HttpStatus.UNAUTHORIZED); + return false; + } + } + return true; + } + + @Override + public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { + // no-op + } + +} diff --git a/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java b/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java index 38f37cf35d..f5be3afa41 100644 --- a/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java @@ -40,6 +40,7 @@ public class WebSocketConfiguration implements WebSocketConfigurer { private static final String WS_API_MAPPING = "/api/ws/**"; private final WebSocketHandler wsHandler; + private final ApiKeyHandshakeInterceptor apiKeyHandshakeInterceptor; @Value("${server.ws.max_text_message_buffer_size:32768}") private int maxTextMessageBufferSize; @@ -60,7 +61,9 @@ public class WebSocketConfiguration implements WebSocketConfigurer { log.error("TbWebSocketHandler expected but [{}] provided", wsHandler); throw new RuntimeException("TbWebSocketHandler expected but " + wsHandler + " provided"); } - registry.addHandler(wsHandler, WS_API_MAPPING).setAllowedOriginPatterns("*"); + registry.addHandler(wsHandler, WS_API_MAPPING) + .addInterceptors(apiKeyHandshakeInterceptor) + .setAllowedOriginPatterns("*"); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java index 73315be73f..5c24c1e6c1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java +++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java @@ -48,10 +48,12 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.config.ApiKeyHandshakeInterceptor; import org.thingsboard.server.config.WebSocketConfiguration; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider; +import org.thingsboard.server.service.security.auth.pat.ApiKeyAuthenticationProvider; import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; @@ -100,6 +102,8 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke private RateLimitService rateLimitService; @Autowired private JwtAuthenticationProvider authenticationProvider; + @Autowired + private ApiKeyAuthenticationProvider apiKeyAuthenticationProvider; @Value("${server.ws.send_timeout:5000}") private long sendTimeout; @@ -194,7 +198,11 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke log.trace("{} Authenticating session", sessionRef); SecurityUser securityCtx; try { - securityCtx = authenticationProvider.authenticate(authCmd.getToken()); + if (StringUtils.isNotEmpty(authCmd.getApiKey())) { + securityCtx = apiKeyAuthenticationProvider.authenticate(authCmd.getApiKey()); + } else { + securityCtx = authenticationProvider.authenticate(authCmd.getToken()); + } } catch (Exception e) { close(sessionRef, CloseStatus.BAD_DATA.withReason(e.getMessage())); return; @@ -328,9 +336,17 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke } SecurityUser securityCtx = null; - String token = StringUtils.substringAfter(session.getUri().getQuery(), "token="); - if (StringUtils.isNotEmpty(token)) { - securityCtx = authenticationProvider.authenticate(token); + Object apiKeyCtx = session.getAttributes().get(ApiKeyHandshakeInterceptor.API_KEY_SECURITY_CTX_ATTR); + if (apiKeyCtx instanceof SecurityUser) { + securityCtx = (SecurityUser) apiKeyCtx; + } else { + String query = session.getUri().getQuery(); + if (query != null) { + String token = extractQueryParam(query, "token"); + if (StringUtils.isNotEmpty(token)) { + securityCtx = authenticationProvider.authenticate(token); + } + } } return WebSocketSessionRef.builder() .sessionId(UUID.randomUUID().toString()) @@ -341,6 +357,15 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke .build(); } + private String extractQueryParam(String query, String paramName) { + for (String param : query.split("&")) { + if (param.startsWith(paramName + "=")) { + return param.substring(paramName.length() + 1); + } + } + return null; + } + private SessionMetaData getSessionMd(String internalSessionId) { SessionMetaData sessionMd = internalSessionMap.get(internalSessionId); if (sessionMd == null) { @@ -482,6 +507,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke } } } + } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProvider.java index 162a780bbb..46af0120da 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProvider.java @@ -46,7 +46,7 @@ public class ApiKeyAuthenticationProvider extends AbstractAuthenticationProvider return new ApiKeyAuthenticationToken(securityUser); } - private SecurityUser authenticate(String key) { + public SecurityUser authenticate(String key) { if (StringUtils.isEmpty(key)) { throw new BadCredentialsException("Empty API key"); } diff --git a/application/src/main/java/org/thingsboard/server/service/ws/AuthCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/AuthCmd.java index e139498d04..a2dc87e4af 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/AuthCmd.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/AuthCmd.java @@ -23,11 +23,14 @@ import lombok.NoArgsConstructor; @NoArgsConstructor @AllArgsConstructor public class AuthCmd implements WsCmd { + private int cmdId; private String token; + private String apiKey; @Override public WsCmdType getType() { return WsCmdType.AUTH; } + } diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java index c25dafa5e6..fa43f83ff2 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java @@ -32,6 +32,7 @@ import org.springframework.web.socket.config.annotation.EnableWebSocket; import java.net.URI; import java.net.URISyntaxException; +import java.util.Map; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; @@ -86,12 +87,12 @@ public abstract class AbstractControllerTest extends AbstractNotifyEntityTest { } @Before - public void beforeWsTest() throws Exception { + public void beforeWsTest() { // placeholder } @After - public void afterWsTest() throws Exception { + public void afterWsTest() { if (wsClient != null) { wsClient.close(); } @@ -113,4 +114,17 @@ public abstract class AbstractControllerTest extends AbstractNotifyEntityTest { return wsClient; } + protected TbTestWebSocketClient buildAndConnectWebSocketClientWithApiKey(String apiKey) throws URISyntaxException, InterruptedException { + TbTestWebSocketClient wsClient = new TbTestWebSocketClient(new URI(WS_URL + wsPort + "/api/ws")); + assertThat(wsClient.connectBlocking(TIMEOUT, TimeUnit.SECONDS)).isTrue(); + wsClient.authenticateWithApiKey(apiKey); + return wsClient; + } + + protected TbTestWebSocketClient buildAndConnectWebSocketClientWithApiKeyHeader(String apiKey) throws URISyntaxException, InterruptedException { + TbTestWebSocketClient wsClient = new TbTestWebSocketClient(new URI(WS_URL + wsPort + "/api/ws"), Map.of("X-API-Key", apiKey)); + assertThat(wsClient.connectBlocking(TIMEOUT, TimeUnit.SECONDS)).isTrue(); + return wsClient; + } + } diff --git a/application/src/test/java/org/thingsboard/server/controller/ApiKeyWebSocketApiTest.java b/application/src/test/java/org/thingsboard/server/controller/ApiKeyWebSocketApiTest.java new file mode 100644 index 0000000000..b4e96beee2 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/ApiKeyWebSocketApiTest.java @@ -0,0 +1,67 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.pat.ApiKey; +import org.thingsboard.server.common.data.pat.ApiKeyInfo; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.net.URISyntaxException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +@DaoSqlTest +public class ApiKeyWebSocketApiTest extends WebSocketApiTest { + + private ApiKey apiKey; + + @Before + public void setUpApiKey() { + ApiKeyInfo apiKeyInfo = new ApiKeyInfo(); + apiKeyInfo.setDescription("WS test API key"); + apiKeyInfo.setEnabled(true); + apiKeyInfo.setUserId(tenantAdminUserId); + apiKey = doPost("/api/apiKey", apiKeyInfo, ApiKey.class); + } + + @After + public void tearDownApiKey() throws Exception { + loginTenantAdmin(); + doDelete("/api/apiKey/" + apiKey.getId()).andExpect(status().isOk()); + } + + @Override + protected TbTestWebSocketClient buildAndConnectWebSocketClient() throws URISyntaxException, InterruptedException { + return buildAndConnectWebSocketClientWithApiKey(apiKey.getValue()); + } + + @Test + public void testApiKeyHeaderAuth() throws Exception { + TbTestWebSocketClient client = buildAndConnectWebSocketClientWithApiKeyHeader(apiKey.getValue()); + try { + assertThat(client.isOpen()).isTrue(); + } finally { + client.close(); + } + } + +} diff --git a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java index 26705ddacb..35536eaecc 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java @@ -39,12 +39,12 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityHistoryCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.TimeSeriesCmd; -import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; import java.net.URI; import java.nio.channels.NotYetConnectedException; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -62,6 +62,10 @@ public class TbTestWebSocketClient extends WebSocketClient { super(serverUri); } + public TbTestWebSocketClient(URI serverUri, Map httpHeaders) { + super(serverUri, httpHeaders); + } + @Override public void onOpen(ServerHandshake serverHandshake) { @@ -69,7 +73,13 @@ public class TbTestWebSocketClient extends WebSocketClient { public void authenticate(String token) { WsCommandsWrapper cmdsWrapper = new WsCommandsWrapper(); - cmdsWrapper.setAuthCmd(new AuthCmd(1, token)); + cmdsWrapper.setAuthCmd(new AuthCmd(1, token, null)); + send(JacksonUtil.toString(cmdsWrapper)); + } + + public void authenticateWithApiKey(String apiKey) { + WsCommandsWrapper cmdsWrapper = new WsCommandsWrapper(); + cmdsWrapper.setAuthCmd(new AuthCmd(1, null, apiKey)); send(JacksonUtil.toString(cmdsWrapper)); } @@ -275,7 +285,7 @@ public class TbTestWebSocketClient extends WebSocketClient { public JsonNode sendTimeseriesCmd(EntityId entityId, String scope) { log.warn("sendTimeseriesCmd entityId: {}, scope: {}", entityId, scope); - TimeseriesSubscriptionCmd cmd = new TimeseriesSubscriptionCmd(0, 0, 0, 10, null); + TimeseriesSubscriptionCmd cmd = new TimeseriesSubscriptionCmd(0, 0, 0, 10, null); cmd.setEntityId(entityId.getId().toString()); cmd.setEntityType(entityId.getEntityType().toString()); cmd.setCmdId(1); diff --git a/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java b/application/src/test/java/org/thingsboard/server/controller/WebSocketApiTest.java similarity index 84% rename from application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java rename to application/src/test/java/org/thingsboard/server/controller/WebSocketApiTest.java index 87ba0ec3e8..b7d03b5bea 100644 --- a/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/WebSocketApiTest.java @@ -71,13 +71,11 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmStatusUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; -import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -91,7 +89,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. "server.ws.alarms_per_alarm_status_subscription_cache_size=5", "server.ws.dynamic_page_link.refresh_interval=15" }) -public class WebsocketApiTest extends AbstractControllerTest { +public class WebSocketApiTest extends AbstractControllerTest { + @Autowired private TelemetrySubscriptionService tsService; @@ -128,8 +127,8 @@ public class WebsocketApiTest extends AbstractControllerTest { PageData pageData = update.getData(); Assert.assertNotNull(pageData); Assert.assertEquals(1, pageData.getData().size()); - Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId()); - Assert.assertEquals(0, pageData.getData().get(0).getTimeseries().get("temperature").length); + Assert.assertEquals(device.getId(), pageData.getData().getFirst().getEntityId()); + Assert.assertEquals(0, pageData.getData().getFirst().getTimeseries().get("temperature").length); TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L)); TsKvEntry dataPoint2 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(2), new LongDataEntry("temperature", 42L)); @@ -144,8 +143,8 @@ public class WebsocketApiTest extends AbstractControllerTest { List dataList = update.getUpdate(); Assert.assertNotNull(dataList); Assert.assertEquals(1, dataList.size()); - Assert.assertEquals(device.getId(), dataList.get(0).getEntityId()); - TsValue[] tsArray = dataList.get(0).getTimeseries().get("temperature"); + Assert.assertEquals(device.getId(), dataList.getFirst().getEntityId()); + TsValue[] tsArray = dataList.getFirst().getTimeseries().get("temperature"); Assert.assertEquals(3, tsArray.length); Assert.assertEquals(new TsValue(dataPoint1.getTs(), dataPoint1.getValueAsString()), tsArray[0]); Assert.assertEquals(new TsValue(dataPoint2.getTs(), dataPoint2.getValueAsString()), tsArray[1]); @@ -162,7 +161,7 @@ public class WebsocketApiTest extends AbstractControllerTest { PageData pageData = update.getData(); Assert.assertNotNull(pageData); Assert.assertEquals(1, pageData.getData().size()); - Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId()); + Assert.assertEquals(device.getId(), pageData.getData().getFirst().getEntityId()); TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L)); TsKvEntry dataPoint2 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(2), new LongDataEntry("temperature", 43L)); @@ -176,8 +175,8 @@ public class WebsocketApiTest extends AbstractControllerTest { List listData = update.getUpdate(); Assert.assertNotNull(listData); Assert.assertEquals(1, listData.size()); - Assert.assertEquals(device.getId(), listData.get(0).getEntityId()); - TsValue[] tsArray = listData.get(0).getTimeseries().get("temperature"); + Assert.assertEquals(device.getId(), listData.getFirst().getEntityId()); + TsValue[] tsArray = listData.getFirst().getTimeseries().get("temperature"); Assert.assertEquals(3, tsArray.length); Assert.assertEquals(new TsValue(dataPoint1.getTs(), dataPoint1.getValueAsString()), tsArray[0]); Assert.assertEquals(new TsValue(dataPoint2.getTs(), dataPoint2.getValueAsString()), tsArray[1]); @@ -186,7 +185,7 @@ public class WebsocketApiTest extends AbstractControllerTest { now = System.currentTimeMillis(); TsKvEntry dataPoint4 = new BasicTsKvEntry(now, new LongDataEntry("temperature", 45L)); getWsClient().registerWaitForUpdate(); - sendTelemetry(device, Arrays.asList(dataPoint4)); + sendTelemetry(device, List.of(dataPoint4)); String msg = getWsClient().waitForUpdate(); update = JacksonUtil.fromString(msg, EntityDataUpdate.class); @@ -194,9 +193,9 @@ public class WebsocketApiTest extends AbstractControllerTest { List eData = update.getUpdate(); Assert.assertNotNull(eData); Assert.assertEquals(1, eData.size()); - Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); - Assert.assertNotNull(eData.get(0).getTimeseries()); - TsValue[] tsValues = eData.get(0).getTimeseries().get("temperature"); + Assert.assertEquals(device.getId(), eData.getFirst().getEntityId()); + Assert.assertNotNull(eData.getFirst().getTimeseries()); + TsValue[] tsValues = eData.getFirst().getTimeseries().get("temperature"); Assert.assertNotNull(tsValues); Assert.assertEquals(new TsValue(dataPoint4.getTs(), dataPoint4.getValueAsString()), tsValues[0]); } @@ -521,7 +520,7 @@ public class WebsocketApiTest extends AbstractControllerTest { String msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); Assert.assertNull(msg); - // check device + // check a device AlarmStatusCmd deviceCmd = new AlarmStatusCmd(2, device.getId(), null, List.of(AlarmSeverity.CRITICAL)); getWsClient().send(deviceCmd); @@ -589,13 +588,13 @@ public class WebsocketApiTest extends AbstractControllerTest { PageData pageData = update.getData(); Assert.assertNotNull(pageData); Assert.assertEquals(1, pageData.getData().size()); - Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId()); - Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature")); - Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getTs()); - Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()); + Assert.assertEquals(device.getId(), pageData.getData().getFirst().getEntityId()); + Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature")); + Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getTs()); + Assert.assertEquals("", pageData.getData().getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()); TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L)); - List tsData = Arrays.asList(dataPoint1); + List tsData = List.of(dataPoint1); sendTelemetry(device, tsData); update = getWsClient().subscribeLatestUpdate(keys); @@ -605,36 +604,36 @@ public class WebsocketApiTest extends AbstractControllerTest { List listData = update.getUpdate(); Assert.assertNotNull(listData); Assert.assertEquals(1, listData.size()); - Assert.assertEquals(device.getId(), listData.get(0).getEntityId()); - Assert.assertNotNull(listData.get(0).getLatest().get(EntityKeyType.TIME_SERIES)); - TsValue tsValue = listData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"); + Assert.assertEquals(device.getId(), listData.getFirst().getEntityId()); + Assert.assertNotNull(listData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES)); + TsValue tsValue = listData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"); Assert.assertEquals(new TsValue(dataPoint1.getTs(), dataPoint1.getValueAsString()), tsValue); now = System.currentTimeMillis(); TsKvEntry dataPoint2 = new BasicTsKvEntry(now, new LongDataEntry("temperature", 52L)); getWsClient().registerWaitForUpdate(); - sendTelemetry(device, Arrays.asList(dataPoint2)); + sendTelemetry(device, List.of(dataPoint2)); update = getWsClient().parseDataReply(getWsClient().waitForUpdate()); Assert.assertEquals(1, update.getCmdId()); List eData = update.getUpdate(); Assert.assertNotNull(eData); Assert.assertEquals(1, eData.size()); - Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); - Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES)); - tsValue = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"); + Assert.assertEquals(device.getId(), eData.getFirst().getEntityId()); + Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES)); + tsValue = eData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"); Assert.assertEquals(new TsValue(dataPoint2.getTs(), dataPoint2.getValueAsString()), tsValue); //Sending update from the past, while latest value has new timestamp; getWsClient().registerWaitForUpdate(); - sendTelemetry(device, Arrays.asList(dataPoint1)); + sendTelemetry(device, List.of(dataPoint1)); String msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); Assert.assertNull(msg); //Sending duplicate update again getWsClient().registerWaitForUpdate(); - sendTelemetry(device, Arrays.asList(dataPoint2)); + sendTelemetry(device, List.of(dataPoint2)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); Assert.assertNull(msg); } @@ -677,14 +676,14 @@ public class WebsocketApiTest extends AbstractControllerTest { PageData pageData = update.getData(); Assert.assertNotNull(pageData); Assert.assertEquals(1, pageData.getData().size()); - Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId()); - Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature")); - Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getTs()); - Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()); + Assert.assertEquals(device.getId(), pageData.getData().getFirst().getEntityId()); + Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature")); + Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getTs()); + Assert.assertEquals("", pageData.getData().getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()); getWsClient().registerWaitForUpdate(); TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L)); - List tsData = Arrays.asList(dataPoint1); + List tsData = List.of(dataPoint1); sendTelemetry(device, tsData); update = getWsClient().parseDataReply(getWsClient().waitForUpdate()); @@ -694,34 +693,34 @@ public class WebsocketApiTest extends AbstractControllerTest { List listData = update.getUpdate(); Assert.assertNotNull(listData); Assert.assertEquals(1, listData.size()); - Assert.assertEquals(device.getId(), listData.get(0).getEntityId()); - Assert.assertNotNull(listData.get(0).getLatest().get(EntityKeyType.TIME_SERIES)); - TsValue tsValue = listData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"); + Assert.assertEquals(device.getId(), listData.getFirst().getEntityId()); + Assert.assertNotNull(listData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES)); + TsValue tsValue = listData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"); Assert.assertEquals(new TsValue(dataPoint1.getTs(), dataPoint1.getValueAsString()), tsValue); now = System.currentTimeMillis(); TsKvEntry dataPoint2 = new BasicTsKvEntry(now, new LongDataEntry("temperature", 52L)); getWsClient().registerWaitForUpdate(); - sendTelemetry(device, Arrays.asList(dataPoint2)); + sendTelemetry(device, List.of(dataPoint2)); update = getWsClient().parseDataReply(getWsClient().waitForUpdate()); Assert.assertEquals(1, update.getCmdId()); List eData = update.getUpdate(); Assert.assertNotNull(eData); Assert.assertEquals(1, eData.size()); - Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); - Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES)); - tsValue = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"); + Assert.assertEquals(device.getId(), eData.getFirst().getEntityId()); + Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES)); + tsValue = eData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"); Assert.assertEquals(new TsValue(dataPoint2.getTs(), dataPoint2.getValueAsString()), tsValue); - //Sending update from the past, while latest value has new timestamp; + //Sending update from the past, while the latest value has new timestamp; getWsClient().registerWaitForUpdate(); - sendTelemetry(device, Arrays.asList(dataPoint1)); + sendTelemetry(device, List.of(dataPoint1)); String msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); Assert.assertNull(msg); //Sending duplicate update again getWsClient().registerWaitForUpdate(); - sendTelemetry(device, Arrays.asList(dataPoint2)); + sendTelemetry(device, List.of(dataPoint2)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); Assert.assertNull(msg); } @@ -736,21 +735,21 @@ public class WebsocketApiTest extends AbstractControllerTest { PageData pageData = update.getData(); Assert.assertNotNull(pageData); Assert.assertEquals(1, pageData.getData().size()); - Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId()); - Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey")); - Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getTs()); - Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getValue()); + Assert.assertEquals(device.getId(), pageData.getData().getFirst().getEntityId()); + Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey")); + Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getTs()); + Assert.assertEquals("", pageData.getData().getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getValue()); getWsClient().registerWaitForUpdate(); - // Pushing update with wrong scope and make sure it will not arrive. + // Pushing update with the wrong scope and make sure it will not arrive. AttributeKvEntry invalidDataPoint = new BaseAttributeKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("serverAttributeKey", 55L)); - sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, Arrays.asList(invalidDataPoint)); + sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, List.of(invalidDataPoint)); Assert.assertNull(getWsClient().waitForUpdate(3000)); AttributeKvEntry dataPoint1 = new BaseAttributeKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("serverAttributeKey", 42L)); - List tsData = Arrays.asList(dataPoint1); + List tsData = List.of(dataPoint1); sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, tsData); String msg = getWsClient().waitForUpdate(); @@ -761,16 +760,16 @@ public class WebsocketApiTest extends AbstractControllerTest { List listData = update.getUpdate(); Assert.assertNotNull(listData); Assert.assertEquals(1, listData.size()); - Assert.assertEquals(device.getId(), listData.get(0).getEntityId()); - Assert.assertNotNull(listData.get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE)); - TsValue tsValue = listData.get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey"); + Assert.assertEquals(device.getId(), listData.getFirst().getEntityId()); + Assert.assertNotNull(listData.getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE)); + TsValue tsValue = listData.getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey"); Assert.assertEquals(new TsValue(dataPoint1.getLastUpdateTs(), dataPoint1.getValueAsString()), tsValue); now = System.currentTimeMillis(); AttributeKvEntry dataPoint2 = new BaseAttributeKvEntry(now, new LongDataEntry("serverAttributeKey", 52L)); getWsClient().registerWaitForUpdate(); - sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, Arrays.asList(dataPoint2)); + sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, List.of(dataPoint2)); msg = getWsClient().waitForUpdate(); Assert.assertNotNull(msg); @@ -779,20 +778,20 @@ public class WebsocketApiTest extends AbstractControllerTest { List eData = update.getUpdate(); Assert.assertNotNull(eData); Assert.assertEquals(1, eData.size()); - Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); - Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE)); - tsValue = eData.get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey"); + Assert.assertEquals(device.getId(), eData.getFirst().getEntityId()); + Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE)); + tsValue = eData.getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey"); Assert.assertEquals(new TsValue(dataPoint2.getLastUpdateTs(), dataPoint2.getValueAsString()), tsValue); - //Sending update from the past, while latest value has new timestamp; + //Sending update from the past, while the latest value has new timestamp; getWsClient().registerWaitForUpdate(); - sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, Arrays.asList(dataPoint1)); + sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, List.of(dataPoint1)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); Assert.assertNull(msg); //Sending duplicate update again getWsClient().registerWaitForUpdate(); - sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, Arrays.asList(dataPoint2)); + sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, List.of(dataPoint2)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); Assert.assertNull(msg); } @@ -812,23 +811,23 @@ public class WebsocketApiTest extends AbstractControllerTest { PageData pageData = update.getData(); Assert.assertNotNull(pageData); Assert.assertEquals(1, pageData.getData().size()); - Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId()); - Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey")); - Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getTs()); - Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getValue()); - Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey")); - Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey").getTs()); - Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey").getValue()); - Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey")); - Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey").getTs()); - Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey").getValue()); - Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey")); - Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey").getTs()); - Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey").getValue()); + Assert.assertEquals(device.getId(), pageData.getData().getFirst().getEntityId()); + Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey")); + Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getTs()); + Assert.assertEquals("", pageData.getData().getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getValue()); + Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey")); + Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey").getTs()); + Assert.assertEquals("", pageData.getData().getFirst().getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey").getValue()); + Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey")); + Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey").getTs()); + Assert.assertEquals("", pageData.getData().getFirst().getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey").getValue()); + Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey")); + Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey").getTs()); + Assert.assertEquals("", pageData.getData().getFirst().getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey").getValue()); getWsClient().registerWaitForUpdate(); AttributeKvEntry dataPoint1 = new BaseAttributeKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("serverAttributeKey", 42L)); - List tsData = Arrays.asList(dataPoint1); + List tsData = List.of(dataPoint1); sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, tsData); @@ -839,78 +838,78 @@ public class WebsocketApiTest extends AbstractControllerTest { List eData = update.getUpdate(); Assert.assertNotNull(eData); Assert.assertEquals(1, eData.size()); - Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); - Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE)); - TsValue attrValue = eData.get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey"); + Assert.assertEquals(device.getId(), eData.getFirst().getEntityId()); + Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE)); + TsValue attrValue = eData.getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey"); Assert.assertEquals(new TsValue(dataPoint1.getLastUpdateTs(), dataPoint1.getValueAsString()), attrValue); - //Sending update from the past, while latest value has new timestamp; + //Sending update from the past, while the latest value has new timestamp; getWsClient().registerWaitForUpdate(); - sendAttributes(device, TbAttributeSubscriptionScope.SHARED_SCOPE, Arrays.asList(dataPoint1)); + sendAttributes(device, TbAttributeSubscriptionScope.SHARED_SCOPE, List.of(dataPoint1)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); Assert.assertNull(msg); //Sending duplicate update again getWsClient().registerWaitForUpdate(); - sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, Arrays.asList(dataPoint1)); + sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, List.of(dataPoint1)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); Assert.assertNull(msg); //Sending update from the past, while latest value has new timestamp; getWsClient().registerWaitForUpdate(); AttributeKvEntry dataPoint2 = new BaseAttributeKvEntry(now, new LongDataEntry("sharedAttributeKey", 42L)); - sendAttributes(device, TbAttributeSubscriptionScope.SHARED_SCOPE, Arrays.asList(dataPoint2)); + sendAttributes(device, TbAttributeSubscriptionScope.SHARED_SCOPE, List.of(dataPoint2)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); update = JacksonUtil.fromString(msg, EntityDataUpdate.class); Assert.assertEquals(1, update.getCmdId()); eData = update.getUpdate(); Assert.assertNotNull(eData); Assert.assertEquals(1, eData.size()); - Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); - Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.SHARED_ATTRIBUTE)); - attrValue = eData.get(0).getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey"); + Assert.assertEquals(device.getId(), eData.getFirst().getEntityId()); + Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.SHARED_ATTRIBUTE)); + attrValue = eData.getFirst().getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey"); Assert.assertEquals(new TsValue(dataPoint2.getLastUpdateTs(), dataPoint2.getValueAsString()), attrValue); getWsClient().registerWaitForUpdate(); AttributeKvEntry dataPoint3 = new BaseAttributeKvEntry(now, new LongDataEntry("clientAttributeKey", 42L)); - sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, Arrays.asList(dataPoint3)); + sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, List.of(dataPoint3)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); update = JacksonUtil.fromString(msg, EntityDataUpdate.class); Assert.assertEquals(1, update.getCmdId()); eData = update.getUpdate(); Assert.assertNotNull(eData); Assert.assertEquals(1, eData.size()); - Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); - Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE)); - attrValue = eData.get(0).getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey"); + Assert.assertEquals(device.getId(), eData.getFirst().getEntityId()); + Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE)); + attrValue = eData.getFirst().getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey"); Assert.assertEquals(new TsValue(dataPoint3.getLastUpdateTs(), dataPoint3.getValueAsString()), attrValue); getWsClient().registerWaitForUpdate(); AttributeKvEntry dataPoint4 = new BaseAttributeKvEntry(now, new LongDataEntry("anyAttributeKey", 42L)); - sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, Arrays.asList(dataPoint4)); + sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, List.of(dataPoint4)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); update = JacksonUtil.fromString(msg, EntityDataUpdate.class); Assert.assertEquals(1, update.getCmdId()); eData = update.getUpdate(); Assert.assertNotNull(eData); Assert.assertEquals(1, eData.size()); - Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); - Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.ATTRIBUTE)); - attrValue = eData.get(0).getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey"); + Assert.assertEquals(device.getId(), eData.getFirst().getEntityId()); + Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.ATTRIBUTE)); + attrValue = eData.getFirst().getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey"); Assert.assertEquals(new TsValue(dataPoint4.getLastUpdateTs(), dataPoint4.getValueAsString()), attrValue); getWsClient().registerWaitForUpdate(); AttributeKvEntry dataPoint5 = new BaseAttributeKvEntry(now, new LongDataEntry("anyAttributeKey", 43L)); - sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, Arrays.asList(dataPoint5)); + sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, List.of(dataPoint5)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); update = JacksonUtil.fromString(msg, EntityDataUpdate.class); Assert.assertEquals(1, update.getCmdId()); eData = update.getUpdate(); Assert.assertNotNull(eData); Assert.assertEquals(1, eData.size()); - Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); - Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.ATTRIBUTE)); - attrValue = eData.get(0).getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey"); + Assert.assertEquals(device.getId(), eData.getFirst().getEntityId()); + Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.ATTRIBUTE)); + attrValue = eData.getFirst().getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey"); Assert.assertEquals(new TsValue(dataPoint5.getLastUpdateTs(), dataPoint5.getValueAsString()), attrValue); } @@ -971,7 +970,7 @@ public class WebsocketApiTest extends AbstractControllerTest { .tenantId(device.getTenantId()) .entityId(device.getId()) .entries(tsData) - .callback(new FutureCallback() { + .callback(new FutureCallback<>() { @Override public void onSuccess(@Nullable Void result) { log.debug("sendTelemetry callback onSuccess"); diff --git a/application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java b/application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java index 053cb6808f..accbbd1d29 100644 --- a/application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java @@ -25,16 +25,25 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.adapter.NativeWebSocketSession; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider; +import org.thingsboard.server.service.security.auth.pat.ApiKeyAuthenticationProvider; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.ws.WebSocketService; import org.thingsboard.server.service.ws.WebSocketSessionRef; +import org.thingsboard.server.service.ws.WebSocketSessionType; import java.io.IOException; +import java.lang.reflect.Method; import java.util.Collection; import java.util.Deque; import java.util.List; import java.util.Random; +import java.util.UUID; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; @@ -184,4 +193,101 @@ class TbWebSocketHandlerTest { assertThat(msgs).map(Integer::parseInt).doesNotHaveDuplicates().hasSize(100); } + private AuthTestFixture createAuthTestFixture() throws IOException { + TbWebSocketHandler handler = spy(new TbWebSocketHandler()); + willDoNothing().given(handler).close(any(), any()); + + ApiKeyAuthenticationProvider apiKeyProvider = mock(ApiKeyAuthenticationProvider.class); + JwtAuthenticationProvider jwtProvider = mock(JwtAuthenticationProvider.class); + WebSocketService wsService = mock(WebSocketService.class); + + ReflectionTestUtils.setField(handler, "apiKeyAuthenticationProvider", apiKeyProvider); + ReflectionTestUtils.setField(handler, "authenticationProvider", jwtProvider); + ReflectionTestUtils.setField(handler, "webSocketService", wsService); + ReflectionTestUtils.setField(handler, "tenantProfileCache", mock(TbTenantProfileCache.class)); + ReflectionTestUtils.setField(handler, "authTimeoutMs", 10000); + ReflectionTestUtils.invokeMethod(handler, "init"); + + WebSocketSessionRef ref = WebSocketSessionRef.builder() + .sessionId(UUID.randomUUID().toString()) + .sessionType(WebSocketSessionType.GENERAL) + .build(); + + NativeWebSocketSession wsSession = mock(NativeWebSocketSession.class); + Session nativeSess = mock(Session.class); + willReturn(nativeSess).given(wsSession).getNativeSession(Session.class); + RemoteEndpoint.Async async = mock(RemoteEndpoint.Async.class); + willReturn(async).given(nativeSess).getAsyncRemote(); + willReturn("test-session-id").given(wsSession).getId(); + + TbWebSocketHandler.SessionMetaData sessionMd = handler.new SessionMetaData(wsSession, ref); + + return new AuthTestFixture(handler, apiKeyProvider, jwtProvider, ref, sessionMd); + } + + @Test + void processMsg_authenticatesWithApiKey() throws Exception { + AuthTestFixture f = createAuthTestFixture(); + + SecurityUser securityUser = mock(SecurityUser.class, Mockito.RETURNS_DEEP_STUBS); + willReturn(securityUser).given(f.apiKeyProvider).authenticate("my-api-key"); + + String msg = "{\"authCmd\":{\"cmdId\":1,\"apiKey\":\"my-api-key\"},\"cmds\":[]}"; + f.handler.processMsg(f.sessionMd, msg); + + verify(f.apiKeyProvider).authenticate("my-api-key"); + verify(f.jwtProvider, never()).authenticate(anyString()); + assertThat(f.ref.getSecurityCtx()).isSameAs(securityUser); + } + + @Test + void processMsg_authenticatesWithJwtToken() throws Exception { + AuthTestFixture f = createAuthTestFixture(); + + SecurityUser securityUser = mock(SecurityUser.class, Mockito.RETURNS_DEEP_STUBS); + willReturn(securityUser).given(f.jwtProvider).authenticate("my-jwt-token"); + + String msg = "{\"authCmd\":{\"cmdId\":1,\"token\":\"my-jwt-token\"},\"cmds\":[]}"; + f.handler.processMsg(f.sessionMd, msg); + + verify(f.jwtProvider).authenticate("my-jwt-token"); + verify(f.apiKeyProvider, never()).authenticate(anyString()); + assertThat(f.ref.getSecurityCtx()).isSameAs(securityUser); + } + + @Test + void processMsg_apiKeyTakesPrecedenceOverToken() throws Exception { + AuthTestFixture f = createAuthTestFixture(); + + SecurityUser securityUser = mock(SecurityUser.class, Mockito.RETURNS_DEEP_STUBS); + willReturn(securityUser).given(f.apiKeyProvider).authenticate("my-api-key"); + + String msg = "{\"authCmd\":{\"cmdId\":1,\"apiKey\":\"my-api-key\",\"token\":\"my-jwt-token\"},\"cmds\":[]}"; + f.handler.processMsg(f.sessionMd, msg); + + verify(f.apiKeyProvider).authenticate("my-api-key"); + verify(f.jwtProvider, never()).authenticate(anyString()); + assertThat(f.ref.getSecurityCtx()).isSameAs(securityUser); + } + + @Test + void extractQueryParam_parsesCorrectly() throws Exception { + TbWebSocketHandler handler = new TbWebSocketHandler(); + Method method = TbWebSocketHandler.class.getDeclaredMethod("extractQueryParam", String.class, String.class); + method.setAccessible(true); + + assertThat(method.invoke(handler, "token=jwt123", "token")).isEqualTo("jwt123"); + assertThat(method.invoke(handler, "token=jwt123&other=abc123", "token")).isEqualTo("jwt123"); + assertThat(method.invoke(handler, "other=value", "token")).isNull(); + assertThat(method.invoke(handler, "tokenExtra=value", "token")).isNull(); + } + + private record AuthTestFixture( + TbWebSocketHandler handler, + ApiKeyAuthenticationProvider apiKeyProvider, + JwtAuthenticationProvider jwtProvider, + WebSocketSessionRef ref, + TbWebSocketHandler.SessionMetaData sessionMd + ) {} + }