diff --git a/application/pom.xml b/application/pom.xml
index 51c267306d..1926dd5a92 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -290,6 +290,11 @@
rest-client
test
+
+ org.thingsboard.client
+ thingsboard-ce-client
+ test
+
org.springframework.security
spring-security-test
diff --git a/application/src/test/java/org/thingsboard/server/client/AIModelJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/AIModelJavaClientTest.java
new file mode 100644
index 0000000000..5a0963aa3f
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/AIModelJavaClientTest.java
@@ -0,0 +1,168 @@
+/**
+ * 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.model.AiModel;
+import org.thingsboard.client.model.OpenAiChatModelConfig;
+import org.thingsboard.client.model.OpenAiProviderConfig;
+import org.thingsboard.client.model.PageDataAiModel;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@DaoSqlTest
+public class AIModelJavaClientTest extends AbstractJavaClientTest {
+
+ private static final String AI_PREFIX = "AiTest_";
+
+ @Test
+ public void testSaveAndGetAiModel() throws Exception {
+ long ts = System.currentTimeMillis();
+ String name = AI_PREFIX + "save_" + ts;
+
+ AiModel model = buildAiModel(name, "gpt-4o", 0.7);
+ AiModel saved = client.saveAiModel(model);
+ assertNotNull(saved);
+ assertNotNull(saved.getId());
+ assertEquals(name, saved.getName());
+ assertNotNull(saved.getConfiguration());
+
+ // get by id
+ AiModel fetched = client.getAiModelById(saved.getId().getId());
+ assertNotNull(fetched);
+ assertEquals(name, fetched.getName());
+ assertEquals(saved.getId().getId(), fetched.getId().getId());
+ }
+
+ @Test
+ public void testGetAiModelById() throws Exception {
+ long ts = System.currentTimeMillis();
+ AiModel saved = createAiModel("getbyid_" + ts);
+
+ AiModel fetched = client.getAiModelById(saved.getId().getId());
+ assertNotNull(fetched);
+ assertEquals(saved.getName(), fetched.getName());
+ assertEquals(saved.getId().getId(), fetched.getId().getId());
+ }
+
+ @Test
+ public void testUpdateAiModel() throws Exception {
+ long ts = System.currentTimeMillis();
+ AiModel saved = createAiModel("update_" + ts);
+
+ saved.setName(AI_PREFIX + "updated_" + ts);
+ OpenAiChatModelConfig updatedConfig = new OpenAiChatModelConfig();
+ updatedConfig.setModelId("gpt-4o-mini");
+ updatedConfig.setTemperature(0.3);
+ updatedConfig.setMaxOutputTokens(2048);
+ updatedConfig.setMaxRetries(50);
+ OpenAiProviderConfig providerConfig = new OpenAiProviderConfig();
+ providerConfig.setApiKey("test-api-key");
+ providerConfig.setBaseUrl("https://api.openai.com/v1");
+ updatedConfig.setProviderConfig(providerConfig);
+ updatedConfig.setProvider("OPENAI");
+ saved.setConfiguration(updatedConfig);
+
+ AiModel updated = client.saveAiModel(saved);
+ assertNotNull(updated);
+ assertEquals(saved.getId().getId(), updated.getId().getId());
+ assertEquals(AI_PREFIX + "updated_" + ts, updated.getName());
+ }
+
+ @Test
+ public void testDeleteAiModel() throws Exception {
+ long ts = System.currentTimeMillis();
+ AiModel saved = createAiModel("delete_" + ts);
+
+ UUID modelId = saved.getId().getId();
+ client.getAiModelById(modelId);
+
+ Boolean deleted = client.deleteAiModelById(modelId);
+ assertTrue(deleted);
+
+ assertReturns404(() -> client.getAiModelById(modelId));
+ }
+
+ @Test
+ public void testGetAiModels() throws Exception {
+ long ts = System.currentTimeMillis();
+
+ for (int i = 0; i < 3; i++) {
+ createAiModel("list_" + ts + "_" + i);
+ }
+
+ PageDataAiModel page = client.getAiModels(100, 0, AI_PREFIX + "list_" + ts, null, null);
+ assertNotNull(page);
+ assertEquals(3, page.getTotalElements().intValue());
+ for (AiModel m : page.getData()) {
+ assertTrue(m.getName().startsWith(AI_PREFIX + "list_" + ts));
+ }
+ }
+
+ @Test
+ public void testGetAiModelById_notFound() {
+ UUID nonExistentId = UUID.randomUUID();
+ assertReturns404(() -> client.getAiModelById(nonExistentId));
+ }
+
+ @Test
+ public void testGetAiModelsPagination() throws Exception {
+ long ts = System.currentTimeMillis();
+
+ for (int i = 0; i < 5; i++) {
+ createAiModel("paged_" + ts + "_" + i);
+ }
+
+ PageDataAiModel page1 = client.getAiModels(2, 0, AI_PREFIX + "paged_" + ts, null, null);
+ assertNotNull(page1);
+ assertEquals(5, page1.getTotalElements().intValue());
+ assertEquals(3, page1.getTotalPages().intValue());
+ assertEquals(2, page1.getData().size());
+ assertTrue(page1.getHasNext());
+
+ PageDataAiModel lastPage = client.getAiModels(2, 2, AI_PREFIX + "paged_" + ts, null, null);
+ assertEquals(1, lastPage.getData().size());
+ assertFalse(lastPage.getHasNext());
+ }
+
+ private AiModel buildAiModel(String name, String modelId, double temperature) {
+ OpenAiChatModelConfig config = new OpenAiChatModelConfig();
+ config.setModelId(modelId);
+ config.setTemperature(temperature);
+ config.setMaxRetries(50);
+ OpenAiProviderConfig openAiProviderConfig = new OpenAiProviderConfig();
+ openAiProviderConfig.setApiKey("test-api-key");
+ openAiProviderConfig.setBaseUrl("https://api.openai.com/v1");
+ config.setProviderConfig(openAiProviderConfig);
+ config.setProvider("OPENAI");
+
+ AiModel model = new AiModel();
+ model.setName(name);
+ model.setConfiguration(config);
+ return model;
+ }
+
+ private AiModel createAiModel(String suffix) throws Exception {
+ return client.saveAiModel(buildAiModel(AI_PREFIX + suffix, "gpt-4o", 0.7));
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/AbstractJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/AbstractJavaClientTest.java
new file mode 100644
index 0000000000..8323915184
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/AbstractJavaClientTest.java
@@ -0,0 +1,127 @@
+/**
+ * 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.client;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.After;
+import org.junit.Before;
+import org.thingsboard.client.ApiException;
+import org.thingsboard.client.ThingsboardClient;
+import org.thingsboard.client.model.ActivateUserRequest;
+import org.thingsboard.client.model.Authority;
+import org.thingsboard.client.model.JwtPair;
+import org.thingsboard.client.model.User;
+import org.thingsboard.client.model.UserId;
+import org.thingsboard.server.common.data.util.ThrowingRunnable;
+import org.thingsboard.server.controller.AbstractControllerTest;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+@Slf4j
+public abstract class AbstractJavaClientTest extends AbstractControllerTest {
+
+ protected static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+ protected static final ObjectMapper MAPPER = new ObjectMapper();
+
+ protected static final String TEST_PREFIX = "JavaClientTestDevice_";
+ protected static final String TEST_PREFIX_2 = "JavaClientTestDevice2_";
+ protected static final String CUSTOMER_USERNAME = "javaClientCustomer@thingsboard.org";
+ protected static final String TENANT_ADMIN_USERNAME = "javaClientTenant@thingsboard.org";
+ protected static final String TEST_PASSWORD = "password123";
+
+ protected ThingsboardClient client;
+
+ // FQN for Tenant/Customer to avoid collision with AbstractWebTest fields
+ protected org.thingsboard.client.model.Tenant savedClientTenant;
+ protected User clientTenantAdmin;
+ protected org.thingsboard.client.model.Customer savedClientCustomer;
+ protected User savedClientCustomerUser;
+
+ @Before
+ public void setUpJavaClient() throws Exception {
+ client = ThingsboardClient.builder()
+ .url("http://localhost:" + wsPort)
+ .build();
+ client.login("sysadmin@thingsboard.org", "sysadmin");
+
+ org.thingsboard.client.model.Tenant tenant = new org.thingsboard.client.model.Tenant();
+ tenant.setTitle("Java client test tenant");
+ savedClientTenant = client.saveTenant(tenant);
+
+ clientTenantAdmin = new User();
+ clientTenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+ clientTenantAdmin.setTenantId(savedClientTenant.getId());
+ clientTenantAdmin.setEmail(TENANT_ADMIN_USERNAME);
+ clientTenantAdmin = client.saveUser(clientTenantAdmin, "false");
+ activateUserAndAuthorize(clientTenantAdmin);
+
+ org.thingsboard.client.model.Customer customer = new org.thingsboard.client.model.Customer();
+ customer.setTitle("Java client test customer");
+ customer.setTenantId(savedClientTenant.getId());
+ savedClientCustomer = client.saveCustomer(customer, null, null, null);
+
+ User customerUser = new User();
+ customerUser.setAuthority(Authority.CUSTOMER_USER);
+ customerUser.setTenantId(savedClientTenant.getId());
+ customerUser.setCustomerId(savedClientCustomer.getId());
+ customerUser.setEmail(CUSTOMER_USERNAME);
+ savedClientCustomerUser = client.saveUser(customerUser, "false");
+ activateUser(savedClientCustomerUser.getId(), "password123", false);
+ }
+
+ @After
+ public void tearDownJavaClient() {
+ client.login("sysadmin@thingsboard.org", "sysadmin");
+ client.deleteTenant(savedClientTenant.getId().getId().toString());
+ }
+
+ protected String getBaseUrl() {
+ return "http://localhost:" + wsPort;
+ }
+
+ protected void activateUserAndAuthorize(User user) throws ApiException {
+ JwtPair jwtPair = activateUser(user.getId(), TEST_PASSWORD, false);
+ client.setToken(jwtPair.getToken());
+ }
+
+ protected JwtPair activateUser(UserId userId, String password, boolean sendActivationMail) throws ApiException {
+ ActivateUserRequest activateRequest = new ActivateUserRequest();
+ activateRequest.setActivateToken(getActivateToken(userId));
+ activateRequest.setPassword(password);
+ return client.activateUser(activateRequest, sendActivationMail);
+ }
+
+ protected String getActivateToken(UserId userId) throws ApiException {
+ String activateTokenRegex = "/api/noauth/activate?activateToken=";
+ String activationLink = client.getActivationLink(userId.getId().toString());
+ return activationLink.substring(activationLink.lastIndexOf(activateTokenRegex) + activateTokenRegex.length());
+ }
+
+ protected void assertReturns404(ThrowingRunnable operation) {
+ try {
+ operation.run();
+ fail("Expected ApiException with 404 status code");
+ } catch (ApiException exception) {
+ assertEquals("Expected 404 status code but got " + exception.getCode(),
+ 404, exception.getCode());
+ } catch (Exception e) {
+ fail("Expected ApiException but got " + e.getClass().getName() + ": " + e.getMessage());
+ }
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/AdminJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/AdminJavaClientTest.java
new file mode 100644
index 0000000000..4c6b3444b0
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/AdminJavaClientTest.java
@@ -0,0 +1,100 @@
+/**
+ * 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.client;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.junit.Test;
+import org.thingsboard.client.model.AdminSettings;
+import org.thingsboard.client.model.FeaturesInfo;
+import org.thingsboard.client.model.JwtSettings;
+import org.thingsboard.client.model.SecuritySettings;
+import org.thingsboard.client.model.SystemInfo;
+import org.thingsboard.client.model.UpdateMessage;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@DaoSqlTest
+public class AdminJavaClientTest extends AbstractJavaClientTest {
+
+ @Test
+ public void testAdminSettingsLifecycle() throws Exception {
+ // authenticate as sysadmin for admin settings management
+ client.login("sysadmin@thingsboard.org", "sysadmin");
+
+ // get mail settings
+ AdminSettings mailSettings = client.getAdminSettings("mail");
+ assertNotNull(mailSettings);
+ assertNotNull(mailSettings.getKey());
+ assertEquals("mail", mailSettings.getKey());
+ assertNotNull(mailSettings.getJsonValue());
+
+ // get general settings
+ AdminSettings generalSettings = client.getAdminSettings("general");
+ assertNotNull(generalSettings);
+ assertEquals("general", generalSettings.getKey());
+ assertNotNull(generalSettings.getJsonValue());
+ assertNotNull(generalSettings.getJsonValue().get("baseUrl").asText());
+
+ // update general settings and restore
+ ((ObjectNode) generalSettings.getJsonValue()).put("prohibitDifferentUrl", true);
+ AdminSettings updatedGeneralSettings = client.saveAdminSettings(generalSettings);
+ assertTrue(updatedGeneralSettings.getJsonValue().get("prohibitDifferentUrl").asBoolean());
+
+ // get security settings
+ SecuritySettings securitySettings = client.getSecuritySettings();
+ assertNotNull(securitySettings);
+ assertNotNull(securitySettings.getPasswordPolicy());
+ Integer originalMaxAttempts = securitySettings.getMaxFailedLoginAttempts();
+
+ // update security settings
+ securitySettings.setMaxFailedLoginAttempts(10);
+ SecuritySettings updatedSecurity = client.saveSecuritySettings(securitySettings);
+ assertNotNull(updatedSecurity);
+ assertEquals(10, updatedSecurity.getMaxFailedLoginAttempts().intValue());
+
+ // restore original security settings
+ updatedSecurity.setMaxFailedLoginAttempts(originalMaxAttempts);
+ client.saveSecuritySettings(updatedSecurity);
+
+ // get JWT settings
+ JwtSettings jwtSettings = client.getJwtSettings();
+ assertNotNull(jwtSettings);
+ assertNotNull(jwtSettings.getTokenExpirationTime());
+ assertNotNull(jwtSettings.getRefreshTokenExpTime());
+ assertEquals("thingsboard.io", jwtSettings.getTokenIssuer());
+ assertNotNull(jwtSettings.getTokenSigningKey());
+
+ // get system info
+ SystemInfo systemInfo = client.getSystemInfo();
+ assertNotNull(systemInfo);
+
+ // get features info
+ FeaturesInfo featuresInfo = client.getFeaturesInfo();
+ assertNotNull(featuresInfo);
+ assertFalse(featuresInfo.getSmsEnabled());
+ assertTrue(featuresInfo.getOauthEnabled());
+
+ // check updates
+ UpdateMessage updateMessage = client.checkUpdates();
+ assertNotNull(updateMessage);
+ assertNotNull(updateMessage.getCurrentVersion());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/AlarmCommentJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/AlarmCommentJavaClientTest.java
new file mode 100644
index 0000000000..6b519d5fd0
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/AlarmCommentJavaClientTest.java
@@ -0,0 +1,106 @@
+/**
+ * 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.client;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.junit.Test;
+import org.thingsboard.client.model.Alarm;
+import org.thingsboard.client.model.AlarmComment;
+import org.thingsboard.client.model.AlarmCommentInfo;
+import org.thingsboard.client.model.AlarmSeverity;
+import org.thingsboard.client.model.Device;
+import org.thingsboard.client.model.PageDataAlarmCommentInfo;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@DaoSqlTest
+public class AlarmCommentJavaClientTest extends AbstractJavaClientTest {
+
+ @Test
+ public void testAlarmComments() throws Exception {
+ long timestamp = System.currentTimeMillis();
+
+ // Create device for alarm
+ Device device = new Device();
+ device.setName("Device_For_Comments_" + timestamp);
+ device.setType("default");
+ Device createdDevice = client.saveDevice(device, null, null, null, null);
+
+ // Create alarm
+ Alarm alarm = new Alarm();
+ alarm.setType("Temperature Alarm");
+ alarm.setSeverity(AlarmSeverity.CRITICAL);
+ alarm.setOriginator(createdDevice.getId());
+
+ Alarm createdAlarm = client.saveAlarm(alarm);
+ String alarmId = createdAlarm.getId().getId().toString();
+
+ List createdComments = new ArrayList<>();
+
+ // Create multiple comments
+ for (int i = 0; i < 5; i++) {
+ AlarmComment alarmComment = new AlarmComment();
+ String message = "Test comment #" + i + " at " + timestamp;
+ ObjectNode comment = OBJECT_MAPPER.createObjectNode().put("message", message);
+ alarmComment.setComment(comment);
+
+ AlarmComment commentInfo = client.saveAlarmComment(alarmId, alarmComment);
+
+ assertNotNull(commentInfo);
+ assertNotNull(commentInfo.getId());
+ JsonNode commentValue = commentInfo.getComment();
+ assertEquals(message, commentValue.get("message").asText());
+ assertNotNull(commentInfo.getCreatedTime());
+
+ createdComments.add(commentInfo);
+ }
+
+ // Get all comments for the alarm
+ PageDataAlarmCommentInfo allComments = client.getAlarmComments(alarmId, 100, 0, null, null);
+ assertEquals("Expected 5 comments", 5, allComments.getData().size());
+
+ // Update a comment
+ AlarmComment commentToUpdate = createdComments.get(2);
+ JsonNode comment = commentToUpdate.getComment();
+ ((ObjectNode) comment).put("message", "New comment");
+ commentToUpdate.setComment(comment);
+
+ AlarmComment updatedComment = client.saveAlarmComment(alarmId, commentToUpdate);
+ assertEquals("New comment", updatedComment.getComment().get("message").asText());
+
+ // Delete a comment
+ UUID commentToDeleteId = createdComments.get(0).getId().getId();
+
+ client.deleteAlarmComment(alarmId, commentToDeleteId.toString());
+
+ // Verify comment was updated to "deleted"
+ PageDataAlarmCommentInfo commentsAfterDelete = client.getAlarmComments(alarmId, 100, 0, null, null);
+ List data = commentsAfterDelete.getData();
+ AlarmCommentInfo deletedComment = data.stream()
+ .filter(alarmCommentInfo -> alarmCommentInfo.getId().getId().equals(commentToDeleteId))
+ .findFirst()
+ .get();
+ assertEquals("User " + clientTenantAdmin.getEmail() + " deleted his comment", deletedComment.getComment().get("text").asText());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/AlarmJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/AlarmJavaClientTest.java
new file mode 100644
index 0000000000..61666297dc
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/AlarmJavaClientTest.java
@@ -0,0 +1,163 @@
+/**
+ * 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.model.Alarm;
+import org.thingsboard.client.model.AlarmInfo;
+import org.thingsboard.client.model.AlarmSeverity;
+import org.thingsboard.client.model.AlarmStatus;
+import org.thingsboard.client.model.Device;
+import org.thingsboard.client.model.EntitySubtype;
+import org.thingsboard.client.model.EntityType;
+import org.thingsboard.client.model.PageDataAlarmInfo;
+import org.thingsboard.client.model.PageDataEntitySubtype;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+@DaoSqlTest
+public class AlarmJavaClientTest extends AbstractJavaClientTest {
+
+ @Test
+ public void testAlarmLifecycle() throws Exception {
+ long timestamp = System.currentTimeMillis();
+ List createdAlarms = new ArrayList<>();
+
+ // First, create devices to attach alarms to
+ Device device1 = new Device();
+ device1.setName("Device_For_Alarm_" + timestamp + "_1");
+ device1.setType("default");
+ Device createdDevice1 = client.saveDevice(device1, null, null, null, null);
+
+ Device device2 = new Device();
+ device2.setName("Device_For_Alarm_" + timestamp + "_2");
+ device2.setType("thermostat");
+ Device createdDevice2 = client.saveDevice(device2, null, null, null, null);
+
+ // Create 2 alarms (1 for each device)
+ for (int i = 0; i < 2; i++) {
+ Alarm alarm = new Alarm();
+ alarm.setType(((i % 2 == 0) ? "Temperature Alarm" : "Connection Alarm"));
+ alarm.setSeverity(((i % 2 == 0) ? AlarmSeverity.CRITICAL : AlarmSeverity.WARNING));
+ alarm.setOriginator((i % 2 == 0) ? createdDevice1.getId() : createdDevice2.getId());
+
+ Alarm createdAlarm = client.saveAlarm(alarm);
+ assertNotNull(createdAlarm);
+ assertNotNull(createdAlarm.getId());
+ assertEquals(alarm.getType(), createdAlarm.getType());
+ assertEquals(alarm.getSeverity(), createdAlarm.getSeverity());
+
+ createdAlarms.add(createdAlarm);
+ }
+
+ // Get all alarms
+ PageDataAlarmInfo allAlarms = client.getAllAlarms(100, 0, null, null, null, null, null, null, null, null, null);
+
+ assertNotNull(allAlarms);
+ assertNotNull(allAlarms.getData());
+ int initialSize = allAlarms.getData().size();
+ assertEquals("Expected at least 2 alarms, but got " + initialSize, 2, initialSize);
+
+ // Get alarms by entity (device1)
+ PageDataAlarmInfo device1Alarms = client.getAlarmsV2(EntityType.DEVICE.toString(), createdDevice1.getId().getId().toString(), 100, 0, null, null, null, null, null, null, null, null, null);
+ assertNotNull(device1Alarms);
+ assertEquals("Expected 1 alarms for device1", 1, device1Alarms.getData().size());
+
+ // Get alarm by id
+ Alarm searchAlarm = createdAlarms.get(0);
+ Alarm fetchedAlarm = client.getAlarmById(searchAlarm.getId().getId().toString());
+ assertEquals(searchAlarm.getType(), fetchedAlarm.getType());
+ assertEquals(searchAlarm.getSeverity(), fetchedAlarm.getSeverity());
+
+ // Get alarm info
+ AlarmInfo alarmInfo = client.getAlarmInfoById(searchAlarm.getId().getId().toString());
+ assertNotNull(alarmInfo);
+ assertEquals(searchAlarm.getId().getId(), alarmInfo.getId().getId());
+
+ // Acknowledge alarm
+ client.ackAlarm(searchAlarm.getId().getId().toString());
+
+ // Verify alarm is acknowledged
+ Alarm ackedAlarm = client.getAlarmById(searchAlarm.getId().getId().toString());
+ assertEquals(AlarmStatus.ACTIVE_ACK, ackedAlarm.getStatus());
+
+ // Clear alarm
+ client.clearAlarm(searchAlarm.getId().getId().toString());
+
+ // Verify alarm is cleared
+ Alarm clearedAlarm = client.getAlarmById(searchAlarm.getId().getId().toString());
+ assertEquals(AlarmStatus.CLEARED_ACK, clearedAlarm.getStatus());
+
+ // Get highest severity alarm for device
+ AlarmSeverity highestSeverity = client.getHighestAlarmSeverity(EntityType.DEVICE.toString(), createdDevice1.getId().getId().toString(), null, null, null);
+ assertNotNull(highestSeverity);
+ assertEquals(AlarmSeverity.CRITICAL, highestSeverity);
+
+ // Assign alarm to customer
+ client.assignAlarm(createdAlarms.get(0).getId().getId().toString(), clientTenantAdmin.getId().getId().toString());
+
+ // Verify assignment
+ Alarm assignedAlarm = client.getAlarmById(createdAlarms.get(0).getId().getId().toString());
+ assertEquals(clientTenantAdmin.getId().getId(), assignedAlarm.getAssigneeId().getId());
+
+ // Unassign alarm
+ client.unassignAlarm(createdAlarms.get(0).getId().getId().toString());
+
+ // Verify unassignment
+ Alarm unassignedAlarm = client.getAlarmById(createdAlarms.get(0).getId().getId().toString());
+ assertNull(unassignedAlarm.getAssigneeId());
+
+ // Get alarm types
+ PageDataEntitySubtype pageDataEntitySubtype = client.getAlarmTypes(100, 0, null, null);
+ assertEquals(2, pageDataEntitySubtype.getData().size());
+ List alarmTypes = pageDataEntitySubtype.getData().stream()
+ .map(EntitySubtype::getType)
+ .collect(Collectors.toList());
+ assertTrue(alarmTypes.containsAll(List.of("Temperature Alarm", "Connection Alarm")));
+
+ // Get alarms V2 (alternative endpoint)
+ PageDataAlarmInfo alarmsV2 = client.getAlarmsV2(EntityType.DEVICE.toString(), createdDevice1.getId().getId().toString(), 100, 0, null, null, null, null, null, null, null, null, null);
+ assertNotNull(alarmsV2);
+ assertEquals(1, alarmsV2.getData().size());
+
+ // Get all alarms V2
+ PageDataAlarmInfo allAlarmsV2 = client.getAllAlarmsV2(100, 0, null, null, null, null, null, null, null, null, null);
+ assertEquals(2, allAlarmsV2.getData().size());
+
+ // Delete alarm
+ UUID alarmToDeleteId = createdAlarms.get(0).getId().getId();
+ client.deleteAlarm(alarmToDeleteId.toString());
+
+ // Verify the alarm is deleted (should return 404)
+ assertReturns404(() ->
+ client.getAlarmById(alarmToDeleteId.toString())
+ );
+
+ // Verify count after deletion
+ PageDataAlarmInfo alarmsAfterDelete = client.getAllAlarms(100, 0, null, null, null, null, null, null, null, null, null);
+ assertEquals(initialSize - 1, alarmsAfterDelete.getData().size());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/AssetJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/AssetJavaClientTest.java
new file mode 100644
index 0000000000..27cde622a4
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/AssetJavaClientTest.java
@@ -0,0 +1,84 @@
+/**
+ * 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.model.Asset;
+import org.thingsboard.client.model.PageDataAsset;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@DaoSqlTest
+public class AssetJavaClientTest extends AbstractJavaClientTest {
+
+ @Test
+ public void testAssetLifecycle() throws Exception {
+ long timestamp = System.currentTimeMillis();
+ List createdAssets = new ArrayList<>();
+
+ // create 20 assets
+ for (int i = 0; i < 20; i++) {
+ Asset asset = new Asset();
+ String assetName = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i;
+ asset.setName(assetName);
+ asset.setLabel("Test Asset " + i);
+ asset.setType(((i % 2 == 0) ? "default" : "building"));
+
+ Asset createdAsset = client.saveAsset(asset, null, null, null);
+ assertNotNull(createdAsset);
+ assertNotNull(createdAsset.getId());
+ assertEquals(assetName, createdAsset.getName());
+
+ createdAssets.add(createdAsset);
+ }
+
+ // find all, check count
+ PageDataAsset allAssets = client.getTenantAssets(100, 0, null, null, null, null);
+
+ assertNotNull(allAssets);
+ assertNotNull(allAssets.getData());
+ int initialSize = allAssets.getData().size();
+ assertEquals("Expected at least 20 assets, but got " + allAssets.getData().size(), 20, initialSize);
+
+ //find all with search text, check count
+ PageDataAsset allAssetsBySearchText = client.getTenantAssets(100, 0, null, TEST_PREFIX_2, null, null);
+ assertEquals("Expected exactly 10 test assets", 10, allAssetsBySearchText.getData().size());
+
+ // find by id
+ Asset searchAsset = createdAssets.get(10);
+ Asset asset = client.getAssetById(searchAsset.getId().getId().toString());
+ assertEquals(searchAsset.getName(), asset.getName());
+
+ // delete asset
+ UUID assetToDeleteId = createdAssets.get(0).getId().getId();
+ client.deleteAsset(assetToDeleteId.toString());
+
+ // Verify the asset is deleted
+ PageDataAsset assetsAfterDelete = client.getTenantAssets(100, 0, null, null, null, null);
+ assertEquals(initialSize - 1, assetsAfterDelete.getData().size());
+
+ assertReturns404(() ->
+ client.getAssetById(assetToDeleteId.toString())
+ );
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/AssetProfileJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/AssetProfileJavaClientTest.java
new file mode 100644
index 0000000000..7825a047bd
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/AssetProfileJavaClientTest.java
@@ -0,0 +1,135 @@
+/**
+ * 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.model.AssetProfile;
+import org.thingsboard.client.model.AssetProfileInfo;
+import org.thingsboard.client.model.EntityInfo;
+import org.thingsboard.client.model.PageDataAssetProfile;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@DaoSqlTest
+public class AssetProfileJavaClientTest extends AbstractJavaClientTest {
+
+ @Test
+ public void testAssetProfileLifecycle() throws Exception {
+ long timestamp = System.currentTimeMillis();
+ List createdProfiles = new ArrayList<>();
+
+ // Get initial count (there should be a default profile)
+ PageDataAssetProfile initialProfiles = client.getAssetProfiles(100, 0, null, null, null);
+ assertNotNull(initialProfiles);
+ int initialSize = initialProfiles.getData().size();
+ assertTrue("Expected at least 1 default asset profile", initialSize == 1);
+
+ // Get default asset profile info
+ AssetProfileInfo defaultProfileInfo = client.getDefaultAssetProfileInfo();
+ assertNotNull(defaultProfileInfo);
+ assertEquals(defaultProfileInfo.getName(), "default");
+
+ // Create multiple asset profiles
+ for (int i = 0; i < 5; i++) {
+ AssetProfile profile = new AssetProfile();
+ profile.setName("Test Asset Profile " + timestamp + "_" + i);
+ profile.setDescription("Test description " + i);
+
+ AssetProfile created = client.saveAssetProfile(profile);
+ assertNotNull(created);
+ assertNotNull(created.getId());
+ assertEquals(profile.getName(), created.getName());
+ assertEquals(profile.getDescription(), created.getDescription());
+ assertFalse(created.getDefault());
+
+ createdProfiles.add(created);
+ }
+
+ // Find all, check count
+ PageDataAssetProfile allProfiles = client.getAssetProfiles(100, 0, null, null, null);
+ assertNotNull(allProfiles);
+ assertEquals(initialSize + 5, allProfiles.getData().size());
+
+ // Find all with text search
+ PageDataAssetProfile filteredProfiles = client.getAssetProfiles(100, 0, "Test Asset Profile " + timestamp, null, null);
+ assertEquals(5, filteredProfiles.getData().size());
+
+ // Get by id
+ AssetProfile searchProfile = createdProfiles.get(2);
+ AssetProfile fetchedProfile = client.getAssetProfileById(searchProfile.getId().getId().toString(), false);
+ assertEquals(searchProfile.getName(), fetchedProfile.getName());
+ assertEquals(searchProfile.getDescription(), fetchedProfile.getDescription());
+
+ // Update asset profile
+ fetchedProfile.setDescription("Updated description");
+ AssetProfile updatedProfile = client.saveAssetProfile(fetchedProfile);
+ assertEquals("Updated description", updatedProfile.getDescription());
+ assertEquals(fetchedProfile.getName(), updatedProfile.getName());
+
+ // Get asset profile info by id
+ AssetProfileInfo profileInfo = client.getAssetProfileInfoById(searchProfile.getId().getId().toString());
+ assertNotNull(profileInfo);
+ assertEquals(searchProfile.getName(), profileInfo.getName());
+
+ // Get asset profile infos (paginated)
+ PageDataAssetProfile profileInfos = client.getAssetProfiles(100, 0, null, null, null);
+ assertNotNull(profileInfos);
+ assertEquals(initialSize + 5, profileInfos.getData().size());
+
+ // Set a profile as default
+ AssetProfile profileToSetDefault = createdProfiles.get(1);
+ AssetProfile newDefault = client.setDefaultAssetProfile(profileToSetDefault.getId().getId().toString());
+ assertNotNull(newDefault);
+ assertTrue(newDefault.getDefault());
+
+ // Verify default profile info now points to the new default
+ AssetProfileInfo newDefaultInfo = client.getDefaultAssetProfileInfo();
+ assertEquals(profileToSetDefault.getName(), newDefaultInfo.getName());
+
+ // Get asset profile names
+ List profileNames = client.getAssetProfileNames(false);
+ assertNotNull(profileNames);
+ assertEquals(createdProfiles.size() + 1, profileNames.size());
+
+ // Delete asset profile (cannot delete the default one, so delete a non-default one)
+ UUID profileToDeleteId = createdProfiles.get(0).getId().getId();
+ client.deleteAssetProfile(profileToDeleteId.toString());
+
+ // Verify the profile is deleted
+ assertReturns404(() ->
+ client.getAssetProfileById(profileToDeleteId.toString(), false));
+
+ // Verify count after deletion
+ PageDataAssetProfile profilesAfterDelete = client.getAssetProfiles(100, 0, null, null, null);
+ assertEquals(initialSize + 4, profilesAfterDelete.getData().size());
+
+ // Restore original default profile
+ AssetProfile originalDefault = initialProfiles.getData().stream()
+ .filter(AssetProfile::getDefault)
+ .findFirst()
+ .orElseThrow();
+ client.setDefaultAssetProfile(originalDefault.getId().getId().toString());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/CalculatedFieldJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/CalculatedFieldJavaClientTest.java
new file mode 100644
index 0000000000..6f79f0d347
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/CalculatedFieldJavaClientTest.java
@@ -0,0 +1,286 @@
+/**
+ * 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.model.AlarmCalculatedFieldConfiguration;
+import org.thingsboard.client.model.AlarmConditionValueAlarmRuleSchedule;
+import org.thingsboard.client.model.AlarmRuleDefinition;
+import org.thingsboard.client.model.AlarmRuleSimpleCondition;
+import org.thingsboard.client.model.AlarmRuleSpecificTimeSchedule;
+import org.thingsboard.client.model.AlarmSeverity;
+import org.thingsboard.client.model.Argument;
+import org.thingsboard.client.model.ArgumentType;
+import org.thingsboard.client.model.CalculatedField;
+import org.thingsboard.client.model.CalculatedFieldType;
+import org.thingsboard.client.model.Device;
+import org.thingsboard.client.model.EntityType;
+import org.thingsboard.client.model.PageDataCalculatedField;
+import org.thingsboard.client.model.ReferencedEntityKey;
+import org.thingsboard.client.model.SimpleCalculatedFieldConfiguration;
+import org.thingsboard.client.model.TbelAlarmConditionExpression;
+import org.thingsboard.client.model.TimeSeriesOutput;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@DaoSqlTest
+public class CalculatedFieldJavaClientTest extends AbstractJavaClientTest {
+
+ @Test
+ public void testCalculatedFieldLifecycle() throws Exception {
+ long timestamp = System.currentTimeMillis();
+ List createdFields = new ArrayList<>();
+
+ // create devices to attach calculated fields to
+ Device device1 = new Device();
+ device1.setName("CalcFieldDevice1_" + timestamp);
+ device1.setType("default");
+ Device createdDevice1 = client.saveDevice(device1, null, null, null, null);
+
+ Device device2 = new Device();
+ device2.setName("CalcFieldDevice2_" + timestamp);
+ device2.setType("default");
+ Device createdDevice2 = client.saveDevice(device2, null, null, null, null);
+
+ // create calculated fields on device1
+ for (int i = 0; i < 5; i++) {
+ CalculatedField cf = new CalculatedField();
+ cf.setName(TEST_PREFIX + "CalcField_" + timestamp + "_" + i);
+ cf.setType(CalculatedFieldType.SIMPLE);
+
+ cf.setEntityId(createdDevice1.getId());
+
+ SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
+
+ Argument arg = new Argument();
+ ReferencedEntityKey refKey = new ReferencedEntityKey();
+ refKey.setKey("temperature");
+ refKey.setType(ArgumentType.TS_LATEST);
+ arg.setRefEntityKey(refKey);
+ config.putArgumentsItem("temp", arg);
+
+ config.setExpression("temp * " + (i + 1));
+
+ TimeSeriesOutput output = new TimeSeriesOutput();
+ output.setName("scaledTemp_" + i);
+ config.setOutput(output);
+
+ cf.setConfiguration(config);
+
+ CalculatedField created = client.saveCalculatedField(cf);
+ assertNotNull(created);
+ assertNotNull(created.getId());
+ assertEquals(cf.getName(), created.getName());
+ assertEquals(CalculatedFieldType.SIMPLE, created.getType());
+
+ createdFields.add(created);
+ }
+
+ // create calculated fields on device2
+ for (int i = 0; i < 3; i++) {
+ CalculatedField cf = new CalculatedField();
+ cf.setName(TEST_PREFIX + "CalcField2_" + timestamp + "_" + i);
+ cf.setType(CalculatedFieldType.SIMPLE);
+ cf.setEntityId(createdDevice2.getId());
+
+ SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
+
+ Argument arg = new Argument();
+ ReferencedEntityKey refKey = new ReferencedEntityKey();
+ refKey.setKey("humidity");
+ refKey.setType(ArgumentType.TS_LATEST);
+ arg.setRefEntityKey(refKey);
+ config.putArgumentsItem("hum", arg);
+
+ config.setExpression("hum + " + i);
+
+ TimeSeriesOutput output = new TimeSeriesOutput();
+ output.setName("adjustedHumidity_" + i);
+ config.setOutput(output);
+
+ cf.setConfiguration(config);
+
+ CalculatedField created = client.saveCalculatedField(cf);
+ assertNotNull(created);
+ createdFields.add(created);
+ }
+
+ // get calculated fields by entity id for device1
+ PageDataCalculatedField device1Fields = client.getCalculatedFieldsByEntityId(
+ EntityType.DEVICE.toString(), createdDevice1.getId().getId().toString(),
+ 100, 0, CalculatedFieldType.SIMPLE, null, null, null);
+ assertNotNull(device1Fields);
+ assertEquals(5, device1Fields.getData().size());
+
+ // get calculated fields by entity id for device2
+ PageDataCalculatedField device2Fields = client.getCalculatedFieldsByEntityId(
+ EntityType.DEVICE.toString(), createdDevice2.getId().getId().toString(),
+ 100, 0, CalculatedFieldType.SIMPLE, null, null, null);
+ assertEquals(3, device2Fields.getData().size());
+
+ // get by id
+ CalculatedField searchField = createdFields.get(2);
+ CalculatedField fetchedField = client.getCalculatedFieldById(searchField.getId().getId().toString());
+ assertEquals(searchField.getName(), fetchedField.getName());
+ assertEquals(searchField.getType(), fetchedField.getType());
+ assertNotNull(fetchedField.getConfiguration());
+ SimpleCalculatedFieldConfiguration fetchedConfig =
+ (SimpleCalculatedFieldConfiguration) fetchedField.getConfiguration();
+ assertEquals("temp * 3", fetchedConfig.getExpression());
+
+ // update calculated field
+ fetchedField.setName(fetchedField.getName() + "_updated");
+ fetchedConfig.setExpression("temp * 100");
+ CalculatedField updatedField = client.saveCalculatedField(fetchedField);
+ assertEquals(fetchedField.getName(), updatedField.getName());
+ SimpleCalculatedFieldConfiguration updatedConfig =
+ (SimpleCalculatedFieldConfiguration) updatedField.getConfiguration();
+ assertEquals("temp * 100", updatedConfig.getExpression());
+
+ // delete calculated field
+ UUID fieldToDeleteId = createdFields.get(0).getId().getId();
+ client.deleteCalculatedField(fieldToDeleteId.toString());
+
+ // verify deletion
+ assertReturns404(() ->
+ client.getCalculatedFieldById(fieldToDeleteId.toString())
+ );
+
+ PageDataCalculatedField device1FieldsAfterDelete = client.getCalculatedFieldsByEntityId(
+ EntityType.DEVICE.toString(), createdDevice1.getId().getId().toString(),
+ 100, 0, null, null, null, null);
+ assertEquals(4, device1FieldsAfterDelete.getData().size());
+ }
+
+ @Test
+ public void testAlarmCalculatedFieldLifecycle() throws Exception {
+ long timestamp = System.currentTimeMillis();
+
+ // create a device to attach the alarm calculated field to
+ Device device = new Device();
+ device.setName("AlarmCalcFieldDevice_" + timestamp);
+ device.setType("default");
+ Device createdDevice = client.saveDevice(device, null, null, null, null);
+
+ // build the alarm calculated field configuration
+ AlarmCalculatedFieldConfiguration config = new AlarmCalculatedFieldConfiguration();
+
+ // argument: temperature time-series
+ Argument tempArg = new Argument();
+ ReferencedEntityKey refKey = new ReferencedEntityKey();
+ refKey.setKey("temperature");
+ refKey.setType(ArgumentType.TS_LATEST);
+ tempArg.setRefEntityKey(refKey);
+ config.putArgumentsItem("temp", tempArg);
+
+ // create rule: HIGH_TEMPERATURE when temp > 50 (TBEL expression)
+ TbelAlarmConditionExpression createExpression = new TbelAlarmConditionExpression();
+ createExpression.setExpression("return temp > 50;");
+ AlarmRuleSimpleCondition createCondition = new AlarmRuleSimpleCondition();
+ createCondition.setExpression(createExpression);
+ AlarmRuleSpecificTimeSchedule specificTimeSchedule = new AlarmRuleSpecificTimeSchedule().addDaysOfWeekItem(3);
+ AlarmConditionValueAlarmRuleSchedule schedule = new AlarmConditionValueAlarmRuleSchedule().staticValue(specificTimeSchedule);
+ createCondition.setSchedule(schedule);
+ AlarmRuleDefinition createRule = new AlarmRuleDefinition();
+ createRule.setCondition(createCondition);
+ createRule.setAlarmDetails("Temperature is too high: ${temp}");
+ config.setCreateRules(Map.of(
+ AlarmSeverity.CRITICAL.name(), createRule
+ ));
+
+ // clear rule: when temp drops below 30
+ TbelAlarmConditionExpression clearExpression = new TbelAlarmConditionExpression();
+ clearExpression.setExpression("return temp < 30;");
+ AlarmRuleSimpleCondition clearCondition = new AlarmRuleSimpleCondition();
+ clearCondition.setExpression(clearExpression);
+ AlarmRuleDefinition clearRule = new AlarmRuleDefinition();
+ clearRule.setCondition(clearCondition);
+ config.setClearRule(clearRule);
+
+ config.setPropagate(true);
+ config.setPropagateToOwner(false);
+
+ // create calculated field
+ CalculatedField cf = new CalculatedField();
+ cf.setName(TEST_PREFIX + "AlarmCalcField_" + timestamp);
+ cf.setType(CalculatedFieldType.ALARM);
+
+ cf.setEntityId(createdDevice.getId());
+ cf.setConfiguration(config);
+
+ CalculatedField created = client.saveCalculatedField(cf);
+ assertNotNull(created);
+ assertNotNull(created.getId());
+ assertEquals(cf.getName(), created.getName());
+ assertEquals(CalculatedFieldType.ALARM, created.getType());
+ AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) created.getConfiguration();
+ AlarmConditionValueAlarmRuleSchedule createdSchedule = configuration.getCreateRules().get(AlarmSeverity.CRITICAL.name()).getCondition().getSchedule();
+ AlarmRuleSpecificTimeSchedule staticSchedule = (AlarmRuleSpecificTimeSchedule) createdSchedule.getStaticValue();
+ assertEquals(Set.of(3), staticSchedule.getDaysOfWeek());
+
+ // get by id and verify configuration
+ CalculatedField fetched = client.getCalculatedFieldById(created.getId().getId().toString());
+ assertNotNull(fetched);
+ assertEquals(created.getName(), fetched.getName());
+ assertEquals(CalculatedFieldType.ALARM, fetched.getType());
+ assertNotNull(fetched.getConfiguration());
+ AlarmCalculatedFieldConfiguration fetchedConfig =
+ (AlarmCalculatedFieldConfiguration) fetched.getConfiguration();
+ assertNotNull(fetchedConfig.getCreateRules());
+ assertEquals(1, fetchedConfig.getCreateRules().size());
+ assertTrue(fetchedConfig.getCreateRules().containsKey("CRITICAL"));
+ assertNotNull(fetchedConfig.getClearRule());
+ assertEquals(Boolean.TRUE, fetchedConfig.getPropagate());
+
+ // update: add a second create rule for CRITICAL_TEMPERATURE
+ TbelAlarmConditionExpression criticalExpression = new TbelAlarmConditionExpression();
+ criticalExpression.setExpression("return temp > 80;");
+ AlarmRuleSimpleCondition criticalCondition = new AlarmRuleSimpleCondition();
+ criticalCondition.setExpression(criticalExpression);
+ AlarmRuleDefinition criticalRule = new AlarmRuleDefinition();
+ criticalRule.setCondition(criticalCondition);
+ fetchedConfig.putCreateRulesItem(AlarmSeverity.INDETERMINATE.name(), criticalRule);
+ fetched.setConfiguration(fetchedConfig);
+
+ CalculatedField updated = client.saveCalculatedField(fetched);
+ AlarmCalculatedFieldConfiguration updatedConfig =
+ (AlarmCalculatedFieldConfiguration) updated.getConfiguration();
+ assertEquals(2, updatedConfig.getCreateRules().size());
+ assertTrue(updatedConfig.getCreateRules().containsKey("INDETERMINATE"));
+
+ // filter by entity and ALARM type
+ PageDataCalculatedField deviceFields = client.getCalculatedFieldsByEntityId(
+ EntityType.DEVICE.toString(), createdDevice.getId().getId().toString(),
+ 100, 0, CalculatedFieldType.ALARM, null, null, null);
+ assertNotNull(deviceFields);
+ assertEquals(1, deviceFields.getData().size());
+
+ // delete and verify
+ UUID fieldId = created.getId().getId();
+ client.deleteCalculatedField(fieldId.toString());
+ assertReturns404(() -> client.getCalculatedFieldById(fieldId.toString()));
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/CustomerJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/CustomerJavaClientTest.java
new file mode 100644
index 0000000000..dbfe1faf56
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/CustomerJavaClientTest.java
@@ -0,0 +1,113 @@
+/**
+ * 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.model.Customer;
+import org.thingsboard.client.model.Device;
+import org.thingsboard.client.model.PageDataCustomer;
+import org.thingsboard.client.model.PageDataDevice;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@DaoSqlTest
+public class CustomerJavaClientTest extends AbstractJavaClientTest {
+
+ @Test
+ public void testCustomerLifecycle() throws Exception {
+ long timestamp = System.currentTimeMillis();
+ List createdCustomers = new ArrayList<>();
+
+ // create 20 customers
+ for (int i = 0; i < 20; i++) {
+ Customer customer = new Customer();
+ String customerTitle = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i;
+ customer.setTitle(customerTitle);
+ customer.setEmail("customer_" + timestamp + "_" + i + "@test.com");
+
+ Customer createdCustomer = client.saveCustomer(customer, null, null, null);
+ assertNotNull(createdCustomer);
+ assertNotNull(createdCustomer.getId());
+ assertEquals(customerTitle, createdCustomer.getTitle());
+
+ createdCustomers.add(createdCustomer);
+ }
+
+ // find all, check count (includes savedClientCustomer from AbstractJavaClientTest setup)
+ PageDataCustomer allCustomers = client.getCustomers(100, 0, null, null, null);
+ assertNotNull(allCustomers);
+ assertNotNull(allCustomers.getData());
+ int initialSize = allCustomers.getData().size();
+ assertEquals("Expected 21 customers (20 created + 1 from setup), but got " + initialSize, 21, initialSize);
+
+ // find all with search text, check count
+ PageDataCustomer filteredCustomers = client.getCustomers(100, 0, TEST_PREFIX_2, null, null);
+ assertEquals("Expected exactly 10 customers matching prefix", 10, filteredCustomers.getData().size());
+
+ // find by id
+ Customer searchCustomer = createdCustomers.get(10);
+ Customer fetchedCustomer = client.getCustomerById(searchCustomer.getId().getId().toString());
+ assertEquals(searchCustomer.getTitle(), fetchedCustomer.getTitle());
+
+ // find by title
+ Customer fetchedByTitle = client.getTenantCustomer(searchCustomer.getTitle());
+ assertEquals(searchCustomer.getId().getId(), fetchedByTitle.getId().getId());
+
+ // update customer
+ fetchedCustomer.setCity("New York");
+ fetchedCustomer.setCountry("US");
+ Customer updatedCustomer = client.saveCustomer(fetchedCustomer, null, null, null);
+ assertEquals("New York", updatedCustomer.getCity());
+ assertEquals("US", updatedCustomer.getCountry());
+
+ // assign device to customer and verify
+ Device device = new Device();
+ device.setName("CustomerTestDevice_" + timestamp);
+ device.setType("default");
+ Device createdDevice = client.saveDevice(device, null, null, null, null);
+
+ String customerId = createdCustomers.get(0).getId().getId().toString();
+ client.assignDeviceToCustomer(customerId, createdDevice.getId().getId().toString());
+
+ PageDataDevice customerDevices = client.getCustomerDevices(customerId, 100, 0, null, null, null, null);
+ assertEquals(1, customerDevices.getData().size());
+ assertEquals(createdDevice.getName(), customerDevices.getData().get(0).getName());
+
+ // unassign device from customer
+ client.unassignDeviceFromCustomer(createdDevice.getId().getId().toString());
+ PageDataDevice devicesAfterUnassign = client.getCustomerDevices(customerId, 100, 0, null, null, null, null);
+ assertEquals(0, devicesAfterUnassign.getData().size());
+
+ // delete customer
+ UUID customerToDeleteId = createdCustomers.get(0).getId().getId();
+ client.deleteCustomer(customerToDeleteId.toString());
+
+ // verify deletion
+ PageDataCustomer customersAfterDelete = client.getCustomers(100, 0, null, null, null);
+ assertEquals(initialSize - 1, customersAfterDelete.getData().size());
+
+ assertReturns404(() ->
+ client.getCustomerById(customerToDeleteId.toString())
+ );
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/DashboardJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/DashboardJavaClientTest.java
new file mode 100644
index 0000000000..5d1fa871c8
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/DashboardJavaClientTest.java
@@ -0,0 +1,123 @@
+/**
+ * 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.model.Dashboard;
+import org.thingsboard.client.model.DashboardInfo;
+import org.thingsboard.client.model.PageDataDashboardInfo;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@DaoSqlTest
+public class DashboardJavaClientTest extends AbstractJavaClientTest {
+
+ @Test
+ public void testDashboardLifecycle() throws Exception {
+ long timestamp = System.currentTimeMillis();
+
+ // create 20 dashboards
+ for (int i = 0; i < 20; i++) {
+ Dashboard dashboard = new Dashboard();
+ String dashboardTitle = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i;
+ dashboard.setTitle(dashboardTitle);
+
+ client.saveDashboard(dashboard, null);
+ }
+
+ // find all, check count
+ PageDataDashboardInfo allDashboards = client.getTenantDashboards(100, 0, null, null, null, null);
+ assertNotNull(allDashboards);
+ assertNotNull(allDashboards.getData());
+ int initialSize = allDashboards.getData().size();
+ assertEquals("Expected 20 dashboards, but got " + initialSize, 20, initialSize);
+
+ List createdDashboards = allDashboards.getData();
+
+ // find all with search text, check count
+ PageDataDashboardInfo filteredDashboards = client.getTenantDashboards(100, 0, null, TEST_PREFIX_2, null, null);
+ assertEquals("Expected exactly 10 dashboards matching prefix", 10, filteredDashboards.getData().size());
+
+ // find by id
+ DashboardInfo searchDashboard = createdDashboards.get(10);
+ DashboardInfo fetchedDashboard = client.getDashboardInfoById(searchDashboard.getId().getId().toString());
+ assertEquals(searchDashboard.getTitle(), fetchedDashboard.getTitle());
+
+ // update dashboard
+ Dashboard dashboardToUpdate = new Dashboard();
+ dashboardToUpdate.setId(fetchedDashboard.getId());
+ dashboardToUpdate.setTitle(fetchedDashboard.getTitle() + "_updated");
+ dashboardToUpdate.setVersion(fetchedDashboard.getVersion());
+ client.saveDashboard(dashboardToUpdate, null);
+
+ DashboardInfo updatedDashboard = client.getDashboardInfoById(fetchedDashboard.getId().getId().toString());
+ assertEquals(fetchedDashboard.getTitle() + "_updated", updatedDashboard.getTitle());
+
+ // assign dashboard to customer and verify
+ String customerId = savedClientCustomer.getId().getId().toString();
+ String dashboardId = createdDashboards.get(0).getId().getId().toString();
+ client.assignDashboardToCustomer(customerId, dashboardId);
+
+ PageDataDashboardInfo customerDashboards = client.getCustomerDashboards(customerId, 100, 0, null, null, null, null);
+ assertEquals(1, customerDashboards.getData().size());
+ assertEquals(createdDashboards.get(0).getTitle(), customerDashboards.getData().get(0).getTitle());
+
+ // unassign dashboard from customer
+ client.unassignDashboardFromCustomer(customerId, dashboardId);
+ PageDataDashboardInfo dashboardsAfterUnassign = client.getCustomerDashboards(customerId, 100, 0, null, null, null, null);
+ assertEquals(0, dashboardsAfterUnassign.getData().size());
+
+ // make dashboard public and verify
+ client.assignDashboardToPublicCustomer(dashboardId);
+ DashboardInfo publicDashboard = client.getDashboardInfoById(dashboardId);
+ assertNotNull(publicDashboard.getAssignedCustomers());
+ assertTrue(publicDashboard.getAssignedCustomers().size() > 0);
+
+ // remove public access
+ client.unassignDashboardFromPublicCustomer(dashboardId);
+
+ // delete dashboard
+ UUID dashboardToDeleteId = createdDashboards.get(0).getId().getId();
+ client.deleteDashboard(dashboardToDeleteId.toString());
+
+ // verify deletion
+ PageDataDashboardInfo dashboardsAfterDelete = client.getTenantDashboards(100, 0, null, null, null, null);
+ assertEquals(initialSize - 1, dashboardsAfterDelete.getData().size());
+
+ assertReturns404(() ->
+ client.getDashboardInfoById(dashboardToDeleteId.toString())
+ );
+ }
+
+ @Test
+ public void testGetServerTime() throws Exception {
+ Long serverTime = client.getServerTime();
+ assertNotNull(serverTime);
+ }
+
+ @Test
+ public void testGetMaxDatapointsLimit() throws Exception {
+ Long maxDatapointsLimit = client.getMaxDatapointsLimit();
+ assertNotNull(maxDatapointsLimit);
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/DeviceConnectivityJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/DeviceConnectivityJavaClientTest.java
new file mode 100644
index 0000000000..2ca2d622e7
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/DeviceConnectivityJavaClientTest.java
@@ -0,0 +1,53 @@
+/**
+ * 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.client;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.junit.Test;
+import org.thingsboard.client.model.Device;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+
+@DaoSqlTest
+public class DeviceConnectivityJavaClientTest extends AbstractJavaClientTest {
+
+ @Test
+ public void testGetDevicePublishTelemetryCommands() throws Exception {
+ Device device = new Device();
+ device.setName(TEST_PREFIX + System.currentTimeMillis());
+ device.setType("default");
+
+ Device savedDevice = client.saveDevice(device, null, null, null, null);
+ String token = client.getDeviceCredentialsByDeviceId(savedDevice.getId().getId().toString()).getCredentialsId();
+
+ String deviceId = savedDevice.getId().getId().toString();
+
+ JsonNode commands = client.getDevicePublishTelemetryCommands(deviceId);
+ assertEquals("curl -v -X POST http://localhost:8080/api/v1/" + token + "/telemetry --header Content-Type:application/json --data \"{temperature:25}\"", commands.get("http").get("http").asText());
+ assertEquals("mosquitto_pub -d -q 1 -h localhost -p 1883 -t v1/devices/me/telemetry -u \"" + token + "\" -m \"{temperature:25}\"", commands.get("mqtt").get("mqtt").asText());
+ assertEquals("coap-client -v 6 -m POST -t \"application/json\" -e \"{temperature:25}\" coap://localhost:5683/api/v1/" + token + "/telemetry", commands.get("coap").get("coap").asText());
+ }
+
+ @Test
+ public void testGetDevicePublishTelemetryCommands_nonExistentDevice() {
+ String nonExistentId = UUID.randomUUID().toString();
+ assertReturns404(() -> client.getDevicePublishTelemetryCommands(nonExistentId));
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/DeviceJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/DeviceJavaClientTest.java
new file mode 100644
index 0000000000..c141565c42
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/DeviceJavaClientTest.java
@@ -0,0 +1,114 @@
+/**
+ * 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.model.Device;
+import org.thingsboard.client.model.DeviceCredentials;
+import org.thingsboard.client.model.DeviceCredentialsType;
+import org.thingsboard.client.model.PageDataDevice;
+import org.thingsboard.client.model.SaveDeviceWithCredentialsRequest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@DaoSqlTest
+public class DeviceJavaClientTest extends AbstractJavaClientTest {
+
+ @Test
+ public void testDeviceLifecycle() throws Exception {
+ long timestamp = System.currentTimeMillis();
+ List createdDevices = new ArrayList<>();
+
+ // create 20 devices
+ for (int i = 0; i < 20; i++) {
+ Device device = new Device();
+ String deviceName = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i;
+ device.setName(deviceName);
+ device.setLabel("Test Device " + i);
+ device.setType(((i % 2 == 0) ? "default" : "thermostat"));
+
+ Device createdDevice = client.saveDevice(device, null, null, null, null);
+ assertNotNull(createdDevice);
+ assertNotNull(createdDevice.getId());
+ assertEquals(deviceName, createdDevice.getName());
+
+ createdDevices.add(createdDevice);
+ }
+
+ // find all, check count
+ PageDataDevice allDevices = client.getTenantDevices(100, 0, null, null, null, null);
+
+ assertNotNull(allDevices);
+ assertNotNull(allDevices.getData());
+ int initialSize = allDevices.getData().size();
+ assertEquals("Expected at least 20 devices, but got " + allDevices.getData().size(), 20, initialSize);
+
+ //find all with search text, check count
+ PageDataDevice allDevicesBySearchText = client.getTenantDevices(10, 0, null, TEST_PREFIX_2, null, null);
+ assertEquals("Expected exactly 10 test devices", 10, allDevicesBySearchText.getData().size());
+
+ // find by id
+ Device searchDevice = createdDevices.get(10);
+ Device device = client.getDeviceById(searchDevice.getId().getId().toString());
+ assertEquals(searchDevice.getName(), device.getName());
+
+ // create device with credentials
+ Device deviceWithCreds = new Device();
+ deviceWithCreds.setName("device-with-creds");
+
+ DeviceCredentials creds = new DeviceCredentials();
+ creds.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN);
+ creds.setCredentialsId("TEST_ACCESS_TOKEN");
+
+ SaveDeviceWithCredentialsRequest request = new SaveDeviceWithCredentialsRequest();
+ request.setDevice(deviceWithCreds);
+ request.setCredentials(creds);
+
+ Device savedDeviceWithCreds = client.saveDeviceWithCredentials(request, null, null, null);
+ assertEquals("device-with-creds", savedDeviceWithCreds.getName());
+
+ // find credentials by device id
+ DeviceCredentials fetchedCreds = client.getDeviceCredentialsByDeviceId(savedDeviceWithCreds.getId().getId().toString());
+ assertEquals(creds.getCredentialsId(), fetchedCreds.getCredentialsId());
+
+ // delete device
+ UUID deviceToDeleteId = createdDevices.get(0).getId().getId();
+ client.deleteDevice(deviceToDeleteId.toString());
+
+ // Verify the device is deleted
+ PageDataDevice devicesAfterDelete = client.getTenantDevices(100, 0, null, null, null, null);
+ assertEquals(initialSize, devicesAfterDelete.getData().size());
+
+ assertReturns404(() ->
+ client.getDeviceById(deviceToDeleteId.toString()));
+
+ // assign device to customer
+ client.assignDeviceToCustomer(savedClientCustomer.getId().getId().toString(), savedDeviceWithCreds.getId().getId().toString());
+
+ // check customer devices
+ PageDataDevice pageDataDevice = client.getCustomerDevices(savedClientCustomer.getId().getId().toString(), 100, 0, null, null, null, null);
+ List data = pageDataDevice.getData();
+ assertEquals(1, data.size());
+ assertEquals(savedDeviceWithCreds.getName(), data.get(0).getName());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/DeviceProfileJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/DeviceProfileJavaClientTest.java
new file mode 100644
index 0000000000..6008c85653
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/DeviceProfileJavaClientTest.java
@@ -0,0 +1,157 @@
+/**
+ * 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.model.DefaultDeviceProfileConfiguration;
+import org.thingsboard.client.model.DefaultDeviceProfileTransportConfiguration;
+import org.thingsboard.client.model.DeviceProfile;
+import org.thingsboard.client.model.DeviceProfileData;
+import org.thingsboard.client.model.DeviceProfileInfo;
+import org.thingsboard.client.model.DeviceProfileType;
+import org.thingsboard.client.model.DeviceTransportType;
+import org.thingsboard.client.model.EntityInfo;
+import org.thingsboard.client.model.PageDataDeviceProfile;
+import org.thingsboard.client.model.PageDataDeviceProfileInfo;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@DaoSqlTest
+public class DeviceProfileJavaClientTest extends AbstractJavaClientTest {
+
+ @Test
+ public void testDeviceProfileLifecycle() throws Exception {
+ long timestamp = System.currentTimeMillis();
+ List createdProfiles = new ArrayList<>();
+
+ // Get initial count (there should be a default profile)
+ PageDataDeviceProfile initialProfiles = client.getDeviceProfiles(100, 0, null, null, null);
+ assertNotNull(initialProfiles);
+ int initialSize = initialProfiles.getData().size();
+ assertTrue("Expected at least 1 default device profile", initialSize >= 1);
+
+ // Get default device profile info
+ DeviceProfileInfo defaultProfileInfo = client.getDefaultDeviceProfileInfo();
+ assertNotNull(defaultProfileInfo);
+ assertNotNull(defaultProfileInfo.getName());
+
+ // Create multiple device profiles
+ for (int i = 0; i < 5; i++) {
+ DeviceProfile deviceProfile = new DeviceProfile();
+ deviceProfile.setName("Test Device Profile " + timestamp + "_" + i);
+ deviceProfile.setDescription("Test description " + i);
+ deviceProfile.setType(DeviceProfileType.DEFAULT);
+ deviceProfile.setTransportType(DeviceTransportType.DEFAULT);
+
+ DeviceProfileData deviceProfileData = new DeviceProfileData();
+ DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration();
+ configuration.setType(DeviceProfileType.DEFAULT.getValue());
+ deviceProfileData.setConfiguration(configuration);
+ DefaultDeviceProfileTransportConfiguration transportConf = new DefaultDeviceProfileTransportConfiguration();
+ transportConf.setType(DeviceTransportType.DEFAULT.getValue());
+ deviceProfileData.setTransportConfiguration(transportConf);
+ deviceProfile.setProfileData(deviceProfileData);
+ deviceProfile.setDefault(false);
+ deviceProfile.setDefaultRuleChainId(null);
+
+ DeviceProfile created = client.saveDeviceProfile(deviceProfile);
+ assertNotNull(created);
+ assertNotNull(created.getId());
+ assertEquals(deviceProfile.getName(), created.getName());
+ assertEquals(deviceProfile.getDescription(), created.getDescription());
+ assertEquals(DeviceProfileType.DEFAULT, created.getType());
+ assertEquals(DeviceTransportType.DEFAULT, created.getTransportType());
+ assertFalse(created.getDefault());
+
+ createdProfiles.add(created);
+ }
+
+ // Find all, check count
+ PageDataDeviceProfile allProfiles = client.getDeviceProfiles(100, 0, null, null, null);
+ assertNotNull(allProfiles);
+ assertEquals(initialSize + 5, allProfiles.getData().size());
+
+ // Find all with text search
+ PageDataDeviceProfile filteredProfiles = client.getDeviceProfiles(100, 0, "Test Device Profile " + timestamp, null, null);
+ assertEquals(5, filteredProfiles.getData().size());
+
+ // Get by id
+ DeviceProfile searchProfile = createdProfiles.get(2);
+ DeviceProfile fetchedProfile = client.getDeviceProfileById(searchProfile.getId().getId().toString(), false);
+ assertEquals(searchProfile.getName(), fetchedProfile.getName());
+ assertEquals(searchProfile.getDescription(), fetchedProfile.getDescription());
+
+ // Update device profile
+ fetchedProfile.setDescription("Updated description");
+ DeviceProfile updatedProfile = client.saveDeviceProfile(fetchedProfile);
+ assertEquals("Updated description", updatedProfile.getDescription());
+ assertEquals(fetchedProfile.getName(), updatedProfile.getName());
+
+ // Get device profile info by id
+ DeviceProfileInfo profileInfo = client.getDefaultDeviceProfileInfo();
+ assertNotNull(profileInfo);
+ assertEquals(searchProfile.getType().getValue().toLowerCase(), profileInfo.getName());
+ assertEquals(DeviceTransportType.DEFAULT, profileInfo.getTransportType());
+
+ // Get device profile infos (paginated)
+ PageDataDeviceProfileInfo profileInfos = client.getDeviceProfileInfos(100, 0, null, null, null, null);
+ assertNotNull(profileInfos);
+ assertEquals(initialSize + 5, profileInfos.getData().size());
+
+ // Set a profile as default
+ DeviceProfile profileToSetDefault = createdProfiles.get(1);
+ DeviceProfile newDefault = client.setDefaultDeviceProfile(profileToSetDefault.getId().getId().toString());
+ assertNotNull(newDefault);
+ assertTrue(newDefault.getDefault());
+
+ // Verify default profile info now points to the new default
+ DeviceProfileInfo newDefaultInfo = client.getDefaultDeviceProfileInfo();
+ assertEquals(profileToSetDefault.getName(), newDefaultInfo.getName());
+
+ // Get device profile names
+ List profileNames = client.getDeviceProfileNames(false);
+ assertNotNull(profileNames);
+ assertEquals(createdProfiles.size() + 1, profileNames.size());
+
+ // Delete device profile (cannot delete the default one, so delete a non-default one)
+ UUID profileToDeleteId = createdProfiles.get(0).getId().getId();
+ client.deleteDeviceProfile(profileToDeleteId.toString());
+
+ // Verify the profile is deleted
+ assertReturns404(() ->
+ client.getDeviceProfileById(profileToDeleteId.toString(), false));
+
+ // Verify count after deletion
+ PageDataDeviceProfile profilesAfterDelete = client.getDeviceProfiles(100, 0, null, null, null);
+ assertEquals(initialSize + 4, profilesAfterDelete.getData().size());
+
+ // Restore original default profile
+ DeviceProfile originalDefault = initialProfiles.getData().stream()
+ .filter(DeviceProfile::getDefault)
+ .findFirst()
+ .orElseThrow();
+ client.setDefaultDeviceProfile(originalDefault.getId().getId().toString());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/DomainJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/DomainJavaClientTest.java
new file mode 100644
index 0000000000..fb8bb9b44f
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/DomainJavaClientTest.java
@@ -0,0 +1,105 @@
+/**
+ * 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.client;
+
+import org.junit.After;
+import org.junit.Test;
+import org.thingsboard.client.ApiException;
+import org.thingsboard.client.model.Domain;
+import org.thingsboard.client.model.DomainInfo;
+import org.thingsboard.client.model.PageDataDomainInfo;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@DaoSqlTest
+public class DomainJavaClientTest extends AbstractJavaClientTest {
+
+ List createdDomains = new ArrayList<>();
+
+ @After
+ public void afterDomainTest() {
+ createdDomains.forEach(domain -> {
+ try {
+ client.deleteDomain(domain.getId().getId());
+ } catch (ApiException e) {
+ // ignore
+ }
+ });
+ }
+
+ @Test
+ public void testDomainLifecycle() throws Exception {
+ client.login("sysadmin@thingsboard.org", "sysadmin");
+
+ long timestamp = System.currentTimeMillis();
+
+ // create 5 domains
+ for (int i = 0; i < 5; i++) {
+ Domain domain = new Domain();
+ domain.setName("domain." + i + ".com");
+ domain.setOauth2Enabled(false);
+ domain.setPropagateToEdge(false);
+
+ Domain created = client.saveDomain(domain, null);
+ assertNotNull(created);
+ assertNotNull(created.getId());
+ assertEquals(domain.getName(), created.getName());
+ assertEquals(false, created.getOauth2Enabled());
+
+ createdDomains.add(created);
+ }
+
+ // list tenant domains with text search
+ PageDataDomainInfo filteredDomains = client.getTenantDomainInfos(100, 0,
+ "domain.", null, null);
+ assertNotNull(filteredDomains);
+ assertEquals(5, filteredDomains.getData().size());
+
+ // get domain info by id
+ Domain searchDomain = createdDomains.get(2);
+ DomainInfo fetchedInfo = client.getDomainInfoById(searchDomain.getId().getId());
+ assertEquals(searchDomain.getName(), fetchedInfo.getName());
+ assertEquals(searchDomain.getOauth2Enabled(), fetchedInfo.getOauth2Enabled());
+ assertNotNull(fetchedInfo.getOauth2ClientInfos());
+
+ // update domain
+ Domain domainToUpdate = createdDomains.get(3);
+ domainToUpdate.setPropagateToEdge(true);
+ Domain updatedDomain = client.saveDomain(domainToUpdate, null);
+ assertEquals(true, updatedDomain.getPropagateToEdge());
+
+ // delete domain
+ UUID domainToDeleteId = createdDomains.get(0).getId().getId();
+ createdDomains.remove(0);
+ client.deleteDomain(domainToDeleteId);
+
+ // verify deletion
+ assertReturns404(() ->
+ client.getDomainInfoById(domainToDeleteId)
+ );
+
+ PageDataDomainInfo domainsAfterDelete = client.getTenantDomainInfos(100, 0,
+ "domain.", null, null);
+ assertEquals(4, domainsAfterDelete.getData().size());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/EdgeJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/EdgeJavaClientTest.java
new file mode 100644
index 0000000000..1400ead0b1
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/EdgeJavaClientTest.java
@@ -0,0 +1,141 @@
+/**
+ * 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.model.Edge;
+import org.thingsboard.client.model.EdgeInfo;
+import org.thingsboard.client.model.PageDataEdge;
+import org.thingsboard.client.model.PageDataEdgeInfo;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@DaoSqlTest
+public class EdgeJavaClientTest extends AbstractJavaClientTest {
+
+ @Test
+ public void testEdgeLifecycle() throws Exception {
+ long timestamp = System.currentTimeMillis();
+ List createdEdges = new ArrayList<>();
+
+ // create 5 edges
+ for (int i = 0; i < 5; i++) {
+ Edge edge = new Edge();
+ edge.setName(TEST_PREFIX + "Edge_" + timestamp + "_" + i);
+ edge.setType("gateway");
+ edge.setLabel("Test Edge " + i);
+ edge.setRoutingKey("routing_key_" + timestamp + "_" + i);
+ edge.setSecret("secret_key_" + timestamp + "_" + i);
+
+ Edge created = client.saveEdge(edge);
+ assertNotNull(created);
+ assertNotNull(created.getId());
+ assertEquals(edge.getName(), created.getName());
+ assertEquals("gateway", created.getType());
+ assertNotNull(created.getRoutingKey());
+ assertNotNull(created.getSecret());
+
+ createdEdges.add(created);
+ }
+
+ // list tenant edges with text search
+ PageDataEdge filteredEdges = client.getTenantEdges(100, 0, null,
+ TEST_PREFIX + "Edge_" + timestamp, null, null);
+ assertNotNull(filteredEdges);
+ assertEquals(5, filteredEdges.getData().size());
+
+ // list tenant edges with type filter
+ PageDataEdge typedEdges = client.getTenantEdges(100, 0, "gateway",
+ TEST_PREFIX + "Edge_" + timestamp, null, null);
+ assertEquals(5, typedEdges.getData().size());
+
+ // get tenant edge infos
+ PageDataEdgeInfo edgeInfos = client.getTenantEdgeInfos(100, 0, null,
+ TEST_PREFIX + "Edge_" + timestamp, null, null);
+ assertEquals(5, edgeInfos.getData().size());
+
+ // get edge by id
+ Edge searchEdge = createdEdges.get(2);
+ Edge fetchedEdge = client.getEdgeById(searchEdge.getId().getId().toString());
+ assertEquals(searchEdge.getName(), fetchedEdge.getName());
+ assertEquals(searchEdge.getType(), fetchedEdge.getType());
+ assertEquals(searchEdge.getRoutingKey(), fetchedEdge.getRoutingKey());
+
+ // get edge by name
+ Edge fetchedByName = client.getTenantEdgeByName(searchEdge.getName());
+ assertEquals(searchEdge.getId().getId(), fetchedByName.getId().getId());
+
+ // get edges by list of ids
+ List idsToFetch = List.of(
+ createdEdges.get(0).getId().getId().toString(),
+ createdEdges.get(1).getId().getId().toString()
+ );
+ List edgeList = client.getEdgeList(idsToFetch);
+ assertEquals(2, edgeList.size());
+
+ // update edge
+ Edge edgeToUpdate = createdEdges.get(3);
+ edgeToUpdate.setLabel("Updated Label");
+ Edge updatedEdge = client.saveEdge(edgeToUpdate);
+ assertEquals("Updated Label", updatedEdge.getLabel());
+
+ // assign edge to customer
+ String customerId = savedClientCustomer.getId().getId().toString();
+ String edgeId = createdEdges.get(1).getId().getId().toString();
+ Edge assignedEdge = client.assignEdgeToCustomer(customerId, edgeId);
+ assertNotNull(assignedEdge.getCustomerId());
+
+ // get customer edges
+ PageDataEdge customerEdges = client.getCustomerEdges(customerId, 100, 0,
+ null, TEST_PREFIX + "Edge_" + timestamp, null, null);
+ assertEquals(1, customerEdges.getData().size());
+
+ // get customer edge infos
+ PageDataEdgeInfo customerEdgeInfos = client.getCustomerEdgeInfos(customerId, 100, 0,
+ null, TEST_PREFIX + "Edge_" + timestamp, null, null);
+ assertEquals(1, customerEdgeInfos.getData().size());
+ EdgeInfo edgeInfo = customerEdgeInfos.getData().get(0);
+ assertNotNull(edgeInfo.getCustomerTitle());
+
+ // unassign edge from customer
+ Edge unassignedEdge = client.unassignEdgeFromCustomer(edgeId);
+ assertNotNull(unassignedEdge);
+
+ PageDataEdge customerEdgesAfter = client.getCustomerEdges(customerId, 100, 0,
+ null, TEST_PREFIX + "Edge_" + timestamp, null, null);
+ assertEquals(0, customerEdgesAfter.getData().size());
+
+ // delete edge
+ UUID edgeToDeleteId = createdEdges.get(0).getId().getId();
+ client.deleteEdge(edgeToDeleteId.toString());
+
+ // verify deletion
+ assertReturns404(() ->
+ client.getEdgeById(edgeToDeleteId.toString())
+ );
+
+ PageDataEdge edgesAfterDelete = client.getTenantEdges(100, 0, null,
+ TEST_PREFIX + "Edge_" + timestamp, null, null);
+ assertEquals(4, edgesAfterDelete.getData().size());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/EntityQueryJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/EntityQueryJavaClientTest.java
new file mode 100644
index 0000000000..5c075f0777
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/EntityQueryJavaClientTest.java
@@ -0,0 +1,289 @@
+/**
+ * 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.model.AliasEntityId;
+import org.thingsboard.client.model.Asset;
+import org.thingsboard.client.model.AssetTypeFilter;
+import org.thingsboard.client.model.Device;
+import org.thingsboard.client.model.DeviceTypeFilter;
+import org.thingsboard.client.model.Direction;
+import org.thingsboard.client.model.EntityData;
+import org.thingsboard.client.model.EntityDataPageLink;
+import org.thingsboard.client.model.EntityDataQuery;
+import org.thingsboard.client.model.EntityDataSortOrder;
+import org.thingsboard.client.model.EntityKey;
+import org.thingsboard.client.model.EntityKeyType;
+import org.thingsboard.client.model.EntityKeyValueType;
+import org.thingsboard.client.model.EntityListFilter;
+import org.thingsboard.client.model.EntityNameFilter;
+import org.thingsboard.client.model.EntityType;
+import org.thingsboard.client.model.FilterPredicateValueString;
+import org.thingsboard.client.model.KeyFilter;
+import org.thingsboard.client.model.PageDataEntityData;
+import org.thingsboard.client.model.SingleEntityFilter;
+import org.thingsboard.client.model.StringFilterPredicate;
+import org.thingsboard.client.model.StringOperation;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@DaoSqlTest
+public class EntityQueryJavaClientTest extends AbstractJavaClientTest {
+
+ private static final String QUERY_TEST_PREFIX = "QueryTest_";
+
+ private EntityDataPageLink pageLink(int pageSize) {
+ return new EntityDataPageLink()
+ .pageSize(pageSize)
+ .page(0)
+ .sortOrder(new EntityDataSortOrder()
+ .key(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name"))
+ .direction(Direction.ASC));
+ }
+
+ @Test
+ public void testFindByDeviceTypeFilter() throws Exception {
+ long ts = System.currentTimeMillis();
+ String type1 = "temperatureSensor";
+ String type2 = "humiditySensor";
+
+ for (int i = 0; i < 3; i++) {
+ Device d = new Device();
+ d.setName(QUERY_TEST_PREFIX + "temp_" + ts + "_" + i);
+ d.setType(type1);
+ client.saveDevice(d, null, null, null, null);
+ }
+ for (int i = 0; i < 2; i++) {
+ Device d = new Device();
+ d.setName(QUERY_TEST_PREFIX + "hum_" + ts + "_" + i);
+ d.setType(type2);
+ client.saveDevice(d, null, null, null, null);
+ }
+
+ // filter by single device type
+ EntityDataQuery singleTypeQuery = new EntityDataQuery()
+ .entityFilter(new DeviceTypeFilter()
+ .deviceTypes(List.of(type1)))
+ .pageLink(pageLink(10))
+ .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name"));
+
+ PageDataEntityData result = client.findEntityDataByQuery(singleTypeQuery);
+ assertNotNull(result);
+ assertEquals(3, result.getTotalElements().intValue());
+ for (EntityData entity : result.getData()) {
+ assertNotNull(entity.getEntityId());
+ }
+
+ // filter by multiple device types
+ EntityDataQuery multiTypeQuery = new EntityDataQuery()
+ .entityFilter(new DeviceTypeFilter()
+ .deviceTypes(List.of(type1, type2)))
+ .pageLink(pageLink(10))
+ .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name"));
+
+ PageDataEntityData multiResult = client.findEntityDataByQuery(multiTypeQuery);
+ assertNotNull(multiResult);
+ assertEquals(5, multiResult.getTotalElements().intValue());
+
+ // filter by device type + name filter
+ EntityDataQuery nameFilterQuery = new EntityDataQuery()
+ .entityFilter(new DeviceTypeFilter()
+ .deviceTypes(List.of(type1, type2))
+ .deviceNameFilter(QUERY_TEST_PREFIX + "temp_" + ts))
+ .pageLink(pageLink(10))
+ .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name"));
+
+ PageDataEntityData nameResult = client.findEntityDataByQuery(nameFilterQuery);
+ assertNotNull(nameResult);
+ assertEquals(3, nameResult.getTotalElements().intValue());
+ }
+
+ @Test
+ public void testFindByEntityNameFilter() throws Exception {
+ long ts = System.currentTimeMillis();
+ String prefix = QUERY_TEST_PREFIX + "named_" + ts;
+
+ for (int i = 0; i < 4; i++) {
+ Device d = new Device();
+ d.setName(prefix + "_" + i);
+ d.setType("default");
+ client.saveDevice(d, null, null, null, null);
+ }
+
+ EntityDataQuery query = new EntityDataQuery()
+ .entityFilter(new EntityNameFilter()
+ .entityType(EntityType.DEVICE)
+ .entityNameFilter(prefix))
+ .pageLink(pageLink(10))
+ .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name"));
+
+ PageDataEntityData result = client.findEntityDataByQuery(query);
+ assertNotNull(result);
+ assertEquals(4, result.getTotalElements().intValue());
+ assertFalse(result.getHasNext());
+ }
+
+ @Test
+ public void testFindByEntityListFilter() throws Exception {
+ long ts = System.currentTimeMillis();
+
+ Device d1 = client.saveDevice(new Device().name(QUERY_TEST_PREFIX + "list_" + ts + "_1").type("default"), null, null, null, null);
+ Device d2 = client.saveDevice(new Device().name(QUERY_TEST_PREFIX + "list_" + ts + "_2").type("default"), null, null, null, null);
+ client.saveDevice(new Device().name(QUERY_TEST_PREFIX + "list_" + ts + "_3").type("default"), null, null, null, null);
+
+ EntityDataQuery query = new EntityDataQuery()
+ .entityFilter(new EntityListFilter()
+ .entityType(EntityType.DEVICE)
+ .entityList(List.of(
+ d1.getId().getId().toString(),
+ d2.getId().getId().toString())))
+ .pageLink(pageLink(10))
+ .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name"));
+
+ PageDataEntityData result = client.findEntityDataByQuery(query);
+ assertNotNull(result);
+ assertEquals(2, result.getTotalElements().intValue());
+
+ List returnedIds = result.getData().stream()
+ .map(e -> e.getEntityId().getId().toString())
+ .collect(Collectors.toList());
+ assertTrue(returnedIds.contains(d1.getId().getId().toString()));
+ assertTrue(returnedIds.contains(d2.getId().getId().toString()));
+ }
+
+ @Test
+ public void testFindBySingleEntityFilter() throws Exception {
+ long ts = System.currentTimeMillis();
+ Device device = client.saveDevice(new Device().name(QUERY_TEST_PREFIX + "single_" + ts).type("default"), null, null, null, null);
+
+ EntityDataQuery query = new EntityDataQuery()
+ .entityFilter(new SingleEntityFilter()
+ .singleEntity(new AliasEntityId()
+ .id(device.getId().getId())
+ .entityType(EntityType.DEVICE)))
+ .pageLink(pageLink(10))
+ .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name"));
+
+ PageDataEntityData result = client.findEntityDataByQuery(query);
+ assertNotNull(result);
+ assertEquals(1, result.getTotalElements().intValue());
+ assertEquals(device.getId().getId().toString(),
+ result.getData().get(0).getEntityId().getId().toString());
+ }
+
+ @Test
+ public void testFindByAssetTypeFilter() throws Exception {
+ long ts = System.currentTimeMillis();
+ String assetType = "building";
+
+ for (int i = 0; i < 3; i++) {
+ Asset a = new Asset();
+ a.setName(QUERY_TEST_PREFIX + "asset_" + ts + "_" + i);
+ a.setType(assetType);
+ client.saveAsset(a, null, null, null);
+ }
+
+ EntityDataQuery query = new EntityDataQuery()
+ .entityFilter(new AssetTypeFilter()
+ .assetTypes(List.of(assetType)))
+ .pageLink(pageLink(10))
+ .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name"));
+
+ PageDataEntityData result = client.findEntityDataByQuery(query);
+ assertNotNull(result);
+ assertEquals(3, result.getTotalElements().intValue());
+ }
+
+ @Test
+ public void testFindWithKeyFilter() throws Exception {
+ long ts = System.currentTimeMillis();
+ String matchName = QUERY_TEST_PREFIX + "kf_match_" + ts;
+ String noMatchName = QUERY_TEST_PREFIX + "kf_other_" + ts;
+
+ client.saveDevice(new Device().name(matchName).type("default"), null, null, null, null);
+ client.saveDevice(new Device().name(noMatchName).type("default"), null, null, null, null);
+
+ KeyFilter nameKeyFilter = new KeyFilter()
+ .key(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name"))
+ .valueType(EntityKeyValueType.STRING)
+ .predicate(new StringFilterPredicate()
+ .operation(StringOperation.CONTAINS)
+ .value(new FilterPredicateValueString().defaultValue("kf_match"))
+ .ignoreCase(true));
+
+ EntityDataQuery query = new EntityDataQuery()
+ .entityFilter(new EntityNameFilter()
+ .entityType(EntityType.DEVICE)
+ .entityNameFilter(QUERY_TEST_PREFIX + "kf_"))
+ .addKeyFiltersItem(nameKeyFilter)
+ .pageLink(pageLink(10))
+ .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name"));
+
+ PageDataEntityData result = client.findEntityDataByQuery(query);
+ assertNotNull(result);
+ assertEquals(1, result.getTotalElements().intValue());
+ }
+
+ @Test
+ public void testFindWithPagination() throws Exception {
+ long ts = System.currentTimeMillis();
+
+ for (int i = 0; i < 5; i++) {
+ Device d = new Device();
+ d.setName(QUERY_TEST_PREFIX + "page_" + ts + "_" + i);
+ d.setType("default");
+ client.saveDevice(d, null, null, null, null);
+ }
+
+ EntityDataPageLink smallPage = new EntityDataPageLink()
+ .pageSize(2)
+ .page(0)
+ .sortOrder(new EntityDataSortOrder()
+ .key(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name"))
+ .direction(Direction.ASC));
+
+ EntityDataQuery query = new EntityDataQuery()
+ .entityFilter(new EntityNameFilter()
+ .entityType(EntityType.DEVICE)
+ .entityNameFilter(QUERY_TEST_PREFIX + "page_" + ts))
+ .pageLink(smallPage)
+ .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name"));
+
+ // first page
+ PageDataEntityData page1 = client.findEntityDataByQuery(query);
+ assertNotNull(page1);
+ assertEquals(5, page1.getTotalElements().intValue());
+ assertEquals(3, page1.getTotalPages().intValue());
+ assertEquals(2, page1.getData().size());
+ assertTrue(page1.getHasNext());
+
+ // last page
+ smallPage.setPage(2);
+ PageDataEntityData lastPage = client.findEntityDataByQuery(query);
+ assertNotNull(lastPage);
+ assertEquals(1, lastPage.getData().size());
+ assertFalse(lastPage.getHasNext());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/EntityRelationJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/EntityRelationJavaClientTest.java
new file mode 100644
index 0000000000..5113dc380b
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/EntityRelationJavaClientTest.java
@@ -0,0 +1,182 @@
+/**
+ * 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.model.Asset;
+import org.thingsboard.client.model.Device;
+import org.thingsboard.client.model.EntityRelation;
+import org.thingsboard.client.model.EntityRelationInfo;
+import org.thingsboard.client.model.EntityRelationsQuery;
+import org.thingsboard.client.model.EntitySearchDirection;
+import org.thingsboard.client.model.EntityType;
+import org.thingsboard.client.model.RelationEntityTypeFilter;
+import org.thingsboard.client.model.RelationTypeGroup;
+import org.thingsboard.client.model.RelationsSearchParameters;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@DaoSqlTest
+public class EntityRelationJavaClientTest extends AbstractJavaClientTest {
+
+ @Test
+ public void testEntityRelationLifecycle() throws Exception {
+ long timestamp = System.currentTimeMillis();
+
+ // create assets and devices to relate
+ Asset building = new Asset();
+ building.setName(TEST_PREFIX + "Building_" + timestamp);
+ building.setType("building");
+ building = client.saveAsset(building, null, null, null);
+
+ Asset floor = new Asset();
+ floor.setName(TEST_PREFIX + "Floor_" + timestamp);
+ floor.setType("floor");
+ floor = client.saveAsset(floor, null, null, null);
+
+ Device device1 = new Device();
+ device1.setName(TEST_PREFIX + "Sensor_" + timestamp + "_1");
+ device1.setType("sensor");
+ device1 = client.saveDevice(device1, null, null, null, null);
+
+ Device device2 = new Device();
+ device2.setName(TEST_PREFIX + "Sensor_" + timestamp + "_2");
+ device2.setType("sensor");
+ device2 = client.saveDevice(device2, null, null, null, null);
+
+ Device device3 = new Device();
+ device3.setName(TEST_PREFIX + "Sensor_" + timestamp + "_3");
+ device3.setType("sensor");
+ device3 = client.saveDevice(device3, null, null, null, null);
+
+ // create relations: building -> Contains -> floor, floor -> Contains -> device1/device2/device3
+ EntityRelation buildingToFloor = new EntityRelation();
+ buildingToFloor.setFrom(building.getId());
+ buildingToFloor.setTo(floor.getId());
+ buildingToFloor.setType("Contains");
+ buildingToFloor.setTypeGroup(RelationTypeGroup.COMMON);
+ EntityRelation savedRelation = client.saveRelation(buildingToFloor);
+ assertNotNull(savedRelation);
+ assertEquals("Contains", savedRelation.getType());
+
+ client.saveRelation(new EntityRelation()
+ .from(floor.getId())
+ .to(device1.getId())
+ .type("Contains")
+ .typeGroup(RelationTypeGroup.COMMON));
+ client.saveRelation(new EntityRelation()
+ .from(floor.getId())
+ .to(device2.getId())
+ .type("Contains").typeGroup(RelationTypeGroup.COMMON));
+ client.saveRelation(new EntityRelation()
+ .from(floor.getId())
+ .to(device3.getId())
+ .type("Manages")
+ .typeGroup(RelationTypeGroup.COMMON));
+
+ // get specific relation
+ EntityRelation fetched = client.getRelation(
+ building.getId().getId().toString(), "ASSET",
+ "Contains",
+ floor.getId().getId().toString(), "ASSET",
+ RelationTypeGroup.COMMON.getValue());
+ assertNotNull(fetched);
+ assertEquals("Contains", fetched.getType());
+
+ // find all relations from floor
+ List fromFloor = client.findEntityRelationsByFrom("ASSET",
+ floor.getId().getId().toString(), RelationTypeGroup.COMMON.getValue());
+ assertEquals(3, fromFloor.size());
+
+ // find relations from floor with type filter "Contains"
+ List containsFromFloor = client.findEntityRelationsByFromAndRelationType("ASSET",
+ floor.getId().getId().toString(), "Contains", RelationTypeGroup.COMMON.getValue());
+ assertEquals(2, containsFromFloor.size());
+
+ // find relations to device1
+ List toDevice1 = client.findEntityRelationsByTo("DEVICE",
+ device1.getId().getId().toString(), RelationTypeGroup.COMMON.getValue());
+ assertEquals(1, toDevice1.size());
+ assertEquals("Contains", toDevice1.get(0).getType());
+
+ // find relations to device3 with type filter "Manages"
+ List managesToDevice3 = client.findEntityRelationsByToAndRelationType("DEVICE",
+ device3.getId().getId().toString(), "Manages", RelationTypeGroup.COMMON.getValue());
+ assertEquals(1, managesToDevice3.size());
+
+ // find info by from (includes entity names)
+ List infoFromFloor = client.findEntityRelationInfosByFrom("ASSET",
+ floor.getId().getId().toString(), RelationTypeGroup.COMMON.getValue());
+ assertEquals(3, infoFromFloor.size());
+ Device finalDevice = device1;
+ assertTrue(infoFromFloor.stream().anyMatch(info ->
+ finalDevice.getName().equals(info.getToName())));
+
+ // find info by to
+ List infoToDevice2 = client.findEntityRelationInfosByTo("DEVICE",
+ device2.getId().getId().toString(), RelationTypeGroup.COMMON.getValue());
+ assertEquals(1, infoToDevice2.size());
+ assertEquals(floor.getName(), infoToDevice2.get(0).getFromName());
+
+ // find by query - search from building, direction FROM, max 2 levels
+ RelationsSearchParameters params = new RelationsSearchParameters();
+ params.setRootId(building.getId().getId());
+ params.setRootType(EntityType.ASSET);
+ params.setDirection(EntitySearchDirection.FROM);
+ params.setRelationTypeGroup(RelationTypeGroup.COMMON);
+ params.setMaxLevel(2);
+
+ RelationEntityTypeFilter filter = new RelationEntityTypeFilter();
+ filter.setRelationType("Contains");
+ filter.setEntityTypes(List.of(EntityType.ASSET, EntityType.DEVICE));
+
+ EntityRelationsQuery query = new EntityRelationsQuery();
+ query.setParameters(params);
+ query.setFilters(List.of(filter));
+
+ List queryResult = client.findEntityRelationsByQuery(query);
+ assertTrue(queryResult.size() >= 3);
+
+ // find info by query
+ List infoQueryResult = client.findEntityRelationInfosByQuery(query);
+ assertTrue(infoQueryResult.size() >= 3);
+
+ // delete single relation
+ client.deleteRelation(
+ floor.getId().getId().toString(), "ASSET",
+ "Manages",
+ device3.getId().getId().toString(), "DEVICE",
+ RelationTypeGroup.COMMON.getValue());
+
+ // verify deletion
+ List afterDelete = client.findEntityRelationsByFrom("ASSET",
+ floor.getId().getId().toString(), RelationTypeGroup.COMMON.getValue());
+ assertEquals(2, afterDelete.size());
+
+ // delete all relations for building
+ client.deleteRelations(building.getId().getId().toString(), "ASSET");
+
+ List afterDeleteAll = client.findEntityRelationsByFrom("ASSET",
+ building.getId().getId().toString(), RelationTypeGroup.COMMON.getValue());
+ assertEquals(0, afterDeleteAll.size());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/EntityViewJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/EntityViewJavaClientTest.java
new file mode 100644
index 0000000000..453420f784
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/EntityViewJavaClientTest.java
@@ -0,0 +1,267 @@
+/**
+ * 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.model.AttributesEntityView;
+import org.thingsboard.client.model.Device;
+import org.thingsboard.client.model.EntitySubtype;
+import org.thingsboard.client.model.EntityView;
+import org.thingsboard.client.model.EntityViewInfo;
+import org.thingsboard.client.model.PageDataEntityView;
+import org.thingsboard.client.model.PageDataEntityViewInfo;
+import org.thingsboard.client.model.TelemetryEntityView;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@DaoSqlTest
+public class EntityViewJavaClientTest extends AbstractJavaClientTest {
+
+ private static final String EV_PREFIX = "EvTest_";
+
+ @Test
+ public void testSaveAndGetEntityView() throws Exception {
+ long ts = System.currentTimeMillis();
+ Device device = createTestDevice(String.valueOf(ts));
+
+ EntityView ev = new EntityView();
+ ev.setName(EV_PREFIX + "save_" + ts);
+ ev.setType("testType");
+ ev.setEntityId(device.getId());
+ ev.setKeys(new TelemetryEntityView()
+ .timeseries(List.of("temperature", "humidity"))
+ .attributes(new AttributesEntityView()
+ .cs(List.of("firmware"))
+ .ss(List.of("active"))
+ .sh(List.of())));
+ ev.setStartTimeMs(1000L);
+ ev.setEndTimeMs(2000L);
+
+ EntityView saved = client.saveEntityView(ev, null, null, null);
+ assertNotNull(saved);
+ assertNotNull(saved.getId());
+ assertEquals(ev.getName(), saved.getName());
+ assertEquals("testType", saved.getType());
+ assertEquals(device.getId().getId(), saved.getEntityId().getId());
+ assertEquals(List.of("temperature", "humidity"), saved.getKeys().getTimeseries());
+ assertEquals(1000L, saved.getStartTimeMs().longValue());
+ assertEquals(2000L, saved.getEndTimeMs().longValue());
+
+ // get by id
+ String evId = saved.getId().getId().toString();
+ EntityView fetched = client.getEntityViewById(evId);
+ assertNotNull(fetched);
+ assertEquals(saved.getName(), fetched.getName());
+ assertEquals(saved.getType(), fetched.getType());
+ assertEquals(saved.getEntityId().getId(), fetched.getEntityId().getId());
+ }
+
+ @Test
+ public void testGetEntityViewInfoById() throws Exception {
+ long ts = System.currentTimeMillis();
+ Device device = createTestDevice(String.valueOf(ts));
+ EntityView saved = createEntityView(EV_PREFIX + "info_" + ts, "infoType", device);
+
+ EntityViewInfo info = client.getEntityViewInfoById(saved.getId().getId().toString());
+ assertNotNull(info);
+ assertEquals(saved.getName(), info.getName());
+ assertEquals("infoType", info.getType());
+ assertNotNull(info.getEntityId());
+ }
+
+ @Test
+ public void testUpdateEntityView() throws Exception {
+ long ts = System.currentTimeMillis();
+ Device device = createTestDevice(String.valueOf(ts));
+ EntityView saved = createEntityView(EV_PREFIX + "update_" + ts, "default", device);
+
+ saved.setName(EV_PREFIX + "updated_" + ts);
+ saved.setKeys(new TelemetryEntityView()
+ .timeseries(List.of("temperature", "pressure"))
+ .attributes(new AttributesEntityView()
+ .cs(List.of())
+ .ss(List.of())
+ .sh(List.of())));
+
+ EntityView updated = client.saveEntityView(saved, null, null, null);
+ assertEquals(EV_PREFIX + "updated_" + ts, updated.getName());
+ assertEquals(List.of("temperature", "pressure"), updated.getKeys().getTimeseries());
+ assertEquals(saved.getId().getId(), updated.getId().getId());
+ }
+
+ @Test
+ public void testDeleteEntityView() throws Exception {
+ long ts = System.currentTimeMillis();
+ Device device = createTestDevice(String.valueOf(ts));
+ EntityView saved = createEntityView(EV_PREFIX + "delete_" + ts, "default", device);
+
+ String evId = saved.getId().getId().toString();
+ client.getEntityViewById(evId);
+
+ client.deleteEntityView(evId);
+
+ assertReturns404(() -> client.getEntityViewById(evId));
+ }
+
+ @Test
+ public void testGetTenantEntityViews() throws Exception {
+ long ts = System.currentTimeMillis();
+ Device device = createTestDevice(String.valueOf(ts));
+
+ for (int i = 0; i < 3; i++) {
+ createEntityView(EV_PREFIX + "tenant_" + ts + "_" + i, "tenantViewType", device);
+ }
+
+ PageDataEntityView page = client.getTenantEntityViews(100, 0, null, EV_PREFIX + "tenant_" + ts, null, null);
+ assertNotNull(page);
+ assertEquals(3, page.getTotalElements().intValue());
+ for (EntityView ev : page.getData()) {
+ assertTrue(ev.getName().startsWith(EV_PREFIX + "tenant_" + ts));
+ }
+ }
+
+ @Test
+ public void testGetTenantEntityViewInfos() throws Exception {
+ long ts = System.currentTimeMillis();
+ Device device = createTestDevice(String.valueOf(ts));
+ createEntityView(EV_PREFIX + "tinfo_" + ts, "default", device);
+
+ PageDataEntityViewInfo page = client.getTenantEntityViewInfos(100, 0, null, EV_PREFIX + "tinfo_" + ts, null, null);
+ assertNotNull(page);
+ assertEquals(1, page.getTotalElements().intValue());
+ assertEquals(EV_PREFIX + "tinfo_" + ts, page.getData().get(0).getName());
+ }
+
+ @Test
+ public void testAssignAndUnassignEntityViewToCustomer() throws Exception {
+ long ts = System.currentTimeMillis();
+ Device device = createTestDevice(String.valueOf(ts));
+ EntityView saved = createEntityView(EV_PREFIX + "assign_" + ts, "default", device);
+
+ String evId = saved.getId().getId().toString();
+ String customerId = savedClientCustomer.getId().getId().toString();
+
+ // assign to customer
+ EntityView assigned = client.assignEntityViewToCustomer(customerId, evId);
+ assertNotNull(assigned);
+ assertEquals(savedClientCustomer.getId().getId(), assigned.getCustomerId().getId());
+
+ // verify in customer entity views
+ PageDataEntityView customerViews = client.getCustomerEntityViews(
+ customerId, 100, 0, null, EV_PREFIX + "assign_" + ts, null, null);
+ assertEquals(1, customerViews.getTotalElements().intValue());
+ assertEquals(saved.getName(), customerViews.getData().get(0).getName());
+
+ // unassign from customer
+ EntityView unassigned = client.unassignEntityViewFromCustomer(evId);
+ assertNotNull(unassigned);
+
+ PageDataEntityView afterUnassign = client.getCustomerEntityViews(
+ customerId, 100, 0, null, EV_PREFIX + "assign_" + ts, null, null);
+ assertEquals(0, afterUnassign.getTotalElements().intValue());
+ }
+
+ @Test
+ public void testGetCustomerEntityViewInfos() throws Exception {
+ long ts = System.currentTimeMillis();
+ Device device = createTestDevice(String.valueOf(ts));
+ EntityView saved = createEntityView(EV_PREFIX + "cinfo_" + ts, "default", device);
+
+ String evId = saved.getId().getId().toString();
+ String customerId = savedClientCustomer.getId().getId().toString();
+
+ client.assignEntityViewToCustomer(customerId, evId);
+
+ PageDataEntityViewInfo infos = client.getCustomerEntityViewInfos(
+ customerId, 100, 0, null, EV_PREFIX + "cinfo_" + ts, null, null);
+ assertNotNull(infos);
+ assertEquals(1, infos.getTotalElements().intValue());
+ assertEquals(saved.getName(), infos.getData().get(0).getName());
+ }
+
+ @Test
+ public void testGetEntityViewTypes() throws Exception {
+ long ts = System.currentTimeMillis();
+ Device device = createTestDevice(String.valueOf(ts));
+ createEntityView(EV_PREFIX + "types_" + ts, "uniqueEvType_" + ts, device);
+
+ List types = client.getEntityViewTypes();
+ assertNotNull(types);
+ assertFalse(types.isEmpty());
+
+ List typeNames = types.stream()
+ .map(EntitySubtype::getType)
+ .collect(Collectors.toList());
+ assertTrue(typeNames.contains("uniqueEvType_" + ts));
+ }
+
+ @Test
+ public void testGetEntityViewById_notFound() {
+ String nonExistentId = UUID.randomUUID().toString();
+ assertReturns404(() -> client.getEntityViewById(nonExistentId));
+ }
+
+ @Test
+ public void testGetTenantEntityViewsPagination() throws Exception {
+ long ts = System.currentTimeMillis();
+ Device device = createTestDevice(String.valueOf(ts));
+
+ for (int i = 0; i < 5; i++) {
+ createEntityView(EV_PREFIX + "paged_" + ts + "_" + i, "default", device);
+ }
+
+ PageDataEntityView page1 = client.getTenantEntityViews(2, 0, null, EV_PREFIX + "paged_" + ts, null, null);
+ assertNotNull(page1);
+ assertEquals(5, page1.getTotalElements().intValue());
+ assertEquals(3, page1.getTotalPages().intValue());
+ assertEquals(2, page1.getData().size());
+ assertTrue(page1.getHasNext());
+
+ PageDataEntityView lastPage = client.getTenantEntityViews(2, 2, null, EV_PREFIX + "paged_" + ts, null, null);
+ assertEquals(1, lastPage.getData().size());
+ assertFalse(lastPage.getHasNext());
+ }
+
+ private Device createTestDevice(String suffix) throws Exception {
+ Device device = new Device();
+ device.setName(EV_PREFIX + "device_" + suffix);
+ device.setType("default");
+ return client.saveDevice(device, null, null, null, null);
+ }
+
+ private EntityView createEntityView(String name, String type, Device device) throws Exception {
+ EntityView ev = new EntityView();
+ ev.setName(name);
+ ev.setType(type);
+ ev.setEntityId(device.getId());
+ ev.setKeys(new TelemetryEntityView()
+ .timeseries(List.of("temperature"))
+ .attributes(new AttributesEntityView()
+ .cs(List.of())
+ .ss(List.of())
+ .sh(List.of())));
+ return client.saveEntityView(ev, null, null, null);
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/MobileAppJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/MobileAppJavaClientTest.java
new file mode 100644
index 0000000000..f3f8c2900d
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/MobileAppJavaClientTest.java
@@ -0,0 +1,156 @@
+/**
+ * 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.model.MobileApp;
+import org.thingsboard.client.model.MobileAppBundle;
+import org.thingsboard.client.model.MobileAppBundleInfo;
+import org.thingsboard.client.model.MobileAppStatus;
+import org.thingsboard.client.model.PageDataMobileApp;
+import org.thingsboard.client.model.PageDataMobileAppBundleInfo;
+import org.thingsboard.client.model.PlatformType;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@DaoSqlTest
+public class MobileAppJavaClientTest extends AbstractJavaClientTest {
+
+ @Test
+ public void testMobileAppLifecycle() throws Exception {
+ long timestamp = System.currentTimeMillis();
+ List createdApps = new ArrayList<>();
+
+ // create 3 Android apps
+ for (int i = 0; i < 3; i++) {
+ MobileApp app = new MobileApp();
+ app.setPkgName("com.test.android." + timestamp + "." + i);
+ app.setTitle(TEST_PREFIX + "AndroidApp_" + timestamp + "_" + i);
+ app.setAppSecret("secret_android_" + timestamp + "_" + i);
+ app.setPlatformType(PlatformType.ANDROID);
+ app.setStatus(MobileAppStatus.DRAFT);
+
+ MobileApp created = client.saveMobileApp(app);
+ assertNotNull(created);
+ assertNotNull(created.getId());
+ assertEquals(app.getPkgName(), created.getPkgName());
+ assertEquals(PlatformType.ANDROID, created.getPlatformType());
+ assertEquals(MobileAppStatus.DRAFT, created.getStatus());
+
+ createdApps.add(created);
+ }
+
+ // create 2 iOS apps
+ for (int i = 0; i < 2; i++) {
+ MobileApp app = new MobileApp();
+ app.setPkgName("com.test.ios." + timestamp + "." + i);
+ app.setTitle(TEST_PREFIX + "IosApp_" + timestamp + "_" + i);
+ app.setAppSecret("secret_ios_" + timestamp + "_" + i);
+ app.setPlatformType(PlatformType.IOS);
+ app.setStatus(MobileAppStatus.DRAFT);
+
+ MobileApp created = client.saveMobileApp(app);
+ assertNotNull(created);
+ createdApps.add(created);
+ }
+
+ // list all tenant mobile apps
+ PageDataMobileApp allApps = client.getTenantMobileApps(100, 0, null,
+ null, null, null);
+ assertNotNull(allApps);
+ assertEquals(5, allApps.getData().size());
+
+ // list with platform type filter
+ PageDataMobileApp androidApps = client.getTenantMobileApps(100, 0, PlatformType.ANDROID,
+ null, null, null);
+ assertEquals(3, androidApps.getData().size());
+
+ PageDataMobileApp iosApps = client.getTenantMobileApps(100, 0, PlatformType.IOS,
+ null, null, null);
+ assertEquals(2, iosApps.getData().size());
+
+ // get mobile app by id
+ MobileApp searchApp = createdApps.get(1);
+ MobileApp fetchedApp = client.getMobileAppById(searchApp.getId().getId());
+ assertEquals(searchApp.getPkgName(), fetchedApp.getPkgName());
+ assertEquals(searchApp.getTitle(), fetchedApp.getTitle());
+ assertEquals(searchApp.getPlatformType(), fetchedApp.getPlatformType());
+
+ // update mobile app
+ MobileApp appToUpdate = createdApps.get(2);
+ appToUpdate.setTitle(appToUpdate.getTitle() + "_updated");
+ MobileApp updatedApp = client.saveMobileApp(appToUpdate);
+ assertEquals(appToUpdate.getTitle(), updatedApp.getTitle());
+
+ // create mobile app bundle with android and ios apps
+ MobileAppBundle bundle = new MobileAppBundle();
+ bundle.setTitle(TEST_PREFIX + "Bundle_" + timestamp);
+ bundle.setDescription("Test bundle");
+ bundle.setAndroidAppId(createdApps.get(0).getId());
+ bundle.setIosAppId(createdApps.get(3).getId());
+ bundle.setOauth2Enabled(false);
+
+ MobileAppBundle savedBundle = client.saveMobileAppBundle(bundle, null);
+ assertNotNull(savedBundle);
+ assertNotNull(savedBundle.getId());
+ assertEquals(bundle.getTitle(), savedBundle.getTitle());
+
+ // get bundle info by id
+ MobileAppBundleInfo bundleInfo = client.getMobileAppBundleInfoById(savedBundle.getId().getId());
+ assertEquals(savedBundle.getTitle(), bundleInfo.getTitle());
+ assertEquals("Test bundle", bundleInfo.getDescription());
+ assertNotNull(bundleInfo.getAndroidPkgName());
+ assertNotNull(bundleInfo.getIosPkgName());
+
+ // list tenant bundles
+ PageDataMobileAppBundleInfo bundles = client.getTenantMobileAppBundleInfos(100, 0,
+ TEST_PREFIX + "Bundle_" + timestamp, null, null);
+ assertEquals(1, bundles.getData().size());
+
+ // update bundle
+ savedBundle.setDescription("Updated description");
+ MobileAppBundle updatedBundle = client.saveMobileAppBundle(savedBundle, null);
+ assertEquals("Updated description", updatedBundle.getDescription());
+
+ // delete bundle
+ client.deleteMobileAppBundle(savedBundle.getId().getId());
+
+ // verify bundle deletion
+ assertReturns404(() ->
+ client.getMobileAppBundleInfoById(savedBundle.getId().getId())
+ );
+
+ // delete mobile app
+ UUID appToDeleteId = createdApps.get(0).getId().getId();
+ client.deleteMobileApp(appToDeleteId);
+
+ // verify app deletion
+ assertReturns404(() ->
+ client.getMobileAppById(appToDeleteId)
+ );
+
+ PageDataMobileApp appsAfterDelete = client.getTenantMobileApps(100, 0, null,
+ null, null, null);
+ assertEquals(4, appsAfterDelete.getData().size());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/NotificationJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/NotificationJavaClientTest.java
new file mode 100644
index 0000000000..027849d153
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/NotificationJavaClientTest.java
@@ -0,0 +1,278 @@
+/**
+ * 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.model.EntityActionNotificationRuleTriggerConfig;
+import org.thingsboard.client.model.EntityActionRecipientsConfig;
+import org.thingsboard.client.model.EntityType;
+import org.thingsboard.client.model.NotificationDeliveryMethod;
+import org.thingsboard.client.model.NotificationRequest;
+import org.thingsboard.client.model.NotificationRequestInfo;
+import org.thingsboard.client.model.NotificationRule;
+import org.thingsboard.client.model.NotificationRuleInfo;
+import org.thingsboard.client.model.NotificationRuleTriggerType;
+import org.thingsboard.client.model.NotificationSettings;
+import org.thingsboard.client.model.NotificationTarget;
+import org.thingsboard.client.model.NotificationTemplate;
+import org.thingsboard.client.model.NotificationTemplateConfig;
+import org.thingsboard.client.model.NotificationType;
+import org.thingsboard.client.model.PageDataNotification;
+import org.thingsboard.client.model.PageDataNotificationRequestInfo;
+import org.thingsboard.client.model.PageDataNotificationRuleInfo;
+import org.thingsboard.client.model.PageDataNotificationTarget;
+import org.thingsboard.client.model.PageDataNotificationTemplate;
+import org.thingsboard.client.model.PlatformUsersNotificationTargetConfig;
+import org.thingsboard.client.model.TenantAdministratorsFilter;
+import org.thingsboard.client.model.WebDeliveryMethodNotificationTemplate;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@DaoSqlTest
+public class NotificationJavaClientTest extends AbstractJavaClientTest {
+
+ @Test
+ public void testNotificationLifecycle() throws Exception {
+ long timestamp = System.currentTimeMillis();
+
+ // === 1. Notification Target CRUD ===
+
+ // Create target
+ TenantAdministratorsFilter usersFilter = new TenantAdministratorsFilter();
+ PlatformUsersNotificationTargetConfig targetConfig =
+ new PlatformUsersNotificationTargetConfig().usersFilter(usersFilter);
+ NotificationTarget target =
+ new NotificationTarget()
+ .name("Test Target " + timestamp)
+ ._configuration(targetConfig);
+
+ NotificationTarget savedTarget = client.saveNotificationTarget(target);
+ assertNotNull(savedTarget);
+ assertNotNull(savedTarget.getId());
+ assertEquals("Test Target " + timestamp, savedTarget.getName());
+
+ // Get target by ID
+ NotificationTarget fetchedTarget =
+ client.getNotificationTargetById(savedTarget.getId().getId());
+ assertEquals(savedTarget.getName(), fetchedTarget.getName());
+
+ // List targets
+ PageDataNotificationTarget targetsPage =
+ client.getNotificationTargets(100, 0, null, null, null);
+ assertNotNull(targetsPage);
+ assertNotNull(targetsPage.getData());
+ assertTrue(
+ targetsPage.getData().stream()
+ .anyMatch(t -> t.getName().equals(savedTarget.getName())));
+
+ // Update target
+ savedTarget.setName("Updated Target " + timestamp);
+ NotificationTarget updatedTarget = client.saveNotificationTarget(savedTarget);
+ assertEquals("Updated Target " + timestamp, updatedTarget.getName());
+
+ // === 2. Notification Template CRUD ===
+
+ // Create template
+ WebDeliveryMethodNotificationTemplate webTemplate =
+ new WebDeliveryMethodNotificationTemplate()
+ .subject("Test Subject")
+ .body("Test notification body")
+ .enabled(true);
+ NotificationTemplateConfig templateConfig =
+ new NotificationTemplateConfig()
+ .putDeliveryMethodsTemplatesItem("WEB", webTemplate);
+ NotificationTemplate template =
+ new NotificationTemplate()
+ .name("Test Template " + timestamp)
+ .notificationType(NotificationType.GENERAL)
+ ._configuration(templateConfig);
+
+ NotificationTemplate savedTemplate = client.saveNotificationTemplate(template);
+ assertNotNull(savedTemplate);
+ assertNotNull(savedTemplate.getId());
+ assertEquals("Test Template " + timestamp, savedTemplate.getName());
+
+ // Get template by ID
+ NotificationTemplate fetchedTemplate =
+ client.getNotificationTemplateById(savedTemplate.getId().getId());
+ assertEquals(savedTemplate.getName(), fetchedTemplate.getName());
+ assertEquals(NotificationType.GENERAL, fetchedTemplate.getNotificationType());
+
+ // List templates
+ PageDataNotificationTemplate templatesPage =
+ client.getNotificationTemplates(100, 0, null, null, null, null);
+ assertNotNull(templatesPage);
+ assertTrue(
+ templatesPage.getData().stream()
+ .anyMatch(t -> t.getName().equals(savedTemplate.getName())));
+
+ // Update template
+ savedTemplate.setName("Updated Template " + timestamp);
+ NotificationTemplate updatedTemplate = client.saveNotificationTemplate(savedTemplate);
+ assertEquals("Updated Template " + timestamp, updatedTemplate.getName());
+
+ // === 3. Send notification & read notifications ===
+
+ // Send notification request
+ NotificationRequest request =
+ new NotificationRequest()
+ .targets(List.of(savedTarget.getId().getId()))
+ .templateId(savedTemplate.getId());
+ NotificationRequest sentRequest = client.createNotificationRequest(request);
+ assertNotNull(sentRequest);
+ assertNotNull(sentRequest.getId());
+
+ // Get request by ID
+ NotificationRequestInfo fetchedRequest =
+ client.getNotificationRequestById(sentRequest.getId().getId());
+ assertNotNull(fetchedRequest);
+
+ // List requests
+ PageDataNotificationRequestInfo requestsPage =
+ client.getNotificationRequests(100, 0, null, null, null);
+ assertNotNull(requestsPage);
+ assertFalse(requestsPage.getData().isEmpty());
+
+ // Get notifications for current user
+ PageDataNotification notificationsPage =
+ client.getNotifications(100, 0, null, null, null, null, null);
+ assertNotNull(notificationsPage);
+ assertFalse(notificationsPage.getData().isEmpty());
+
+ // Get unread count
+ Integer unreadCount = client.getUnreadNotificationsCount("WEB");
+ assertNotNull(unreadCount);
+ assertTrue("Expected at least one unread notification", unreadCount > 0);
+
+ // Mark single notification as read
+ client.markNotificationAsRead(
+ notificationsPage.getData().get(0).getId().getId());
+
+ // Mark all as read
+ client.markAllNotificationsAsRead(null);
+ Integer unreadAfterMarkAll = client.getUnreadNotificationsCount(null);
+ assertEquals("Expected no unread notifications after marking all as read", 0, unreadAfterMarkAll.intValue());
+
+ // === 4. Notification Settings ===
+
+ NotificationSettings settings = client.getNotificationSettings();
+ assertNotNull(settings);
+
+ List deliveryMethods = client.getAvailableDeliveryMethods();
+ assertNotNull(deliveryMethods);
+ assertTrue(deliveryMethods.contains(NotificationDeliveryMethod.WEB));
+
+ // === 5. Cleanup ===
+
+ // Delete notification request
+ client.deleteNotificationRequest(sentRequest.getId().getId());
+ assertReturns404(() -> client.getNotificationRequestById(sentRequest.getId().getId()));
+
+ // Delete template
+ client.deleteNotificationTemplateById(savedTemplate.getId().getId());
+ assertReturns404(() -> client.getNotificationTemplateById(savedTemplate.getId().getId()));
+
+ // Delete target
+ client.deleteNotificationTargetById(savedTarget.getId().getId());
+ assertReturns404(() -> client.getNotificationTargetById(savedTarget.getId().getId()));
+ }
+
+ @Test
+ public void testNotificationRuleLifecycle() throws Exception {
+ long timestamp = System.currentTimeMillis();
+
+ // Create a target for the rule recipients
+ TenantAdministratorsFilter usersFilter = new TenantAdministratorsFilter();
+ PlatformUsersNotificationTargetConfig targetConfig =
+ new PlatformUsersNotificationTargetConfig().usersFilter(usersFilter);
+ NotificationTarget target =
+ new NotificationTarget()
+ .name("Rule Test Target " + timestamp)
+ ._configuration(targetConfig);
+ NotificationTarget savedTarget = client.saveNotificationTarget(target);
+
+ // Create a template of type ENTITY_ACTION
+ WebDeliveryMethodNotificationTemplate webTemplate =
+ new WebDeliveryMethodNotificationTemplate()
+ .subject("Entity action: ${entityType}")
+ .body("Entity ${entityName} was ${actionType}")
+ .enabled(true);
+ NotificationTemplateConfig templateConfig =
+ new NotificationTemplateConfig()
+ .putDeliveryMethodsTemplatesItem("WEB", webTemplate);
+ NotificationTemplate template =
+ new NotificationTemplate()
+ .name("Rule Test Template " + timestamp)
+ .notificationType(NotificationType.ENTITY_ACTION)
+ ._configuration(templateConfig);
+ NotificationTemplate savedTemplate = client.saveNotificationTemplate(template);
+
+ // Build trigger config: fire on DEVICE create/update
+ EntityActionNotificationRuleTriggerConfig triggerConfig =
+ new EntityActionNotificationRuleTriggerConfig()
+ .addEntityTypesItem(EntityType.DEVICE)
+ .created(true)
+ .updated(true)
+ .deleted(false);
+
+ // Build recipients config
+ EntityActionRecipientsConfig recipientsConfig = new EntityActionRecipientsConfig()
+ .addTargetsItem(savedTarget.getId().getId());
+
+ // saveNotificationRule - create
+ NotificationRule rule = new NotificationRule()
+ .name("Test Rule " + timestamp)
+ .enabled(true)
+ .templateId(savedTemplate.getId())
+ .triggerType(NotificationRuleTriggerType.ENTITY_ACTION)
+ .triggerConfig(triggerConfig)
+ .recipientsConfig(recipientsConfig);
+
+ NotificationRule savedRule = client.saveNotificationRule(rule);
+ assertNotNull(savedRule);
+ assertNotNull(savedRule.getId());
+ assertEquals("Test Rule " + timestamp, savedRule.getName());
+ assertEquals(NotificationRuleTriggerType.ENTITY_ACTION, savedRule.getTriggerType());
+ assertEquals(Boolean.TRUE, savedRule.getEnabled());
+
+ // getNotificationRuleById
+ NotificationRuleInfo fetchedRule = client.getNotificationRuleById(savedRule.getId().getId());
+ assertNotNull(fetchedRule);
+ assertEquals(savedRule.getName(), fetchedRule.getName());
+ assertEquals(NotificationRuleTriggerType.ENTITY_ACTION, fetchedRule.getTriggerType());
+
+ // getNotificationRules - verify it appears in the list
+ PageDataNotificationRuleInfo rulesPage = client.getNotificationRules(100, 0, null, null, null);
+ assertNotNull(rulesPage);
+ assertTrue(rulesPage.getData().stream()
+ .anyMatch(r -> r.getId().getId().equals(savedRule.getId().getId())));
+
+ // deleteNotificationRule
+ client.deleteNotificationRule(savedRule.getId().getId());
+ assertReturns404(() -> client.getNotificationRuleById(savedRule.getId().getId()));
+
+ // Cleanup
+ client.deleteNotificationTemplateById(savedTemplate.getId().getId());
+ client.deleteNotificationTargetById(savedTarget.getId().getId());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/Oauth2JavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/Oauth2JavaClientTest.java
new file mode 100644
index 0000000000..835b5fc64e
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/Oauth2JavaClientTest.java
@@ -0,0 +1,138 @@
+/**
+ * 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.model.MapperType;
+import org.thingsboard.client.model.OAuth2BasicMapperConfig;
+import org.thingsboard.client.model.OAuth2Client;
+import org.thingsboard.client.model.OAuth2ClientInfo;
+import org.thingsboard.client.model.OAuth2MapperConfig;
+import org.thingsboard.client.model.PageDataOAuth2ClientInfo;
+import org.thingsboard.client.model.PlatformType;
+import org.thingsboard.client.model.TenantNameStrategyType;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@DaoSqlTest
+public class Oauth2JavaClientTest extends AbstractJavaClientTest {
+
+ private OAuth2Client createOAuth2Client(String title, String clientId, String clientSecret) {
+ OAuth2BasicMapperConfig basicConfig = new OAuth2BasicMapperConfig();
+ basicConfig.setEmailAttributeKey("email");
+ basicConfig.setFirstNameAttributeKey("given_name");
+ basicConfig.setLastNameAttributeKey("family_name");
+ basicConfig.setTenantNameStrategy(TenantNameStrategyType.DOMAIN);
+
+ OAuth2MapperConfig mapperConfig = new OAuth2MapperConfig();
+ mapperConfig.setType(MapperType.BASIC);
+ mapperConfig.setAllowUserCreation(true);
+ mapperConfig.setActivateUser(false);
+ mapperConfig.setBasic(basicConfig);
+
+ OAuth2Client oAuth2Client = new OAuth2Client();
+ oAuth2Client.setTitle(title);
+ oAuth2Client.setClientId(clientId);
+ oAuth2Client.setClientSecret(clientSecret);
+ oAuth2Client.setAuthorizationUri("https://accounts.google.com/o/oauth2/v2/auth");
+ oAuth2Client.setAccessTokenUri("https://oauth2.googleapis.com/token");
+ oAuth2Client.setScope(List.of("openid", "email", "profile"));
+ oAuth2Client.setUserInfoUri("https://openidconnect.googleapis.com/v1/userinfo");
+ oAuth2Client.setUserNameAttributeName("email");
+ oAuth2Client.setClientAuthenticationMethod("POST");
+ oAuth2Client.setLoginButtonLabel(title);
+ oAuth2Client.setMapperConfig(mapperConfig);
+ oAuth2Client.setPlatforms(List.of(PlatformType.WEB));
+
+ return oAuth2Client;
+ }
+
+ @Test
+ public void testOAuth2ClientLifecycle() throws Exception {
+ long timestamp = System.currentTimeMillis();
+ List createdClients = new ArrayList<>();
+
+ // create 5 OAuth2 clients
+ for (int i = 0; i < 5; i++) {
+ String title = TEST_PREFIX + "OAuth2_" + timestamp + "_" + i;
+ OAuth2Client oAuth2Client = createOAuth2Client(title,
+ "client_id_" + timestamp + "_" + i,
+ "client_secret_" + timestamp + "_" + i);
+
+ OAuth2Client created = client.saveOAuth2Client(oAuth2Client);
+ assertNotNull(created);
+ assertNotNull(created.getId());
+ assertEquals(title, created.getTitle());
+ assertEquals("POST", created.getClientAuthenticationMethod());
+ assertNotNull(created.getMapperConfig());
+ assertEquals(MapperType.BASIC, created.getMapperConfig().getType());
+
+ createdClients.add(created);
+ }
+
+ // list tenant OAuth2 client infos
+ PageDataOAuth2ClientInfo clientInfos = client.findTenantOAuth2ClientInfos(100, 0,
+ TEST_PREFIX + "OAuth2_" + timestamp, null, null);
+ assertNotNull(clientInfos);
+ assertEquals(5, clientInfos.getData().size());
+
+ // get OAuth2 client by id
+ OAuth2Client searchClient = createdClients.get(2);
+ OAuth2Client fetchedClient = client.getOAuth2ClientById(searchClient.getId().getId());
+ assertEquals(searchClient.getTitle(), fetchedClient.getTitle());
+ assertEquals(searchClient.getClientId(), fetchedClient.getClientId());
+ assertEquals(searchClient.getAuthorizationUri(), fetchedClient.getAuthorizationUri());
+ assertEquals(3, fetchedClient.getScope().size());
+
+ // fetch client infos by ids
+ List idsToFetch = List.of(
+ createdClients.get(0).getId().getId().toString(),
+ createdClients.get(1).getId().getId().toString()
+ );
+ List fetchedInfos = client.findTenantOAuth2ClientInfosByIds(idsToFetch);
+ assertEquals(2, fetchedInfos.size());
+
+ // update OAuth2 client
+ OAuth2Client clientToUpdate = client.getOAuth2ClientById(createdClients.get(3).getId().getId());
+ clientToUpdate.setTitle(clientToUpdate.getTitle() + "_updated");
+ clientToUpdate.setLoginButtonLabel("Updated Login");
+ clientToUpdate.setPlatforms(List.of(PlatformType.WEB, PlatformType.ANDROID));
+ OAuth2Client updatedClient = client.saveOAuth2Client(clientToUpdate);
+ assertEquals(clientToUpdate.getTitle(), updatedClient.getTitle());
+ assertEquals("Updated Login", updatedClient.getLoginButtonLabel());
+ assertEquals(2, updatedClient.getPlatforms().size());
+
+ // delete OAuth2 client
+ UUID clientToDeleteId = createdClients.get(0).getId().getId();
+ client.deleteOauth2Client(clientToDeleteId);
+
+ // verify deletion
+ assertReturns404(() ->
+ client.getOAuth2ClientById(clientToDeleteId)
+ );
+
+ PageDataOAuth2ClientInfo clientsAfterDelete = client.findTenantOAuth2ClientInfos(100, 0,
+ TEST_PREFIX + "OAuth2_" + timestamp, null, null);
+ assertEquals(4, clientsAfterDelete.getData().size());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/OtaPackageJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/OtaPackageJavaClientTest.java
new file mode 100644
index 0000000000..ebd6413b65
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/OtaPackageJavaClientTest.java
@@ -0,0 +1,261 @@
+/**
+ * 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.model.ChecksumAlgorithm;
+import org.thingsboard.client.model.DeviceProfileId;
+import org.thingsboard.client.model.DeviceProfileInfo;
+import org.thingsboard.client.model.OtaPackage;
+import org.thingsboard.client.model.OtaPackageInfo;
+import org.thingsboard.client.model.OtaPackageType;
+import org.thingsboard.client.model.PageDataOtaPackageInfo;
+import org.thingsboard.client.model.SaveOtaPackageInfoRequest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.nio.file.Files;
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@DaoSqlTest
+public class OtaPackageJavaClientTest extends AbstractJavaClientTest {
+
+ private static final String OTA_PREFIX = "OtaTest_";
+
+ private DeviceProfileId getDefaultDeviceProfileId() throws Exception {
+ DeviceProfileInfo profileInfo = client.getDefaultDeviceProfileInfo();
+ return (DeviceProfileId) profileInfo.getId();
+ }
+
+ private SaveOtaPackageInfoRequest buildOtaPackageInfoRequest(
+ String title, String version, OtaPackageType type,
+ DeviceProfileId deviceProfileId, boolean usesUrl, String url) {
+ SaveOtaPackageInfoRequest request = new SaveOtaPackageInfoRequest();
+ request.setTitle(title);
+ request.setType(type);
+ request.setUrl(url);
+ request.setVersion(version);
+ request.setDeviceProfileId(deviceProfileId);
+ return request;
+ }
+
+ private OtaPackageInfo createFirmwareInfo(String suffix) throws Exception {
+ DeviceProfileId profileId = getDefaultDeviceProfileId();
+ SaveOtaPackageInfoRequest request = buildOtaPackageInfoRequest(
+ OTA_PREFIX + suffix, "1.0." + System.currentTimeMillis(),
+ OtaPackageType.FIRMWARE, profileId, false, null);
+ return client.saveOtaPackageInfo(request);
+ }
+
+ private OtaPackageInfo createFirmwareWithUrl(String suffix) throws Exception {
+ DeviceProfileId profileId = getDefaultDeviceProfileId();
+ SaveOtaPackageInfoRequest request = buildOtaPackageInfoRequest(
+ OTA_PREFIX + suffix, "1.0." + System.currentTimeMillis(),
+ OtaPackageType.FIRMWARE, profileId, true, "https://example.com/firmware.bin");
+ return client.saveOtaPackageInfo(request);
+ }
+
+ @Test
+ public void testSaveAndGetOtaPackageInfo() throws Exception {
+ long ts = System.currentTimeMillis();
+ DeviceProfileId profileId = getDefaultDeviceProfileId();
+ String title = OTA_PREFIX + "save_" + ts;
+ String version = "1.0." + ts;
+
+ SaveOtaPackageInfoRequest request = buildOtaPackageInfoRequest(
+ title, version, OtaPackageType.FIRMWARE, profileId, true, "https://example.com/fw.bin");
+
+ OtaPackageInfo saved = client.saveOtaPackageInfo(request);
+ assertNotNull(saved);
+ assertNotNull(saved.getId());
+ assertEquals(title, saved.getTitle());
+ assertEquals(version, saved.getVersion());
+ assertEquals(OtaPackageType.FIRMWARE, saved.getType());
+ assertTrue(saved.getUrl().contains("example.com"));
+
+ // get info by id
+ String pkgId = saved.getId().getId().toString();
+ OtaPackageInfo fetched = client.getOtaPackageInfoById(pkgId);
+ assertNotNull(fetched);
+ assertEquals(title, fetched.getTitle());
+ assertEquals(version, fetched.getVersion());
+ }
+
+ @Test
+ public void testGetOtaPackageById() throws Exception {
+ long ts = System.currentTimeMillis();
+ OtaPackageInfo saved = createFirmwareWithUrl("getbyid_" + ts);
+
+ OtaPackage fullPkg = client.getOtaPackageById(saved.getId().getId().toString());
+ assertNotNull(fullPkg);
+ assertEquals(saved.getTitle(), fullPkg.getTitle());
+ assertEquals(saved.getVersion(), fullPkg.getVersion());
+ }
+
+ @Test
+ public void testSaveOtaPackageInfoForSoftware() throws Exception {
+ long ts = System.currentTimeMillis();
+ DeviceProfileId profileId = getDefaultDeviceProfileId();
+ String title = OTA_PREFIX + "sw_" + ts;
+
+ SaveOtaPackageInfoRequest request = buildOtaPackageInfoRequest(
+ title, "2.0." + ts, OtaPackageType.SOFTWARE, profileId, true, "https://example.com/sw.bin");
+
+ OtaPackageInfo saved = client.saveOtaPackageInfo(request);
+ assertNotNull(saved);
+ assertEquals(OtaPackageType.SOFTWARE, saved.getType());
+ assertEquals(title, saved.getTitle());
+ }
+
+ @Test
+ public void testSaveOtaPackageData() throws Exception {
+ long ts = System.currentTimeMillis();
+ OtaPackageInfo info = createFirmwareInfo("data_" + ts);
+
+ File tempFile = Files.createTempFile("ota_test_", ".bin").toFile();
+ tempFile.deleteOnExit();
+ try (FileWriter writer = new FileWriter(tempFile)) {
+ writer.write("test firmware content " + ts);
+ }
+
+ OtaPackageInfo updated = client.saveOtaPackageData(
+ info.getId().getId().toString(), "MD5", tempFile, null);
+ assertNotNull(updated);
+ assertTrue(updated.getHasData());
+ assertNotNull(updated.getFileName());
+ assertNotNull(updated.getDataSize());
+ assertTrue(updated.getDataSize() > 0);
+ assertEquals(ChecksumAlgorithm.MD5, updated.getChecksumAlgorithm());
+ }
+
+ @Test
+ public void testDownloadOtaPackage() throws Exception {
+ long ts = System.currentTimeMillis();
+ OtaPackageInfo info = createFirmwareInfo("download_" + ts);
+
+ String content = "downloadable firmware " + ts;
+ File tempFile = Files.createTempFile("ota_dl_", ".bin").toFile();
+ tempFile.deleteOnExit();
+ try (FileWriter writer = new FileWriter(tempFile)) {
+ writer.write(content);
+ }
+
+ client.saveOtaPackageData(info.getId().getId().toString(), "MD5", tempFile, null);
+
+ File downloaded = client.downloadOtaPackage(info.getId().getId().toString());
+ assertNotNull(downloaded);
+ assertTrue(downloaded.length() > 0);
+ String downloadedContent = Files.readString(downloaded.toPath());
+ assertEquals(content, downloadedContent);
+ }
+
+ @Test
+ public void testDeleteOtaPackage() throws Exception {
+ long ts = System.currentTimeMillis();
+ OtaPackageInfo saved = createFirmwareWithUrl("delete_" + ts);
+
+ String pkgId = saved.getId().getId().toString();
+ client.getOtaPackageInfoById(pkgId);
+
+ client.deleteOtaPackage(pkgId);
+
+ assertReturns404(() -> client.getOtaPackageInfoById(pkgId));
+ }
+
+ @Test
+ public void testGetOtaPackages() throws Exception {
+ long ts = System.currentTimeMillis();
+
+ for (int i = 0; i < 3; i++) {
+ createFirmwareWithUrl("list_" + ts + "_" + i);
+ }
+
+ PageDataOtaPackageInfo page = client.getOtaPackages(100, 0, OTA_PREFIX + "list_" + ts, null, null);
+ assertNotNull(page);
+ assertEquals(3, page.getTotalElements().intValue());
+ for (OtaPackageInfo pkg : page.getData()) {
+ assertTrue(pkg.getTitle().startsWith(OTA_PREFIX + "list_" + ts));
+ }
+ }
+
+ @Test
+ public void testGetOtaPackagesByDeviceProfileAndType() throws Exception {
+ long ts = System.currentTimeMillis();
+ DeviceProfileId profileId = getDefaultDeviceProfileId();
+
+ createFirmwareWithUrl("byprofile_" + ts + "_0");
+ createFirmwareWithUrl("byprofile_" + ts + "_1");
+
+ PageDataOtaPackageInfo page = client.getOtaPackagesByDeviceProfileAndType(
+ profileId.getId().toString(), "FIRMWARE", 100, 0,
+ OTA_PREFIX + "byprofile_" + ts, null, null);
+ assertNotNull(page);
+ assertEquals(2, page.getTotalElements().intValue());
+ }
+
+ @Test
+ public void testGetOtaPackageInfoById_notFound() {
+ String nonExistentId = UUID.randomUUID().toString();
+ assertReturns404(() -> client.getOtaPackageInfoById(nonExistentId));
+ }
+
+ @Test
+ public void testGetOtaPackagesPagination() throws Exception {
+ long ts = System.currentTimeMillis();
+
+ for (int i = 0; i < 5; i++) {
+ createFirmwareWithUrl("paged_" + ts + "_" + i);
+ }
+
+ PageDataOtaPackageInfo page1 = client.getOtaPackages(2, 0, OTA_PREFIX + "paged_" + ts, null, null);
+ assertNotNull(page1);
+ assertEquals(5, page1.getTotalElements().intValue());
+ assertEquals(3, page1.getTotalPages().intValue());
+ assertEquals(2, page1.getData().size());
+ assertTrue(page1.getHasNext());
+
+ PageDataOtaPackageInfo lastPage = client.getOtaPackages(2, 2, OTA_PREFIX + "paged_" + ts, null, null);
+ assertEquals(1, lastPage.getData().size());
+ assertFalse(lastPage.getHasNext());
+ }
+
+ @Test
+ public void testUpdateOtaPackageInfo() throws Exception {
+ long ts = System.currentTimeMillis();
+ OtaPackageInfo saved = createFirmwareWithUrl("update_" + ts);
+
+ SaveOtaPackageInfoRequest updateReq = new SaveOtaPackageInfoRequest();
+ updateReq.setId(saved.getId());
+ updateReq.setTitle(saved.getTitle());
+ updateReq.setType(saved.getType());
+ updateReq.setVersion(saved.getVersion());
+ updateReq.setDeviceProfileId(saved.getDeviceProfileId());
+ updateReq.setUrl(saved.getUrl());
+ updateReq.setAdditionalInfo(OBJECT_MAPPER.createObjectNode().put("infoKey", "infoValue"));
+
+ OtaPackageInfo updated = client.saveOtaPackageInfo(updateReq);
+ assertNotNull(updated);
+ assertEquals(saved.getId().getId(), updated.getId().getId());
+ assertEquals("infoValue", updated.getAdditionalInfo().get("infoKey").asText());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/RpcV1JavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/RpcV1JavaClientTest.java
new file mode 100644
index 0000000000..399ecd4df9
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/RpcV1JavaClientTest.java
@@ -0,0 +1,72 @@
+/**
+ * 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.ApiException;
+import org.thingsboard.client.model.Device;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import static org.junit.Assert.assertEquals;
+
+@DaoSqlTest
+public class RpcV1JavaClientTest extends AbstractJavaClientTest {
+
+ private static final String ONE_WAY_BODY =
+ "{\"method\":\"setGpio\",\"params\":{\"pin\":7,\"value\":1},\"persistent\":true}";
+ private static final String TWO_WAY_BODY =
+ "{\"method\":\"getGpio\",\"params\":{\"pin\":7},\"persistent\":true}";
+
+ @Test
+ public void testHandleOneWayDeviceRPCRequest() throws Exception {
+ long ts = System.currentTimeMillis();
+ Device device = createNewDevice(TEST_PREFIX + ts);
+ String deviceId = device.getId().getId().toString();
+
+ try {
+ client.handleOneWayDeviceRPCRequestV1(deviceId, ONE_WAY_BODY);
+ } catch (ApiException e) {
+ assertEquals("handleOneWayDeviceRPCRequest got an unexpected HTTP error: " + e.getCode(),
+ 0, e.getCode());
+ }
+
+ client.deleteDevice(deviceId);
+ }
+
+ @Test
+ public void testHandleTwoWayDeviceRPCRequest() throws Exception {
+ long ts = System.currentTimeMillis();
+ Device device = createNewDevice(TEST_PREFIX + ts);
+ String deviceId = device.getId().getId().toString();
+
+ try {
+ client.handleTwoWayDeviceRPCRequestV1(deviceId, TWO_WAY_BODY);
+ } catch (ApiException e) {
+ assertEquals("handleTwoWayDeviceRPCRequest got an unexpected HTTP error: " + e.getCode(),
+ 0, e.getCode());
+ }
+
+ client.deleteDevice(deviceId);
+ }
+
+ private Device createNewDevice(String name) throws ApiException {
+ Device device = new Device();
+ device.setName(name);
+ device.setType("default");
+ return client.saveDevice(device, null, null, null, null);
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/RpcV2JavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/RpcV2JavaClientTest.java
new file mode 100644
index 0000000000..43669c1620
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/RpcV2JavaClientTest.java
@@ -0,0 +1,133 @@
+/**
+ * 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.ApiException;
+import org.thingsboard.client.model.Device;
+import org.thingsboard.client.model.Rpc;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@DaoSqlTest
+public class RpcV2JavaClientTest extends AbstractJavaClientTest {
+
+ private static final String PERSISTENT_BODY =
+ "{\"method\":\"setGpio\",\"params\":{\"pin\":7,\"value\":1},\"persistent\":true}";
+
+ @Test
+ public void testHandleOneWayDeviceRPCRequest() throws Exception {
+ long ts = System.currentTimeMillis();
+ Device device = createNewDevice(TEST_PREFIX + ts);
+ String deviceId = device.getId().getId().toString();
+
+ try {
+ client.handleOneWayDeviceRPCRequestV2(deviceId, PERSISTENT_BODY);
+ } catch (ApiException e) {
+ assertEquals("handleOneWayDeviceRPCRequest1 got an unexpected HTTP error: " + e.getCode(),
+ 0, e.getCode());
+ }
+
+ client.deleteDevice(deviceId);
+ }
+
+ @Test
+ public void testHandleTwoWayDeviceRPCRequest() throws Exception {
+ long ts = System.currentTimeMillis();
+ Device device = createNewDevice(TEST_PREFIX + ts);
+ String deviceId = device.getId().getId().toString();
+
+ try {
+ client.handleTwoWayDeviceRPCRequestV2(deviceId, PERSISTENT_BODY);
+ } catch (ApiException e) {
+ assertEquals("handleTwoWayDeviceRPCRequest1 got an unexpected HTTP error: " + e.getCode(),
+ 0, e.getCode());
+ }
+
+ client.deleteDevice(deviceId);
+ }
+
+ @Test
+ public void testGetPersistedRpcAndDeleteRpc() throws Exception {
+ long ts = System.currentTimeMillis();
+ Device device = createNewDevice(TEST_PREFIX + ts);
+ String deviceId = device.getId().getId().toString();
+
+ String rpcId = postPersistentRpcAndGetId(deviceId);
+ assertNotNull(rpcId);
+
+ Rpc rpc = client.getPersistedRpc(rpcId);
+ assertNotNull(rpc);
+ assertNotNull(rpc.getId());
+
+ client.deleteRpc(rpcId);
+
+ assertReturns404(() -> client.getPersistedRpc(rpcId));
+
+ client.deleteDevice(deviceId);
+ }
+
+ @Test
+ public void testGetPersistedRpcNotFound() {
+ assertReturns404(() -> client.getPersistedRpc(UUID.randomUUID().toString()));
+ }
+
+ @Test
+ public void testGetPersistedRpcByDevice() throws Exception {
+ long ts = System.currentTimeMillis();
+ Device device = createNewDevice(TEST_PREFIX + ts);
+ String deviceId = device.getId().getId().toString();
+
+ postPersistentRpcAndGetId(deviceId);
+
+ try {
+ client.getPersistedRpcByDevice(deviceId, 100, 0, null, null, null, null);
+ } catch (ApiException e) {
+ assertEquals("getPersistedRpcByDevice got an unexpected HTTP error: " + e.getCode(),
+ 0, e.getCode());
+ }
+
+ client.deleteDevice(deviceId);
+ }
+
+ private Device createNewDevice(String name) throws ApiException {
+ Device device = new Device();
+ device.setName(name);
+ device.setType("default");
+ return client.saveDevice(device, null, null, null, null);
+ }
+
+ private String postPersistentRpcAndGetId(String deviceId) throws IOException, InterruptedException {
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(getBaseUrl() + "/api/plugins/rpc/oneway/" + deviceId))
+ .header("Content-Type", "application/json")
+ .header("Authorization", "Bearer " + client.getToken())
+ .POST(HttpRequest.BodyPublishers.ofString(PERSISTENT_BODY))
+ .build();
+ HttpResponse response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
+ return OBJECT_MAPPER.readTree(response.body()).get("rpcId").asText();
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/RuleChainJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/RuleChainJavaClientTest.java
new file mode 100644
index 0000000000..95b5301d9f
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/RuleChainJavaClientTest.java
@@ -0,0 +1,163 @@
+/**
+ * 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.model.NodeConnectionInfo;
+import org.thingsboard.client.model.PageDataRuleChain;
+import org.thingsboard.client.model.RuleChain;
+import org.thingsboard.client.model.RuleChainMetaData;
+import org.thingsboard.client.model.RuleChainType;
+import org.thingsboard.client.model.RuleNode;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@DaoSqlTest
+public class RuleChainJavaClientTest extends AbstractJavaClientTest {
+
+ @Test
+ public void testRuleChainAndNodeLifecycle() throws Exception {
+ long timestamp = System.currentTimeMillis();
+ List createdChains = new ArrayList<>();
+
+ // create 5 rule chains
+ for (int i = 0; i < 5; i++) {
+ RuleChain ruleChain = new RuleChain();
+ ruleChain.setName(TEST_PREFIX + "RuleChain_" + timestamp + "_" + i);
+ ruleChain.setType(RuleChainType.CORE);
+ ruleChain.setDebugMode(false);
+
+ RuleChain created = client.saveRuleChain(ruleChain);
+ assertNotNull(created);
+ assertNotNull(created.getId());
+ assertEquals(ruleChain.getName(), created.getName());
+ assertEquals(RuleChainType.CORE, created.getType());
+
+ createdChains.add(created);
+ }
+
+ // list rule chains with text search
+ PageDataRuleChain filteredChains = client.getRuleChains(100, 0, null,
+ TEST_PREFIX + "RuleChain_" + timestamp, null, null);
+ assertNotNull(filteredChains);
+ assertEquals(5, filteredChains.getData().size());
+
+ // get rule chain by id
+ RuleChain searchChain = createdChains.get(2);
+ RuleChain fetchedChain = client.getRuleChainById(searchChain.getId().getId().toString());
+ assertEquals(searchChain.getName(), fetchedChain.getName());
+ assertEquals(searchChain.getType(), fetchedChain.getType());
+
+ // get metadata (initially has default node)
+ RuleChainMetaData metadata = client.getRuleChainMetaData(searchChain.getId().getId().toString());
+ assertNotNull(metadata);
+ assertEquals(searchChain.getId().getId(), metadata.getRuleChainId().getId());
+
+ // save metadata with rule nodes and connections
+ RuleChainMetaData newMetadata = new RuleChainMetaData(metadata.getRuleChainId());
+ newMetadata.setVersion(metadata.getVersion());
+ newMetadata.setFirstNodeIndex(0);
+
+ // node 0: message type switch
+ RuleNode switchNode = new RuleNode();
+ switchNode.setName("Message Type Switch");
+ switchNode.setType("org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode");
+ switchNode.setConfiguration(OBJECT_MAPPER.createObjectNode().put("version", 0));
+ switchNode.setAdditionalInfo(OBJECT_MAPPER.createObjectNode().put("layoutX", 200).put("layoutY", 150));
+
+ // node 1: log node for telemetry
+ RuleNode logNode = new RuleNode();
+ logNode.setName("Log Telemetry");
+ logNode.setType("org.thingsboard.rule.engine.action.TbLogNode");
+ logNode.setConfiguration(OBJECT_MAPPER.createObjectNode()
+ .put("scriptLang", "TBEL")
+ .put("jsScript", "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);")
+ .put("tbelScript", "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"));
+ logNode.setAdditionalInfo(OBJECT_MAPPER.createObjectNode().put("layoutX", 500).put("layoutY", 100));
+
+ // node 2: save timeseries
+ RuleNode saveNode = new RuleNode();
+ saveNode.setName("Save Timeseries");
+ saveNode.setType("org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode");
+ saveNode.setConfiguration(OBJECT_MAPPER.createObjectNode()
+ .put("defaultTTL", 0)
+ .put("skipLatestPersistence", false)
+ .put("useServerTs", false));
+ saveNode.setAdditionalInfo(OBJECT_MAPPER.createObjectNode().put("layoutX", 500).put("layoutY", 250));
+
+ newMetadata.setNodes(List.of(switchNode, logNode, saveNode));
+
+ // connection: switch -> log (on "Post telemetry")
+ NodeConnectionInfo conn1 = new NodeConnectionInfo();
+ conn1.setFromIndex(0);
+ conn1.setToIndex(1);
+ conn1.setType("Post telemetry");
+
+ // connection: switch -> save timeseries (on "Post telemetry")
+ NodeConnectionInfo conn2 = new NodeConnectionInfo();
+ conn2.setFromIndex(0);
+ conn2.setToIndex(2);
+ conn2.setType("Post telemetry");
+
+ newMetadata.setConnections(List.of(conn1, conn2));
+ newMetadata.setRuleChainConnections(List.of());
+
+ RuleChainMetaData savedMetadata = client.saveRuleChainMetaData(newMetadata, false);
+ assertNotNull(savedMetadata);
+ assertEquals(3, savedMetadata.getNodes().size());
+ assertEquals(2, savedMetadata.getConnections().size());
+
+ // verify saved nodes
+ RuleChainMetaData fetchedMetadata = client.getRuleChainMetaData(searchChain.getId().getId().toString());
+ assertEquals(3, fetchedMetadata.getNodes().size());
+ assertTrue(fetchedMetadata.getNodes().stream()
+ .anyMatch(node -> "Log Telemetry".equals(node.getName())));
+ assertTrue(fetchedMetadata.getNodes().stream()
+ .anyMatch(node -> "Save Timeseries".equals(node.getName())));
+
+ // get output labels
+ client.getRuleChainOutputLabels(searchChain.getId().getId().toString());
+
+ // update rule chain
+ RuleChain chainToUpdate = createdChains.get(3);
+ chainToUpdate.setName(chainToUpdate.getName() + "_updated");
+ chainToUpdate.setDebugMode(true);
+ RuleChain updatedChain = client.saveRuleChain(chainToUpdate);
+ assertEquals(chainToUpdate.getName(), updatedChain.getName());
+ assertEquals(true, updatedChain.getDebugMode());
+
+ // delete rule chain
+ UUID chainToDeleteId = createdChains.get(0).getId().getId();
+ client.deleteRuleChain(chainToDeleteId.toString());
+
+ // verify deletion
+ assertReturns404(() ->
+ client.getRuleChainById(chainToDeleteId.toString())
+ );
+
+ PageDataRuleChain chainsAfterDelete = client.getRuleChains(100, 0, null,
+ TEST_PREFIX + "RuleChain_" + timestamp, null, null);
+ assertEquals(4, chainsAfterDelete.getData().size());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/TbImageJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/TbImageJavaClientTest.java
new file mode 100644
index 0000000000..5dbe358991
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/TbImageJavaClientTest.java
@@ -0,0 +1,151 @@
+/**
+ * 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.model.PageDataTbResourceInfo;
+import org.thingsboard.client.model.ResourceExportData;
+import org.thingsboard.client.model.TbImageDeleteResult;
+import org.thingsboard.client.model.TbResourceInfo;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import javax.imageio.ImageIO;
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@DaoSqlTest
+public class TbImageJavaClientTest extends AbstractJavaClientTest {
+
+ private File createTempImage(String name, Color color) throws IOException {
+ BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
+ Graphics2D g = img.createGraphics();
+ g.setColor(color);
+ g.fillRect(0, 0, 100, 100);
+ g.dispose();
+
+ File tempFile = File.createTempFile(name, ".png");
+ tempFile.deleteOnExit();
+ ImageIO.write(img, "png", tempFile);
+ return tempFile;
+ }
+
+ @Test
+ public void testImageLifecycle() throws Exception {
+ long timestamp = System.currentTimeMillis();
+ List createdImages = new ArrayList<>();
+ Color[] colors = {Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.CYAN};
+
+ // upload 5 images
+ for (int i = 0; i < 5; i++) {
+ String title = TEST_PREFIX + "Image_" + timestamp + "_" + i;
+ File imageFile = createTempImage("test_image_" + i, colors[i]);
+
+ TbResourceInfo uploaded = client.uploadImage(imageFile, title, null);
+ assertNotNull(uploaded);
+ assertNotNull(uploaded.getResourceKey());
+ assertEquals(title, uploaded.getTitle());
+ assertNotNull(uploaded.getLink());
+
+ createdImages.add(uploaded);
+ }
+
+ // list images with text search
+ PageDataTbResourceInfo filteredImages = client.getImages(100, 0, null, false,
+ TEST_PREFIX + "Image_" + timestamp, null, null);
+ assertNotNull(filteredImages);
+ assertEquals(5, filteredImages.getData().size());
+
+ // get image info by type and key
+ TbResourceInfo searchImage = createdImages.get(2);
+ TbResourceInfo fetchedInfo = client.getImageInfo("tenant", searchImage.getResourceKey());
+ assertEquals(searchImage.getTitle(), fetchedInfo.getTitle());
+ assertEquals(searchImage.getResourceKey(), fetchedInfo.getResourceKey());
+
+ // download image
+ File downloadedImage = client.downloadImage("tenant", searchImage.getResourceKey(), null, null);
+ assertNotNull(downloadedImage);
+ assertTrue(downloadedImage.exists());
+ assertTrue(downloadedImage.length() > 0);
+
+ // download image preview
+ File preview = client.downloadImagePreview("tenant", searchImage.getResourceKey(), null, null);
+ assertNotNull(preview);
+ assertTrue(preview.exists());
+ assertTrue(preview.length() > 0);
+
+ // update image file
+ File updatedImageFile = createTempImage("updated_image", Color.MAGENTA);
+ TbResourceInfo updatedImage = client.updateImage("tenant", searchImage.getResourceKey(), updatedImageFile);
+ assertNotNull(updatedImage);
+ assertEquals(searchImage.getResourceKey(), updatedImage.getResourceKey());
+
+ // update image info (title)
+ TbResourceInfo infoToUpdate = client.getImageInfo("tenant", createdImages.get(3).getResourceKey());
+ infoToUpdate.setTitle(infoToUpdate.getTitle() + "_updated");
+ TbResourceInfo updatedInfo = client.updateImageInfo("tenant", infoToUpdate.getResourceKey(), infoToUpdate);
+ assertEquals(infoToUpdate.getTitle(), updatedInfo.getTitle());
+
+ // make image public
+ TbResourceInfo publicImage = client.updateImagePublicStatus("tenant",
+ createdImages.get(1).getResourceKey(), true);
+ assertTrue(publicImage.getPublic());
+ assertNotNull(publicImage.getPublicResourceKey());
+ assertNotNull(publicImage.getPublicLink());
+
+ // download public image
+ File publicDownload = client.downloadPublicImage(publicImage.getPublicResourceKey(), null, null);
+ assertNotNull(publicDownload);
+ assertTrue(publicDownload.exists());
+ assertTrue(publicDownload.length() > 0);
+
+ // make image private again
+ TbResourceInfo privateImage = client.updateImagePublicStatus("tenant",
+ createdImages.get(1).getResourceKey(), false);
+ assertEquals(false, privateImage.getPublic());
+
+ // export image
+ ResourceExportData exportData = client.exportImage("tenant", createdImages.get(4).getResourceKey());
+ assertNotNull(exportData);
+ assertNotNull(exportData.getData());
+ assertEquals(createdImages.get(4).getTitle(), exportData.getTitle());
+ assertEquals(createdImages.get(4).getResourceKey(), exportData.getResourceKey());
+
+ // delete image
+ String keyToDelete = createdImages.get(0).getResourceKey();
+ TbImageDeleteResult deleteResult = client.deleteImage("tenant", keyToDelete, false);
+ assertNotNull(deleteResult);
+ assertTrue(deleteResult.getSuccess());
+
+ // verify deletion
+ assertReturns404(() ->
+ client.getImageInfo("tenant", keyToDelete)
+ );
+
+ PageDataTbResourceInfo imagesAfterDelete = client.getImages(100, 0, null, false,
+ TEST_PREFIX + "Image_" + timestamp, null, null);
+ assertEquals(4, imagesAfterDelete.getData().size());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/TbResourceJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/TbResourceJavaClientTest.java
new file mode 100644
index 0000000000..156667fecd
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/TbResourceJavaClientTest.java
@@ -0,0 +1,128 @@
+/**
+ * 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.model.PageDataTbResourceInfo;
+import org.thingsboard.client.model.ResourceType;
+import org.thingsboard.client.model.TbResource;
+import org.thingsboard.client.model.TbResourceInfo;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@DaoSqlTest
+public class TbResourceJavaClientTest extends AbstractJavaClientTest {
+
+ @Test
+ public void testResourceLifecycle() throws Exception {
+ long timestamp = System.currentTimeMillis();
+ List createdResources = new ArrayList<>();
+
+ // create 5 JS_MODULE resources
+ for (int i = 0; i < 5; i++) {
+ TbResource resource = new TbResource();
+ resource.setTitle(TEST_PREFIX + "Resource_" + timestamp + "_" + i);
+ resource.setResourceType(ResourceType.JS_MODULE);
+ resource.setResourceKey("test_module_" + timestamp + "_" + i + ".js");
+ resource.setFileName("test_module_" + timestamp + "_" + i + ".js");
+
+ String jsContent = "export default function test" + i + "() { return " + i + "; }";
+ resource.setData(Base64.getEncoder().encodeToString(jsContent.getBytes()));
+
+ TbResourceInfo created = client.saveResource(resource);
+ assertNotNull(created);
+ assertNotNull(created.getId());
+ assertEquals(resource.getTitle(), created.getTitle());
+ assertEquals(ResourceType.JS_MODULE, created.getResourceType());
+
+ createdResources.add(created);
+ }
+
+ // get tenant resources, check count
+ PageDataTbResourceInfo tenantResources = client.getTenantResources(100, 0, null, null, null);
+ assertNotNull(tenantResources);
+ assertNotNull(tenantResources.getData());
+ int initialSize = tenantResources.getData().size();
+ assertTrue("Expected at least 5 resources, but got " + initialSize, initialSize >= 5);
+
+ // find with text search
+ PageDataTbResourceInfo filteredResources = client.getTenantResources(100, 0,
+ TEST_PREFIX + "Resource_" + timestamp, null, null);
+ assertEquals(5, filteredResources.getData().size());
+
+ // get resources with type filter
+ PageDataTbResourceInfo jsResources = client.getResources(100, 0,
+ ResourceType.JS_MODULE.getValue(), null, TEST_PREFIX + "Resource_" + timestamp, null, null);
+ assertEquals(5, jsResources.getData().size());
+
+ // get resource info by id
+ TbResourceInfo searchResource = createdResources.get(2);
+ TbResourceInfo fetchedInfo = client.getResourceInfoById(searchResource.getId().getId().toString());
+ assertEquals(searchResource.getTitle(), fetchedInfo.getTitle());
+ assertEquals(searchResource.getResourceKey(), fetchedInfo.getResourceKey());
+
+ // get full resource by id (includes data)
+ TbResource fullResource = client.getResourceById(searchResource.getId().getId().toString());
+ assertNotNull(fullResource);
+ assertEquals(searchResource.getTitle(), fullResource.getTitle());
+ assertNotNull(fullResource.getData());
+
+ // download resource
+ File downloadedFile = client.downloadResource(searchResource.getId().getId().toString());
+ assertNotNull(downloadedFile);
+ assertTrue(downloadedFile.exists());
+ assertTrue(downloadedFile.length() > 0);
+
+ // get resources by list of ids
+ List idsToFetch = List.of(
+ createdResources.get(0).getId().getId().toString(),
+ createdResources.get(1).getId().getId().toString()
+ );
+ List resourceList = client.getSystemOrTenantResourcesByIds(idsToFetch);
+ assertEquals(2, resourceList.size());
+
+ // update resource
+ TbResource resourceToUpdate = client.getResourceById(createdResources.get(3).getId().getId().toString());
+ resourceToUpdate.setTitle(resourceToUpdate.getTitle() + "_updated");
+ String updatedContent = "export default function updated() { return 42; }";
+ resourceToUpdate.setData(Base64.getEncoder().encodeToString(updatedContent.getBytes()));
+ TbResourceInfo updatedResource = client.saveResource(resourceToUpdate);
+ assertEquals(resourceToUpdate.getTitle(), updatedResource.getTitle());
+
+ // delete resource
+ UUID resourceToDeleteId = createdResources.get(0).getId().getId();
+ client.deleteResource(resourceToDeleteId.toString(), false);
+
+ // verify deletion
+ assertReturns404(() ->
+ client.getResourceInfoById(resourceToDeleteId.toString())
+ );
+
+ PageDataTbResourceInfo resourcesAfterDelete = client.getTenantResources(100, 0,
+ TEST_PREFIX + "Resource_" + timestamp, null, null);
+ assertEquals(4, resourcesAfterDelete.getData().size());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/TelemetryJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/TelemetryJavaClientTest.java
new file mode 100644
index 0000000000..8028c66682
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/TelemetryJavaClientTest.java
@@ -0,0 +1,150 @@
+/**
+ * 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.model.AttributeData;
+import org.thingsboard.client.model.Device;
+import org.thingsboard.client.model.TsData;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@DaoSqlTest
+public class TelemetryJavaClientTest extends AbstractJavaClientTest {
+
+ @Test
+ public void testTelemetryLifecycle() throws Exception {
+ long timestamp = System.currentTimeMillis();
+
+ // create a device for telemetry operations
+ Device device = new Device();
+ device.setName("TelemetryTestDevice_" + timestamp);
+ device.setType("default");
+ Device createdDevice = client.saveDevice(device, null, null, null, null);
+ assertNotNull(createdDevice);
+
+ String entityType = "DEVICE";
+ String entityId = createdDevice.getId().getId().toString();
+
+ // save server-side attributes
+ String serverAttributes = "{\"serverAttr1\": \"value1\", \"serverAttr2\": 42}";
+ client.saveEntityAttributesV2(entityType, entityId, "SERVER_SCOPE", serverAttributes);
+
+ // save shared attributes
+ String sharedAttributes = "{\"sharedAttr1\": \"sharedValue1\", \"sharedAttr2\": true}";
+ client.saveEntityAttributesV2(entityType, entityId, "SHARED_SCOPE", sharedAttributes);
+
+ // get attribute keys
+ List allKeys = client.getAttributeKeys(entityType, entityId);
+ assertNotNull(allKeys);
+ assertTrue(allKeys.containsAll(List.of("serverAttr1", "serverAttr2", "sharedAttr1", "sharedAttr2")));
+
+ // get attribute keys by scope
+ List serverKeys = client.getAttributeKeysByScope(entityType, entityId, "SERVER_SCOPE");
+ assertEquals(2 + 1, serverKeys.size()); //active attribute is automatically added to server scope
+ assertTrue(serverKeys.containsAll(List.of("serverAttr1", "serverAttr2", "active")));
+
+ // get attributes by scope
+ List serverAttrs = client.getAttributesByScope(entityType, entityId, "SERVER_SCOPE", "serverAttr1,serverAttr2", null);
+ assertNotNull(serverAttrs);
+ assertEquals(2, serverAttrs.size());
+
+ // get all attributes
+ List allAttrs = client.getAttributes(entityType, entityId, "serverAttr1,sharedAttr1", null);
+ assertEquals(2, allAttrs.size());
+ assertEquals("value1", allAttrs.stream().filter(attr -> attr.getKey().equals("serverAttr1")).findFirst().orElseThrow().getValue().toString());
+ assertEquals("sharedValue1", allAttrs.stream().filter(attr -> attr.getKey().equals("sharedAttr1")).findFirst().orElseThrow().getValue().toString());
+
+ // save timeseries data
+ long ts1 = timestamp - 60000;
+ long ts2 = timestamp - 30000;
+ long ts3 = timestamp;
+ String telemetryBody = "{\"ts\":" + ts1 + ",\"values\":{\"temperature\":25.5,\"humidity\":60}}";
+ client.saveEntityTelemetry(entityType, entityId, "ANY", telemetryBody);
+
+ String telemetryBody2 = "{\"ts\":" + ts2 + ",\"values\":{\"temperature\":26.0,\"humidity\":58}}";
+ client.saveEntityTelemetry(entityType, entityId, "ANY", telemetryBody2);
+
+ String telemetryBody3 = "{\"ts\":" + ts3 + ",\"values\":{\"temperature\":27.1,\"humidity\":55}}";
+ client.saveEntityTelemetry(entityType, entityId, "ANY", telemetryBody3);
+
+ // get timeseries keys
+ List tsKeys = client.getTimeseriesKeys(entityType, entityId);
+ assertNotNull(tsKeys);
+ assertEquals(2, tsKeys.size());
+ assertTrue(tsKeys.containsAll(List.of("humidity", "temperature")));
+
+ // get latest timeseries
+ Map> latestData = client.getLatestTimeseries(entityType, entityId, "temperature,humidity", false, null);
+ assertNotNull(latestData);
+ assertNotNull(latestData.get("temperature"));
+ assertFalse(latestData.get("temperature").isEmpty());
+ assertEquals("27.1", latestData.get("temperature").get(0).getValue().toString());
+
+ // get timeseries history
+ Map> historyData = client.getTimeseriesHistory(
+ entityType, entityId,
+ ts1 - 1000, ts3 + 1000, "temperature",
+ null, null, null, null, "NONE", "ASC", false, null);
+ assertNotNull(historyData);
+ List tempHistory = historyData.get("temperature");
+ assertNotNull(tempHistory);
+ assertEquals(3, tempHistory.size());
+ assertEquals("25.5", tempHistory.get(0).getValue().toString());
+ assertEquals("27.1", tempHistory.get(2).getValue().toString());
+
+ // delete timeseries
+ client.deleteEntityTimeseries(entityType, entityId, "humidity", true, null, null, true, false, null);
+
+ List keysAfterDelete = client.getTimeseriesKeys(entityType, entityId);
+ assertFalse(keysAfterDelete.contains("humidity"));
+
+ // delete attributes
+ client.deleteEntityAttributes(entityType, entityId, "SERVER_SCOPE", "serverAttr1", null);
+
+ List serverKeysAfterDelete = client.getAttributeKeysByScope(entityType, entityId, "SERVER_SCOPE");
+ assertFalse(serverKeysAfterDelete.contains("serverAttr1"));
+ assertTrue(serverKeysAfterDelete.contains("serverAttr2"));
+
+ // save device attributes using device-specific endpoint
+ client.saveDeviceAttributes(entityId, "SERVER_SCOPE", "{\"deviceSpecificAttr\": \"test\"}");
+
+ List deviceKeys = client.getAttributeKeysByScope(entityType, entityId, "SERVER_SCOPE");
+ assertTrue(deviceKeys.contains("deviceSpecificAttr"));
+
+ // delete device attributes
+ client.deleteDeviceAttributes(entityId, "SERVER_SCOPE", "deviceSpecificAttr", null);
+
+ List deviceKeysAfterDelete = client.getAttributeKeysByScope(entityType, entityId, "SERVER_SCOPE");
+ assertFalse(deviceKeysAfterDelete.contains("deviceSpecificAttr"));
+
+ // save telemetry with TTL
+ String ttlTelemetry = "{\"ts\":" + timestamp + ",\"values\":{\"shortLived\":99}}";
+ client.saveEntityTelemetryWithTTL(entityType, entityId, "ANY", 86400L, ttlTelemetry);
+
+ Map> latestWithTtl = client.getLatestTimeseries(entityType, entityId, "shortLived", false, null);
+ assertNotNull(latestWithTtl.get("shortLived"));
+ assertEquals("99", latestWithTtl.get("shortLived").get(0).getValue().toString());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/TenantJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/TenantJavaClientTest.java
new file mode 100644
index 0000000000..4ab0f2710e
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/TenantJavaClientTest.java
@@ -0,0 +1,119 @@
+/**
+ * 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.ApiException;
+import org.thingsboard.client.model.Authority;
+import org.thingsboard.client.model.PageDataTenant;
+import org.thingsboard.client.model.PageDataUser;
+import org.thingsboard.client.model.Tenant;
+import org.thingsboard.client.model.User;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@DaoSqlTest
+public class TenantJavaClientTest extends AbstractJavaClientTest {
+
+ @Test
+ public void testTenantLifecycle() throws Exception {
+ long timestamp = System.currentTimeMillis();
+ List createdTenants = new ArrayList<>();
+
+ // authenticate as sysadmin for tenant management
+ client.login("sysadmin@thingsboard.org", "sysadmin");
+
+ // create 20 tenants
+ for (int i = 0; i < 20; i++) {
+ Tenant tenant = new Tenant();
+ String tenantTitle = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i;
+ tenant.setTitle(tenantTitle);
+ tenant.setEmail("tenant_" + timestamp + "_" + i + "@test.com");
+ tenant.setCountry("US");
+ tenant.setCity("City" + i);
+
+ Tenant createdTenant = client.saveTenant(tenant);
+ assertNotNull(createdTenant);
+ assertNotNull(createdTenant.getId());
+ assertEquals(tenantTitle, createdTenant.getTitle());
+
+ createdTenants.add(createdTenant);
+ }
+
+ try {
+ // find all with search text, check count
+ PageDataTenant filteredTenants = client.getTenants(100, 0, TEST_PREFIX_2, null, null);
+ assertEquals("Expected exactly 10 tenants matching prefix", 10, filteredTenants.getData().size());
+
+ // find by id
+ Tenant searchTenant = createdTenants.get(10);
+ Tenant fetchedTenant = client.getTenantById(searchTenant.getId().getId().toString());
+ assertEquals(searchTenant.getTitle(), fetchedTenant.getTitle());
+ assertEquals(searchTenant.getEmail(), fetchedTenant.getEmail());
+
+ // update tenant
+ fetchedTenant.setCity("Updated City");
+ fetchedTenant.setCountry("DE");
+ Tenant updatedTenant = client.saveTenant(fetchedTenant);
+ assertEquals("Updated City", updatedTenant.getCity());
+ assertEquals("DE", updatedTenant.getCountry());
+
+ // create a tenant admin for one of the tenants and verify listing
+ Tenant tenantForAdmin = createdTenants.get(0);
+ User adminUser = new User();
+ adminUser.setEmail("tenanttest_admin_" + timestamp + "@test.com");
+ adminUser.setAuthority(Authority.TENANT_ADMIN);
+ adminUser.setTenantId(tenantForAdmin.getId());
+ adminUser.setFirstName("TestAdmin");
+ User savedAdmin = client.saveUser(adminUser, "false");
+ assertNotNull(savedAdmin);
+
+ PageDataUser tenantAdmins = client.getTenantAdmins(
+ tenantForAdmin.getId().getId().toString(), 100, 0, null, null, null);
+ assertEquals(1, tenantAdmins.getData().size());
+ assertEquals(savedAdmin.getEmail(), tenantAdmins.getData().get(0).getEmail());
+
+ // delete tenant
+ UUID tenantToDeleteId = createdTenants.get(0).getId().getId();
+ client.deleteTenant(tenantToDeleteId.toString());
+ createdTenants.remove(0);
+
+ // verify deletion
+ PageDataTenant tenantsAfterDelete = client.getTenants(100, 0, TEST_PREFIX_2, null, null);
+ assertEquals(10, tenantsAfterDelete.getData().size());
+
+ assertReturns404(() ->
+ client.getTenantById(tenantToDeleteId.toString())
+ );
+ } finally {
+ // clean up all created tenants (deleting tenant cascades to users)
+ client.login("sysadmin@thingsboard.org", "sysadmin");
+ for (Tenant tenant : createdTenants) {
+ try {
+ client.deleteTenant(tenant.getId().getId().toString());
+ } catch (ApiException ignored) {
+ }
+ }
+ }
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/TenantProfileJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/TenantProfileJavaClientTest.java
new file mode 100644
index 0000000000..e8c110a004
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/TenantProfileJavaClientTest.java
@@ -0,0 +1,179 @@
+/**
+ * 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.ApiException;
+import org.thingsboard.client.model.DefaultTenantProfileConfiguration;
+import org.thingsboard.client.model.EntityInfo;
+import org.thingsboard.client.model.PageDataEntityInfo;
+import org.thingsboard.client.model.PageDataTenantProfile;
+import org.thingsboard.client.model.TenantProfile;
+import org.thingsboard.client.model.TenantProfileData;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@DaoSqlTest
+public class TenantProfileJavaClientTest extends AbstractJavaClientTest {
+
+ @Test
+ public void testTenantProfileLifecycle() throws Exception {
+ long timestamp = System.currentTimeMillis();
+ List createdProfiles = new ArrayList<>();
+
+ // authenticate as sysadmin for tenant profile management
+ client.login("sysadmin@thingsboard.org", "sysadmin");
+
+ // get initial count (there should be a default profile)
+ PageDataTenantProfile initialProfiles = client.getTenantProfiles(100, 0, null, null, null);
+ assertNotNull(initialProfiles);
+ int initialSize = initialProfiles.getData().size();
+ assertTrue("Expected at least 1 default tenant profile", initialSize >= 1);
+
+ // get default tenant profile info
+ EntityInfo defaultProfileInfo = client.getDefaultTenantProfileInfo();
+ assertNotNull(defaultProfileInfo);
+ assertNotNull(defaultProfileInfo.getName());
+
+ try {
+ // create 5 tenant profiles
+ for (int i = 0; i < 5; i++) {
+ TenantProfile profile = new TenantProfile();
+ profile.setName(TEST_PREFIX + "TenantProfile_" + timestamp + "_" + i);
+ profile.setDescription("Test tenant profile " + i);
+ profile.setIsolatedTbRuleEngine(false);
+
+ TenantProfileData profileData = new TenantProfileData();
+ DefaultTenantProfileConfiguration config = new DefaultTenantProfileConfiguration();
+ config.setMaxDevices(100L);
+ config.setMaxAssets(100L);
+ config.setMaxCustomers(50L);
+ config.setMaxUsers(50L);
+ config.setMaxDashboards(50L);
+ config.setMaxRuleChains(20L);
+ config.setMaxDataPointsPerRollingArg(20L);
+ config.setMaxRelatedEntitiesToReturnPerCfArgument(20);
+ config.setMaxRelationLevelPerCfArgument(20);
+ profileData.setConfiguration(config);
+ profile.setProfileData(profileData);
+ profile.setDefault(false);
+
+ TenantProfile created = client.saveTenantProfile(profile);
+ assertNotNull(created);
+ assertNotNull(created.getId());
+ assertEquals(profile.getName(), created.getName());
+ assertEquals(profile.getDescription(), created.getDescription());
+ assertFalse(created.getDefault());
+
+ createdProfiles.add(created);
+ }
+
+ // find all, check count
+ PageDataTenantProfile allProfiles = client.getTenantProfiles(100, 0, null, null, null);
+ assertNotNull(allProfiles);
+ assertEquals(initialSize + 5, allProfiles.getData().size());
+
+ // find with text search
+ PageDataTenantProfile filteredProfiles = client.getTenantProfiles(100, 0,
+ TEST_PREFIX + "TenantProfile_" + timestamp, null, null);
+ assertEquals(5, filteredProfiles.getData().size());
+
+ // get by id
+ TenantProfile searchProfile = createdProfiles.get(2);
+ TenantProfile fetchedProfile = client.getTenantProfileById(searchProfile.getId().getId().toString());
+ assertEquals(searchProfile.getName(), fetchedProfile.getName());
+ assertEquals(searchProfile.getDescription(), fetchedProfile.getDescription());
+
+ // update tenant profile
+ fetchedProfile.setDescription("Updated description");
+ TenantProfile updatedProfile = client.saveTenantProfile(fetchedProfile);
+ assertEquals("Updated description", updatedProfile.getDescription());
+ assertEquals(fetchedProfile.getName(), updatedProfile.getName());
+
+ // get tenant profile infos (paginated)
+ PageDataEntityInfo profileInfos = client.getTenantProfileInfos(100, 0, null, null, null);
+ assertNotNull(profileInfos);
+ assertEquals(initialSize + 5, profileInfos.getData().size());
+
+ // get profiles by list of ids
+ List idsToFetch = List.of(
+ createdProfiles.get(0).getId().getId().toString(),
+ createdProfiles.get(1).getId().getId().toString()
+ );
+ List profileList = client.getTenantProfileList(idsToFetch);
+ assertEquals(2, profileList.size());
+
+ // set a profile as default
+ TenantProfile profileToSetDefault = createdProfiles.get(1);
+ client.setDefaultTenantProfile(profileToSetDefault.getId().getId().toString());
+ EntityInfo defaultTenantProfileInfo = client.getDefaultTenantProfileInfo();
+ assertEquals(profileToSetDefault.getName(), defaultTenantProfileInfo.getName());
+
+ // verify default profile info now points to the new default
+ EntityInfo newDefaultInfo = client.getDefaultTenantProfileInfo();
+ assertEquals(profileToSetDefault.getName(), newDefaultInfo.getName());
+
+ // restore original default profile
+ TenantProfile originalDefault = initialProfiles.getData().stream()
+ .filter(TenantProfile::getDefault)
+ .findFirst()
+ .orElseThrow();
+ client.setDefaultTenantProfile(originalDefault.getId().getId().toString());
+
+ // delete tenant profile (cannot delete the default one)
+ UUID profileToDeleteId = createdProfiles.get(0).getId().getId();
+ client.deleteTenantProfile(profileToDeleteId.toString());
+ createdProfiles.remove(0);
+
+ // verify deletion
+ assertReturns404(() ->
+ client.getTenantProfileById(profileToDeleteId.toString())
+ );
+
+ PageDataTenantProfile profilesAfterDelete = client.getTenantProfiles(100, 0, null, null, null);
+ assertEquals(initialSize + 4, profilesAfterDelete.getData().size());
+ } finally {
+ // clean up created profiles
+ client.login("sysadmin@thingsboard.org", "sysadmin");
+
+ // ensure original default is restored before deleting test profiles
+ TenantProfile originalDefault = initialProfiles.getData().stream()
+ .filter(TenantProfile::getDefault)
+ .findFirst()
+ .orElseThrow();
+ try {
+ client.setDefaultTenantProfile(originalDefault.getId().getId().toString());
+ } catch (ApiException ignored) {
+ }
+
+ for (TenantProfile profile : createdProfiles) {
+ try {
+ client.deleteTenantProfile(profile.getId().getId().toString());
+ } catch (ApiException ignored) {
+ }
+ }
+ }
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/TwoFactorAuthJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/TwoFactorAuthJavaClientTest.java
new file mode 100644
index 0000000000..972049d44c
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/TwoFactorAuthJavaClientTest.java
@@ -0,0 +1,78 @@
+/**
+ * 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.model.AccountTwoFaSettings;
+import org.thingsboard.client.model.PlatformTwoFaSettings;
+import org.thingsboard.client.model.TotpTwoFaAccountConfig;
+import org.thingsboard.client.model.TotpTwoFaProviderConfig;
+import org.thingsboard.client.model.TwoFaAccountConfig;
+import org.thingsboard.client.model.TwoFaProviderType;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+@DaoSqlTest
+public class TwoFactorAuthJavaClientTest extends AbstractJavaClientTest {
+
+ @Test
+ public void testTwoFactorAuthLifecycle() throws Exception {
+ // save original platform 2FA settings as sysadmin
+ client.login("sysadmin@thingsboard.org", "sysadmin");
+
+ // configure platform 2FA settings with TOTP provider
+ TotpTwoFaProviderConfig totpProviderConfig = new TotpTwoFaProviderConfig();
+ totpProviderConfig.setIssuerName("TestThingsBoard");
+
+ PlatformTwoFaSettings newSettings = new PlatformTwoFaSettings();
+ newSettings.setProviders(List.of(totpProviderConfig));
+ newSettings.setMinVerificationCodeSendPeriod(30);
+ newSettings.setTotalAllowedTimeForVerification(300);
+ newSettings.setMaxVerificationFailuresBeforeUserLockout(5);
+
+ PlatformTwoFaSettings savedSettings = client.savePlatformTwoFaSettings(newSettings);
+ assertNotNull(savedSettings);
+ assertNotNull(savedSettings.getProviders());
+ assertFalse(savedSettings.getProviders().isEmpty());
+ assertEquals(30, savedSettings.getMinVerificationCodeSendPeriod().intValue());
+ assertEquals(300, savedSettings.getTotalAllowedTimeForVerification().intValue());
+
+ // get available 2FA providers (should include TOTP)
+ List providerTypes = client.getAvailableTwoFaProviderTypes();
+ assertNotNull(providerTypes);
+ assertTrue(providerTypes.contains(TwoFaProviderType.TOTP));
+
+ // get account 2FA settings (should be empty initially)
+ AccountTwoFaSettings accountSettings = client.getAccountTwoFaSettings();
+ assertNull(accountSettings);
+
+ // generate TOTP account config
+ TwoFaAccountConfig generatedConfig = client.generateTwoFaAccountConfig(TwoFaProviderType.TOTP.getValue());
+ assertNotNull(generatedConfig);
+ TotpTwoFaAccountConfig totpConfig = (TotpTwoFaAccountConfig) generatedConfig;
+ assertNotNull(totpConfig);
+ assertNotNull(totpConfig.getAuthUrl());
+ assertTrue(totpConfig.getAuthUrl().startsWith("otpauth://totp/"));
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/UserJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/UserJavaClientTest.java
new file mode 100644
index 0000000000..378f48846f
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/UserJavaClientTest.java
@@ -0,0 +1,135 @@
+/**
+ * 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.model.Authority;
+import org.thingsboard.client.model.Customer;
+import org.thingsboard.client.model.JwtPair;
+import org.thingsboard.client.model.PageDataUser;
+import org.thingsboard.client.model.User;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@DaoSqlTest
+public class UserJavaClientTest extends AbstractJavaClientTest {
+
+ @Test
+ public void testUserLifecycle() throws Exception {
+ long timestamp = System.currentTimeMillis();
+ List createdUsers = new ArrayList<>();
+
+ // create 20 tenant admin users
+ for (int i = 0; i < 20; i++) {
+ User user = new User();
+ String email = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i + "@test.com";
+ user.setEmail(email);
+ user.setAuthority(Authority.TENANT_ADMIN);
+ user.setTenantId(savedClientTenant.getId());
+ user.setFirstName("First" + i);
+ user.setLastName("Last" + i);
+
+ User createdUser = client.saveUser(user, "false");
+ assertNotNull(createdUser);
+ assertNotNull(createdUser.getId());
+ assertEquals(email, createdUser.getEmail());
+ assertEquals(Authority.TENANT_ADMIN, createdUser.getAuthority());
+
+ createdUsers.add(createdUser);
+ }
+
+ // find all tenant admins, check count (20 created + 1 from setup)
+ PageDataUser allUsers = client.getUsers(100, 0, null, null, null);
+ assertNotNull(allUsers);
+ assertNotNull(allUsers.getData());
+ int initialSize = allUsers.getData().size();
+ assertEquals("Expected 21 users (20 created + 2 from setup), but got " + initialSize, 22, initialSize);
+
+ // find with search text, check count
+ PageDataUser filteredUsers = client.getUsers(100, 0, TEST_PREFIX_2, null, null);
+ assertEquals("Expected exactly 10 users matching prefix", 10, filteredUsers.getData().size());
+
+ // find by id
+ User searchUser = createdUsers.get(10);
+ User fetchedUser = client.getUserById(searchUser.getId().getId().toString());
+ assertEquals(searchUser.getEmail(), fetchedUser.getEmail());
+ assertEquals(searchUser.getFirstName(), fetchedUser.getFirstName());
+
+ // update user
+ fetchedUser.setFirstName("UpdatedFirst");
+ fetchedUser.setLastName("UpdatedLast");
+ User updatedUser = client.saveUser(fetchedUser, "false");
+ assertEquals("UpdatedFirst", updatedUser.getFirstName());
+ assertEquals("UpdatedLast", updatedUser.getLastName());
+
+ // activate user and get token
+ activateUser(createdUsers.get(0).getId(), "password123", false);
+ JwtPair userToken = client.getUserToken(createdUsers.get(0).getId().getId().toString());
+ assertNotNull(userToken);
+ assertNotNull(userToken.getToken());
+
+ // disable user credentials
+ client.setUserCredentialsEnabled(createdUsers.get(0).getId().getId().toString(), "false");
+
+ // re-enable user credentials
+ client.setUserCredentialsEnabled(createdUsers.get(0).getId().getId().toString(), "true");
+
+ // create customer users and verify listing
+ Customer customer2 = new Customer();
+ customer2.setTitle("User test customer " + timestamp);
+ customer2.setEmail("usertest_" + timestamp + "@test.com");
+ Customer savedCustomer2 = client.saveCustomer(customer2, null, null, null);
+
+ List customerUsers = new ArrayList<>();
+ for (int i = 0; i < 5; i++) {
+ User customerUser = new User();
+ customerUser.setEmail("custuser_" + timestamp + "_" + i + "@test.com");
+ customerUser.setAuthority(Authority.CUSTOMER_USER);
+ customerUser.setTenantId(savedClientTenant.getId());
+ customerUser.setCustomerId(savedCustomer2.getId());
+ customerUser.setFirstName("CustFirst" + i);
+ customerUser.setLastName("CustLast" + i);
+
+ User created = client.saveUser(customerUser, "false");
+ assertNotNull(created);
+ customerUsers.add(created);
+ }
+
+ // list customer users
+ PageDataUser customerUserPage = client.getCustomerUsers(
+ savedCustomer2.getId().getId().toString(), 100, 0, null, null, null);
+ assertEquals("Expected 5 customer users", 5, customerUserPage.getData().size());
+
+ // delete user
+ UUID userToDeleteId = createdUsers.get(0).getId().getId();
+ client.deleteUser(userToDeleteId.toString());
+
+ // verify deletion
+ PageDataUser usersAfterDelete = client.getUsers(100, 0, null, null, null);
+ assertEquals(initialSize + 5 - 1, usersAfterDelete.getData().size());
+
+ assertReturns404(() ->
+ client.getUserById(userToDeleteId.toString())
+ );
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/client/WidgetTypeJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/WidgetTypeJavaClientTest.java
new file mode 100644
index 0000000000..503c8efcf5
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/WidgetTypeJavaClientTest.java
@@ -0,0 +1,155 @@
+/**
+ * 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.client;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.junit.Test;
+import org.thingsboard.client.model.PageDataWidgetTypeInfo;
+import org.thingsboard.client.model.WidgetTypeDetails;
+import org.thingsboard.client.model.WidgetTypeInfo;
+import org.thingsboard.client.model.WidgetsBundle;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@DaoSqlTest
+public class WidgetTypeJavaClientTest extends AbstractJavaClientTest {
+
+ private JsonNode createDescriptor(String type) {
+ return OBJECT_MAPPER.createObjectNode()
+ .put("type", type)
+ .put("sizeX", 7.5)
+ .put("sizeY", 5)
+ .put("resources", "[]")
+ .put("templateHtml", "Test
")
+ .put("templateCss", ".test-widget { font-size: 14px; }")
+ .put("controllerScript", "self.onInit = function() {};")
+ .put("settingsSchema", "{}")
+ .put("dataKeySettingsSchema", "{}");
+ }
+
+ @Test
+ public void testWidgetTypeLifecycle() throws Exception {
+ long timestamp = System.currentTimeMillis();
+ List createdWidgetTypes = new ArrayList<>();
+
+ // create a widgets bundle
+ WidgetsBundle bundle = new WidgetsBundle(null, null, null,
+ TEST_PREFIX + "Bundle_" + timestamp, null, false,
+ "Test bundle description", null, null);
+ WidgetsBundle savedBundle = client.saveWidgetsBundle(bundle);
+ assertNotNull(savedBundle);
+ assertNotNull(savedBundle.getId());
+ assertEquals(bundle.getTitle(), savedBundle.getTitle());
+
+ // create 5 widget types
+ for (int i = 0; i < 5; i++) {
+ String name = TEST_PREFIX + "Widget_" + timestamp + "_" + i;
+ JsonNode descriptor = createDescriptor("latest");
+
+ WidgetTypeDetails widgetType = new WidgetTypeDetails(null, null, null, name, descriptor);
+ widgetType.setDescription("Test widget " + i);
+ widgetType.setDeprecated(false);
+ widgetType.setTags(List.of("test", "automated"));
+
+ WidgetTypeDetails created = client.saveWidgetType(widgetType, false);
+ assertNotNull(created);
+ assertNotNull(created.getId());
+ assertEquals(name, created.getName());
+ assertNotNull(created.getFqn());
+
+ createdWidgetTypes.add(created);
+ }
+
+ // list widget types with text search (tenant only)
+ PageDataWidgetTypeInfo filteredTypes = client.getWidgetTypes(100, 0,
+ TEST_PREFIX + "Widget_" + timestamp, null, null,
+ true, false, null, null, null);
+ assertNotNull(filteredTypes);
+ assertEquals(5, filteredTypes.getData().size());
+
+ // get widget type details by id
+ WidgetTypeDetails searchWidget = createdWidgetTypes.get(2);
+ WidgetTypeDetails fetchedDetails = client.getWidgetTypeById(
+ searchWidget.getId().getId().toString(), true);
+ assertEquals(searchWidget.getName(), fetchedDetails.getName());
+ assertEquals(searchWidget.getFqn(), fetchedDetails.getFqn());
+ assertEquals("Test widget 2", fetchedDetails.getDescription());
+
+ // get widget type info by id
+ WidgetTypeInfo fetchedInfo = client.getWidgetTypeInfoById(
+ searchWidget.getId().getId().toString());
+ assertEquals(searchWidget.getName(), fetchedInfo.getName());
+
+ // add widget types to bundle
+ List widgetTypeIds = createdWidgetTypes.stream()
+ .map(wt -> wt.getId().getId().toString())
+ .collect(Collectors.toList());
+ client.updateWidgetsBundleWidgetTypes(savedBundle.getId().getId().toString(), widgetTypeIds);
+
+ // get bundle widget type fqns
+ List bundleFqns = client.getBundleWidgetTypeFqns(savedBundle.getId().getId().toString());
+ assertEquals(5, bundleFqns.size());
+
+ // get bundle widget types details
+ List bundleDetails = client.getBundleWidgetTypesDetails(
+ savedBundle.getId().getId().toString(), false);
+ assertEquals(5, bundleDetails.size());
+
+ // get bundle widget types infos (paginated)
+ PageDataWidgetTypeInfo bundleInfos = client.getBundleWidgetTypesInfos(
+ savedBundle.getId().getId().toString(), 100, 0,
+ null, null, null, null, null, null);
+ assertEquals(5, bundleInfos.getData().size());
+
+ // update widget type
+ WidgetTypeDetails widgetToUpdate = client.getWidgetTypeById(
+ createdWidgetTypes.get(3).getId().getId().toString(), true);
+ widgetToUpdate.setDescription("Updated description");
+ widgetToUpdate.setDeprecated(true);
+ widgetToUpdate.setTags(List.of("test", "updated"));
+ WidgetTypeDetails updatedWidget = client.saveWidgetType(widgetToUpdate, false);
+ assertEquals("Updated description", updatedWidget.getDescription());
+ assertEquals(true, updatedWidget.getDeprecated());
+
+ // delete widget type
+ String widgetToDeleteId = createdWidgetTypes.get(0).getId().getId().toString();
+ client.deleteWidgetType(widgetToDeleteId);
+
+ // verify deletion
+ assertReturns404(() ->
+ client.getWidgetTypeById(widgetToDeleteId, false)
+ );
+
+ PageDataWidgetTypeInfo typesAfterDelete = client.getWidgetTypes(100, 0,
+ TEST_PREFIX + "Widget_" + timestamp, null, null,
+ true, false, null, null, null);
+ assertEquals(4, typesAfterDelete.getData().size());
+
+ // delete widgets bundle
+ client.deleteWidgetsBundle(savedBundle.getId().getId().toString());
+
+ assertReturns404(() ->
+ client.getWidgetsBundleById(savedBundle.getId().getId().toString(), false)
+ );
+ }
+
+}
diff --git a/pom.xml b/pom.xml
index 4dcc3929fd..6cea12b1bd 100755
--- a/pom.xml
+++ b/pom.xml
@@ -38,6 +38,7 @@
${project.name}
/var/log/${pkg.name}
/usr/share/${pkg.name}
+ 4.4.0-SNAPSHOT
3.4.13
10.1.52
2.18.6
@@ -1134,6 +1135,12 @@
${project.version}
test
+
+ org.thingsboard.client
+ thingsboard-ce-client
+ ${thingsboard.client.version}
+ test
+
org.thingsboard.msa
js-executor