diff --git a/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java b/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java index 0942cef75d..b8ab48b38d 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java @@ -25,11 +25,13 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.TbStopWatch; import org.thingsboard.script.api.ScriptType; +import org.thingsboard.script.api.TbScriptException; import org.thingsboard.script.api.js.NashornJsInvokeService; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.service.DaoSqlTest; +import javax.script.ScriptException; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -39,6 +41,7 @@ import java.util.concurrent.TimeoutException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.InstanceOfAssertFactories.type; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; @@ -59,6 +62,25 @@ class NashornJsInvokeServiceTest extends AbstractControllerTest { @Value("${js.local.max_errors}") private int maxJsErrors; + @Test + void givenUncompilableScript_whenEvaluating_thenThrowsErrorWithCompilationErrorCode() { + // GIVEN + var uncompilableScript = "return msg.temperature?.value;"; + + // WHEN-THEN + assertThatThrownBy(() -> evalScript(uncompilableScript)) + .isInstanceOf(ExecutionException.class) + .cause() + .isInstanceOf(TbScriptException.class) + .asInstanceOf(type(TbScriptException.class)) + .satisfies(ex -> { + assertThat(ex.getScriptId()).isNotNull(); + assertThat(ex.getErrorCode()).isEqualTo(TbScriptException.ErrorCode.COMPILATION); + assertThat(ex.getBody()).contains(uncompilableScript); + assertThat(ex.getCause()).isInstanceOf(ScriptException.class); + }); + } + @Test void givenSimpleScriptTestPerformance() throws ExecutionException, InterruptedException { int iterations = 1000; diff --git a/application/src/test/java/org/thingsboard/server/service/script/RemoteJsInvokeServiceTest.java b/application/src/test/java/org/thingsboard/server/service/script/RemoteJsInvokeServiceTest.java index 363f21fa10..36990d9768 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/RemoteJsInvokeServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/RemoteJsInvokeServiceTest.java @@ -23,9 +23,9 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.script.api.ScriptType; +import org.thingsboard.script.api.TbScriptException; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.stats.DefaultStatsFactory; import org.thingsboard.server.common.stats.StatsCounter; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.TbApiUsageReportClient; @@ -42,8 +42,11 @@ import java.util.List; import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.concurrent.ExecutionException; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.InstanceOfAssertFactories.type; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.doAnswer; @@ -60,7 +63,6 @@ class RemoteJsInvokeServiceTest { private RemoteJsInvokeService remoteJsInvokeService; private TbQueueRequestTemplate, TbProtoQueueMsg> jsRequestTemplate; - @BeforeEach public void beforeEach() { TbApiUsageStateClient apiUsageStateClient = mock(TbApiUsageStateClient.class); @@ -74,7 +76,7 @@ class RemoteJsInvokeServiceTest { remoteJsInvokeService.requestTemplate = jsRequestTemplate; StatsFactory statsFactory = mock(StatsFactory.class); when(statsFactory.createStatsCounter(any(), any())).thenReturn(mock(StatsCounter.class)); - ReflectionTestUtils.setField(remoteJsInvokeService, "statsFactory",statsFactory); + ReflectionTestUtils.setField(remoteJsInvokeService, "statsFactory", statsFactory); remoteJsInvokeService.init(); } @@ -84,7 +86,36 @@ class RemoteJsInvokeServiceTest { } @Test - public void whenInvokingFunction_thenDoNotSendScriptBody() throws Exception { + void givenUncompilableScript_whenEvaluating_thenThrowsErrorWithCompilationErrorCode() { + // GIVEN + doAnswer(methodCall -> Futures.immediateFuture(new TbProtoJsQueueMsg<>(UUID.randomUUID(), RemoteJsResponse.newBuilder() + .setCompileResponse(JsInvokeProtos.JsCompileResponse.newBuilder() + .setSuccess(false) + .setErrorCode(JsInvokeProtos.JsInvokeErrorCode.COMPILATION_ERROR) + .setErrorDetails("SyntaxError: Unexpected token 'const'") + .setScriptHash(methodCall.>getArgument(0).getValue().getCompileRequest().getScriptHash()) + .build()) + .build()))) + .when(jsRequestTemplate).send(argThat(jsQueueMsg -> jsQueueMsg.getValue().hasCompileRequest())); + + var uncompilableScript = "let const = 'this is not allowed';"; + + // WHEN-THEN + assertThatThrownBy(() -> remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, uncompilableScript).get()) + .isInstanceOf(ExecutionException.class) + .cause() + .isInstanceOf(TbScriptException.class) + .asInstanceOf(type(TbScriptException.class)) + .satisfies(ex -> { + assertThat(ex.getScriptId()).isNotNull(); + assertThat(ex.getErrorCode()).isEqualTo(TbScriptException.ErrorCode.COMPILATION); + assertThat(ex.getBody()).contains(uncompilableScript); + assertThat(ex.getCause()).isInstanceOf(RuntimeException.class).hasMessage("SyntaxError: Unexpected token 'const'"); + }); + } + + @Test + void whenInvokingFunction_thenDoNotSendScriptBody() throws Exception { mockJsEvalResponse(); String scriptBody = "return { a: 'b'};"; UUID scriptId = remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, scriptBody).get(); @@ -110,7 +141,7 @@ class RemoteJsInvokeServiceTest { } @Test - public void whenInvokingFunctionAndRemoteJsExecutorRemovedScript_thenHandleNotFoundErrorAndMakeInvokeRequestWithScriptBody() throws Exception { + void whenInvokingFunctionAndRemoteJsExecutorRemovedScript_thenHandleNotFoundErrorAndMakeInvokeRequestWithScriptBody() throws Exception { mockJsEvalResponse(); String scriptBody = "return { a: 'b'};"; UUID scriptId = remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, scriptBody).get(); @@ -156,7 +187,7 @@ class RemoteJsInvokeServiceTest { } @Test - public void whenDoingEval_thenSaveScriptByHashOfTenantIdAndScriptBody() throws Exception { + void whenDoingEval_thenSaveScriptByHashOfTenantIdAndScriptBody() throws Exception { mockJsEvalResponse(); TenantId tenantId1 = TenantId.fromUUID(UUID.randomUUID()); @@ -187,7 +218,7 @@ class RemoteJsInvokeServiceTest { } @Test - public void whenReleasingScript_thenCheckForHashUsages() throws Exception { + void whenReleasingScript_thenCheckForHashUsages() throws Exception { mockJsEvalResponse(); String scriptBody = "return { a: 'b'};"; UUID scriptId1 = remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, scriptBody).get(); diff --git a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java index 732f31f044..fc66f806d7 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java @@ -20,10 +20,12 @@ import com.github.benmanes.caffeine.cache.Cache; import org.junit.Assert; import org.junit.Ignore; import org.junit.jupiter.api.Test; +import org.mvel2.CompileException; import org.springframework.beans.factory.annotation.Value; import org.springframework.test.context.TestPropertySource; import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.script.api.TbScriptException; import org.thingsboard.script.api.tbel.TbelScript; import java.io.Serializable; @@ -37,6 +39,7 @@ import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.InstanceOfAssertFactories.type; @TestPropertySource(properties = { "tbel.max_script_body_size=100", @@ -50,6 +53,25 @@ class TbelInvokeServiceTest extends AbstractTbelInvokeTest { @Value("${tbel.max_errors}") private int maxJsErrors; + @Test + void givenUncompilableScript_whenEvaluating_thenThrowsErrorWithCompilationErrorCode() { + // GIVEN + var uncompilableScript = "return msg.property !== undefined;"; + + // WHEN-THEN + assertThatThrownBy(() -> evalScript(uncompilableScript)) + .isInstanceOf(ExecutionException.class) + .cause() + .isInstanceOf(TbScriptException.class) + .asInstanceOf(type(TbScriptException.class)) + .satisfies(ex -> { + assertThat(ex.getScriptId()).isNotNull(); + assertThat(ex.getErrorCode()).isEqualTo(TbScriptException.ErrorCode.COMPILATION); + assertThat(ex.getBody()).isEqualTo(uncompilableScript); + assertThat(ex.getCause()).isInstanceOf(CompileException.class); + }); + } + @Test void givenSimpleScriptTestPerformance() throws ExecutionException, InterruptedException { int iterations = 100000; diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/TbScriptExceptionTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/TbScriptExceptionTest.java new file mode 100644 index 0000000000..330b895504 --- /dev/null +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/TbScriptExceptionTest.java @@ -0,0 +1,49 @@ +/** + * 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.script.api; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class TbScriptExceptionTest { + + @Test + void givenCompilationError_whenCheckingIsUnrecoverable_thenReturnsTrue() { + // GIVEN + var exception = new TbScriptException(null, TbScriptException.ErrorCode.COMPILATION, null, null); + + // WHEN-THEN + assertThat(exception.isUnrecoverable()).isTrue(); + } + + @ParameterizedTest + @EnumSource( + value = TbScriptException.ErrorCode.class, + mode = EnumSource.Mode.EXCLUDE, + names = "COMPILATION" + ) + void givenRecoverableErrorCodes_whenCheckingIsUnrecoverable_thenReturnsFalse(TbScriptException.ErrorCode errorCode) { + // GIVEN + var exception = new TbScriptException(null, errorCode, null, null); + + // WHEN-THEN + assertThat(exception.isUnrecoverable()).isFalse(); + } + +}