2 changed files with 294 additions and 69 deletions
@ -0,0 +1,268 @@ |
|||
/** |
|||
* 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.service.entitiy.ai; |
|||
|
|||
import org.junit.jupiter.api.BeforeEach; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.api.extension.ExtendWith; |
|||
import org.mockito.InjectMocks; |
|||
import org.mockito.Mock; |
|||
import org.mockito.Spy; |
|||
import org.mockito.junit.jupiter.MockitoExtension; |
|||
import org.springframework.test.util.ReflectionTestUtils; |
|||
import org.thingsboard.server.common.data.User; |
|||
import org.thingsboard.server.common.data.ai.AiModel; |
|||
import org.thingsboard.server.common.data.ai.model.AiModelConfig; |
|||
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.AiModelId; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.dao.ai.AiModelService; |
|||
import org.thingsboard.server.service.entitiy.TbLogEntityActionService; |
|||
import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService; |
|||
|
|||
import java.util.UUID; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
import static org.assertj.core.api.Assertions.assertThatThrownBy; |
|||
import static org.mockito.ArgumentMatchers.any; |
|||
import static org.mockito.BDDMockito.given; |
|||
import static org.mockito.BDDMockito.then; |
|||
import static org.mockito.Mockito.never; |
|||
|
|||
@ExtendWith(MockitoExtension.class) |
|||
class DefaultTbAiModelServiceTest { |
|||
|
|||
@Mock |
|||
EntitiesVersionControlService vcServiceMock; |
|||
|
|||
@Mock |
|||
AiModelService aiModelServiceMock; |
|||
|
|||
@Mock |
|||
TbLogEntityActionService logEntityActionServiceMock; |
|||
|
|||
@Spy |
|||
@InjectMocks |
|||
DefaultTbAiModelService service; |
|||
|
|||
TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); |
|||
|
|||
User user; |
|||
|
|||
@BeforeEach |
|||
void setUp() { |
|||
user = new User(); |
|||
user.setTenantId(tenantId); |
|||
|
|||
service = new DefaultTbAiModelService(aiModelServiceMock); |
|||
ReflectionTestUtils.setField(service, "vcService", vcServiceMock); |
|||
ReflectionTestUtils.setField(service, "logEntityActionService", logEntityActionServiceMock); |
|||
} |
|||
|
|||
@Test |
|||
void save_whenCreatingNewModel_shouldAutoCommitAndLogAddedActionAndUseTenantIdFromUser() { |
|||
// GIVEN
|
|||
var modelToSave = AiModel.builder() |
|||
.name("Model to save") |
|||
.configuration(constructValidOpenAiModelConfig()) |
|||
.build(); |
|||
|
|||
var savedModel = new AiModel(modelToSave); |
|||
savedModel.setId(new AiModelId(UUID.randomUUID())); |
|||
savedModel.setTenantId(user.getTenantId()); |
|||
savedModel.setVersion(1L); |
|||
savedModel.setCreatedTime(System.currentTimeMillis()); |
|||
|
|||
given(aiModelServiceMock.save(modelToSave)).willReturn(savedModel); |
|||
|
|||
// WHEN
|
|||
AiModel result = service.save(modelToSave, user); |
|||
|
|||
// THEN
|
|||
assertThat(result).isEqualTo(savedModel); |
|||
|
|||
then(aiModelServiceMock).should().save(modelToSave); |
|||
then(vcServiceMock).should().autoCommit(user, savedModel.getId()); |
|||
then(logEntityActionServiceMock).should().logEntityAction(tenantId, savedModel.getId(), savedModel, ActionType.ADDED, user); |
|||
} |
|||
|
|||
@Test |
|||
void save_whenUpdatingExistingModel_shouldAutoCommitAndLogUpdatedAction() { |
|||
// GIVEN
|
|||
var modelToUpdate = AiModel.builder() |
|||
.tenantId(tenantId) |
|||
.version(1L) |
|||
.name("Model to update") |
|||
.configuration(constructValidOpenAiModelConfig()) |
|||
.build(); |
|||
modelToUpdate.setId(new AiModelId(UUID.randomUUID())); |
|||
modelToUpdate.setCreatedTime(System.currentTimeMillis()); |
|||
|
|||
var updatedModel = new AiModel(modelToUpdate); |
|||
updatedModel.setVersion(2L); |
|||
updatedModel.setName("Updated model"); |
|||
|
|||
given(aiModelServiceMock.save(modelToUpdate)).willReturn(updatedModel); |
|||
|
|||
// WHEN
|
|||
AiModel result = service.save(modelToUpdate, user); |
|||
|
|||
// THEN
|
|||
assertThat(result).isEqualTo(updatedModel); |
|||
|
|||
then(aiModelServiceMock).should().save(modelToUpdate); |
|||
then(vcServiceMock).should().autoCommit(user, updatedModel.getId()); |
|||
then(logEntityActionServiceMock).should().logEntityAction(tenantId, updatedModel.getId(), updatedModel, ActionType.UPDATED, user); |
|||
} |
|||
|
|||
@Test |
|||
void save_whenCreatingNewModelThrowsException_shouldUseEmptyIdAndLogError() { |
|||
// GIVEN
|
|||
var modelToSave = AiModel.builder() |
|||
.tenantId(tenantId) |
|||
.name("Model to save") |
|||
.configuration(constructValidOpenAiModelConfig()) |
|||
.build(); |
|||
|
|||
var exception = new RuntimeException("Failed to save"); |
|||
|
|||
given(aiModelServiceMock.save(modelToSave)).willThrow(exception); |
|||
|
|||
// WHEN-THEN
|
|||
assertThatThrownBy(() -> service.save(modelToSave, user)) |
|||
.isInstanceOf(RuntimeException.class) |
|||
.hasMessageContaining("Failed to save"); |
|||
|
|||
then(aiModelServiceMock).should().save(modelToSave); |
|||
then(vcServiceMock).should(never()).autoCommit(any(), any()); |
|||
then(logEntityActionServiceMock).should().logEntityAction(tenantId, new AiModelId(EntityId.NULL_UUID), modelToSave, ActionType.ADDED, user, exception); |
|||
} |
|||
|
|||
@Test |
|||
void save_whenUpdatingExistingModelThrowsException_shouldUseExistingModelIdAndLogError() { |
|||
// GIVEN
|
|||
var modelToUpdate = AiModel.builder() |
|||
.tenantId(tenantId) |
|||
.version(1L) |
|||
.name("Model to update") |
|||
.configuration(constructValidOpenAiModelConfig()) |
|||
.build(); |
|||
modelToUpdate.setId(new AiModelId(UUID.randomUUID())); |
|||
modelToUpdate.setCreatedTime(System.currentTimeMillis()); |
|||
|
|||
var exception = new RuntimeException("Failed to save"); |
|||
|
|||
given(aiModelServiceMock.save(modelToUpdate)).willThrow(exception); |
|||
|
|||
// WHEN-THEN
|
|||
assertThatThrownBy(() -> service.save(modelToUpdate, user)) |
|||
.isInstanceOf(RuntimeException.class) |
|||
.hasMessageContaining("Failed to save"); |
|||
|
|||
then(aiModelServiceMock).should().save(modelToUpdate); |
|||
then(vcServiceMock).should(never()).autoCommit(any(), any()); |
|||
then(logEntityActionServiceMock).should().logEntityAction(tenantId, modelToUpdate.getId(), modelToUpdate, ActionType.UPDATED, user, exception); |
|||
} |
|||
|
|||
@Test |
|||
void delete_whenDeleteSuccessful_shouldLogDeletedAction() { |
|||
// GIVEN
|
|||
var modelToDelete = AiModel.builder() |
|||
.tenantId(tenantId) |
|||
.version(1L) |
|||
.name("Model to delete") |
|||
.configuration(constructValidOpenAiModelConfig()) |
|||
.build(); |
|||
modelToDelete.setId(new AiModelId(UUID.randomUUID())); |
|||
modelToDelete.setCreatedTime(System.currentTimeMillis()); |
|||
|
|||
given(aiModelServiceMock.deleteByTenantIdAndId(tenantId, modelToDelete.getId())).willReturn(true); |
|||
|
|||
// WHEN
|
|||
boolean result = service.delete(modelToDelete, user); |
|||
|
|||
// THEN
|
|||
assertThat(result).isTrue(); |
|||
then(aiModelServiceMock).should().deleteByTenantIdAndId(tenantId, modelToDelete.getId()); |
|||
then(logEntityActionServiceMock).should().logEntityAction(tenantId, modelToDelete.getId(), modelToDelete, ActionType.DELETED, user, modelToDelete.getId().toString()); |
|||
} |
|||
|
|||
@Test |
|||
void delete_whenDeleteReturnsFalse_shouldNotLogAction() { |
|||
// GIVEN
|
|||
var modelToDelete = AiModel.builder() |
|||
.tenantId(tenantId) |
|||
.version(1L) |
|||
.name("Model to delete") |
|||
.configuration(constructValidOpenAiModelConfig()) |
|||
.build(); |
|||
modelToDelete.setId(new AiModelId(UUID.randomUUID())); |
|||
modelToDelete.setCreatedTime(System.currentTimeMillis()); |
|||
|
|||
given(aiModelServiceMock.deleteByTenantIdAndId(tenantId, modelToDelete.getId())).willReturn(false); |
|||
|
|||
// WHEN
|
|||
boolean result = service.delete(modelToDelete, user); |
|||
|
|||
// THEN
|
|||
assertThat(result).isFalse(); |
|||
then(aiModelServiceMock).should().deleteByTenantIdAndId(tenantId, modelToDelete.getId()); |
|||
then(logEntityActionServiceMock).should(never()).logEntityAction(tenantId, modelToDelete.getId(), modelToDelete, ActionType.DELETED, user, modelToDelete.getId().toString()); |
|||
} |
|||
|
|||
@Test |
|||
void delete_whenDeleteThrowsException_shouldLogError() { |
|||
// GIVEN
|
|||
var modelToDelete = AiModel.builder() |
|||
.tenantId(tenantId) |
|||
.version(1L) |
|||
.name("Model to delete") |
|||
.configuration(constructValidOpenAiModelConfig()) |
|||
.build(); |
|||
modelToDelete.setId(new AiModelId(UUID.randomUUID())); |
|||
modelToDelete.setCreatedTime(System.currentTimeMillis()); |
|||
|
|||
var exception = new RuntimeException("Failed to delete"); |
|||
|
|||
given(aiModelServiceMock.deleteByTenantIdAndId(tenantId, modelToDelete.getId())).willThrow(exception); |
|||
|
|||
// WHEN-THEN
|
|||
assertThatThrownBy(() -> service.delete(modelToDelete, user)) |
|||
.isInstanceOf(RuntimeException.class) |
|||
.hasMessageContaining("Failed to delete"); |
|||
|
|||
then(aiModelServiceMock).should().deleteByTenantIdAndId(tenantId, modelToDelete.getId()); |
|||
then(logEntityActionServiceMock).should().logEntityAction(tenantId, modelToDelete.getId(), modelToDelete, ActionType.DELETED, user, exception, modelToDelete.getId().toString()); |
|||
} |
|||
|
|||
private static AiModelConfig constructValidOpenAiModelConfig() { |
|||
return 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(); |
|||
} |
|||
|
|||
} |
|||
Loading…
Reference in new issue