AI rule node: add basic save API tests

This commit is contained in:
Dmytro Skarzhynets 2025-07-10 12:26:10 +03:00
parent 82d2c1d93a
commit 17d7931e5d
No known key found for this signature in database
GPG Key ID: 2B51652F224037DF
2 changed files with 194 additions and 0 deletions

View File

@ -0,0 +1,192 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller;
import org.junit.Test;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.web.servlet.ResultActions;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig;
import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.service.entitiy.TbLogEntityActionService;
import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import java.util.Objects;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.times;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
public class AiModelControllerTest extends AbstractControllerTest {
@SpyBean
private EntitiesVersionControlService versionControlService;
@SpyBean
private TbLogEntityActionService logEntityActionService;
/* --- Save API tests --- */
@Test
public void saveAiModel_whenUserIsSysAdmin_shouldReturnForbidden() throws Exception {
// GIVEN
loginSysAdmin();
AiModel model = constructValidModel();
// WHEN
ResultActions result = doPost("/api/ai/model", model);
// THEN
result.andExpect(status().isForbidden()).andExpect(statusReason(equalTo(msgErrorPermission)));
}
@Test
public void saveAiModel_whenUserIsCustomerUser_shouldReturnForbidden() throws Exception {
// GIVEN
loginCustomerUser();
AiModel model = constructValidModel();
// WHEN
ResultActions result = doPost("/api/ai/model", model);
// THEN
result.andExpect(status().isForbidden()).andExpect(statusReason(equalTo(msgErrorPermission)));
}
@Test
public void saveAiModel_whenCreatingValidModelAsTenantAdmin_shouldSucceed() throws Exception {
// GIVEN
loginTenantAdmin();
AiModel model = constructValidModel();
// WHEN
var savedModel = doPost("/api/ai/model", model, AiModel.class);
// THEN
// verify returned object
assertThat(savedModel.getId()).isNotNull();
assertThat(savedModel.getUuidId()).isNotNull().isNotEqualTo(EntityId.NULL_UUID);
assertThat(savedModel.getId().getEntityType()).isEqualTo(EntityType.AI_MODEL);
assertThat(savedModel.getCreatedTime()).isPositive();
assertThat(savedModel.getVersion()).isEqualTo(1);
assertThat(savedModel.getTenantId()).isEqualTo(tenantId);
assertThat(savedModel.getName()).isEqualTo("Test model");
assertThat(savedModel.getConfiguration()).isEqualTo(model.getConfiguration());
assertThat(savedModel.getExternalId()).isNull();
// verify auto-commit
then(versionControlService).should().autoCommit(
argThat(actualUser -> Objects.equals(actualUser.getId(), tenantAdminUser.getId())), eq(savedModel.getId())
);
// verify a rule engine message was sent, and an audit log was created
then(logEntityActionService).should().logEntityAction(
eq(tenantId),
eq(savedModel.getId()),
eq(savedModel),
eq(ActionType.ADDED),
argThat(actualUser -> Objects.equals(actualUser.getId(), tenantAdminUser.getId()))
);
}
@Test
public void saveAiModel_whenUpdatingExistingModelAsTenantAdmin_shouldSucceedAndLogAction() throws Exception {
// GIVEN
loginTenantAdmin();
var model = doPost("/api/ai/model", constructValidModel(), AiModel.class);
var newModelConfig = OpenAiChatModelConfig.builder()
.providerConfig(new OpenAiProviderConfig("test-api-key-updated"))
.modelId("o4-mini")
.temperature(0.2)
.topP(0.4)
.frequencyPenalty(0.2)
.presencePenalty(0.5)
.maxOutputTokens(2000)
.timeoutSeconds(20)
.maxRetries(0)
.build();
model.setName("Test model updated");
model.setConfiguration(newModelConfig);
// WHEN
var updatedModel = doPost("/api/ai/model", model, AiModel.class);
// THEN
// verify returned object
assertThat(updatedModel.getId()).isEqualTo(model.getId());
assertThat(updatedModel.getCreatedTime()).isEqualTo(model.getCreatedTime());
assertThat(updatedModel.getVersion()).isEqualTo(2);
assertThat(updatedModel.getTenantId()).isEqualTo(tenantId);
assertThat(updatedModel.getName()).isEqualTo("Test model updated");
assertThat(updatedModel.getConfiguration()).isEqualTo(newModelConfig);
assertThat(updatedModel.getExternalId()).isNull();
// verify auto-commit
then(versionControlService).should(times(2)).autoCommit(
argThat(actualUser -> Objects.equals(actualUser.getId(), tenantAdminUser.getId())), eq(updatedModel.getId())
);
// verify a rule engine message was sent, and an audit log was created
then(logEntityActionService).should().logEntityAction(
eq(tenantId), eq(updatedModel.getId()), eq(updatedModel), eq(ActionType.UPDATED),
argThat(actualUser -> Objects.equals(actualUser.getId(), tenantAdminUser.getId()))
);
}
private AiModel constructValidModel() {
var modelConfig = OpenAiChatModelConfig.builder()
.providerConfig(new OpenAiProviderConfig("test-api-key"))
.modelId("gpt-4o")
.temperature(0.5)
.topP(0.3)
.frequencyPenalty(0.1)
.presencePenalty(0.2)
.maxOutputTokens(1000)
.timeoutSeconds(60)
.maxRetries(2)
.build();
return AiModel.builder()
.tenantId(tenantId)
.name("Test model")
.configuration(modelConfig)
.build();
}
}

View File

@ -22,10 +22,12 @@ import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.PositiveOrZero; import jakarta.validation.constraints.PositiveOrZero;
import lombok.Builder;
import lombok.With; import lombok.With;
import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.AiProvider;
import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
@Builder
public record OpenAiChatModelConfig( public record OpenAiChatModelConfig(
@NotNull @Valid OpenAiProviderConfig providerConfig, @NotNull @Valid OpenAiProviderConfig providerConfig,
@NotBlank String modelId, @NotBlank String modelId,