From 26d949d22fc8443ca06bfdff966da80608ddae0b Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Wed, 16 Apr 2025 16:18:21 +0200 Subject: [PATCH 1/2] invalidate async js scripts --- .../api/AbstractScriptInvokeService.java | 4 +- .../api/js/AbstractJsInvokeService.java | 17 +++ .../script/api/js/JsValidator.java | 120 ++++++++++++++++++ .../api/js/AbstractJsInvokeServiceTest.java | 91 +++++++++++++ .../script/api/js/JsValidatorTest.java | 88 +++++++++++++ 5 files changed, 318 insertions(+), 2 deletions(-) create mode 100644 common/script/script-api/src/main/java/org/thingsboard/script/api/js/JsValidator.java create mode 100644 common/script/script-api/src/test/java/org/thingsboard/script/api/js/AbstractJsInvokeServiceTest.java create mode 100644 common/script/script-api/src/test/java/org/thingsboard/script/api/js/JsValidatorTest.java diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java index 3d9b855064..f7b640d3af 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java @@ -269,7 +269,7 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService } } - private boolean scriptBodySizeExceeded(String scriptBody) { + public boolean scriptBodySizeExceeded(String scriptBody) { if (getMaxScriptBodySize() <= 0) return false; return scriptBody.length() > getMaxScriptBodySize(); } @@ -297,7 +297,7 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService return result != null && result.length() > getMaxResultSize(); } - private ListenableFuture error(String message) { + public ListenableFuture error(String message) { return Futures.immediateFailedFuture(new RuntimeException(message)); } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java index b6ddf16e66..d8fae31290 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java @@ -35,6 +35,8 @@ import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import static java.lang.String.format; + /** * Created by ashvayka on 26.09.18. */ @@ -93,6 +95,21 @@ public abstract class AbstractJsInvokeService extends AbstractScriptInvokeServic doRelease(scriptId, scriptInfoMap.remove(scriptId)); } + @Override + public ListenableFuture eval(TenantId tenantId, ScriptType scriptType, String scriptBody, String... argNames) { + if (!isExecEnabled(tenantId)) { + return error("Script Execution is disabled due to API limits!"); + } + if (scriptBodySizeExceeded(scriptBody)) { + return error(format("Script body exceeds maximum allowed size of %s symbols", getMaxScriptBodySize())); + } + final String validationIssue = JsValidator.validate(scriptBody); + if (validationIssue != null ) { + return error(validationIssue); + } + return super.eval(tenantId, scriptType, scriptBody, argNames); + } + protected abstract ListenableFuture doEval(UUID scriptId, JsScriptInfo jsInfo, String scriptBody); protected abstract ListenableFuture doInvokeFunction(UUID scriptId, JsScriptInfo jsInfo, Object[] args); diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/js/JsValidator.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/JsValidator.java new file mode 100644 index 0000000000..9c65064802 --- /dev/null +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/JsValidator.java @@ -0,0 +1,120 @@ +/** + * 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.js; + +import java.util.regex.Pattern; + +public class JsValidator { + + static final Pattern ASYNC_PATTERN = Pattern.compile("\\basync\\b"); + static final Pattern AWAIT_PATTERN = Pattern.compile("\\bawait\\b"); + static final Pattern PROMISE_PATTERN = Pattern.compile("\\bPromise\\b"); + static final Pattern SET_TIMEOUT_PATTERN = Pattern.compile("\\bsetTimeout\\b"); + + public static String validate(String scriptBody) { + if (scriptBody == null || scriptBody.trim().isEmpty()) { + return "Script body is empty"; + } + + //Quick check + if (!ASYNC_PATTERN.matcher(scriptBody).find() + && !AWAIT_PATTERN.matcher(scriptBody).find() + && !PROMISE_PATTERN.matcher(scriptBody).find() + && !SET_TIMEOUT_PATTERN.matcher(scriptBody).find()) { + return null; + } + + //Recheck if quick check failed. Ignoring comments and strings + String[] lines = scriptBody.split("\\r?\\n"); + boolean insideMultilineComment = false; + + for (String line : lines) { + String stripped = line; + + // Handle multiline comments + if (insideMultilineComment) { + if (line.contains("*/")) { + insideMultilineComment = false; + stripped = line.substring(line.indexOf("*/") + 2); // continue after comment + } else { + continue; // skip line inside multiline comment + } + } + + // Check for start of multiline comment + if (stripped.contains("/*")) { + int start = stripped.indexOf("/*"); + int end = stripped.indexOf("*/", start + 2); + + if (end != -1) { + // Inline multiline comment + stripped = stripped.substring(0, start) + stripped.substring(end + 2); + } else { + // Starts a block comment, continues on next lines + insideMultilineComment = true; + stripped = stripped.substring(0, start); + } + } + + stripped = stripInlineComment(stripped); + stripped = stripStringLiterals(stripped); + + if (ASYNC_PATTERN.matcher(stripped).find()) { + return "Script must not contain 'async' keyword."; + } + if (AWAIT_PATTERN.matcher(stripped).find()) { + return "Script must not contain 'await' keyword."; + } + if (PROMISE_PATTERN.matcher(stripped).find()) { + return "Script must not use 'Promise'."; + } + if (SET_TIMEOUT_PATTERN.matcher(stripped).find()) { + return "Script must not use 'setTimeout' method."; + } + } + return null; + } + + private static String stripInlineComment(String line) { + int index = line.indexOf("//"); + return index >= 0 ? line.substring(0, index) : line; + } + + private static String stripStringLiterals(String line) { + StringBuilder sb = new StringBuilder(); + boolean inSingleQuote = false; + boolean inDoubleQuote = false; + + for (int i = 0; i < line.length(); i++) { + char c = line.charAt(i); + + if (c == '"' && !inSingleQuote) { + inDoubleQuote = !inDoubleQuote; + continue; + } else if (c == '\'' && !inDoubleQuote) { + inSingleQuote = !inSingleQuote; + continue; + } + + if (!inSingleQuote && !inDoubleQuote) { + sb.append(c); + } + } + + return sb.toString(); + } + +} diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/js/AbstractJsInvokeServiceTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/js/AbstractJsInvokeServiceTest.java new file mode 100644 index 0000000000..760ca9e3fb --- /dev/null +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/js/AbstractJsInvokeServiceTest.java @@ -0,0 +1,91 @@ +/** + * 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.js; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import org.thingsboard.server.common.stats.StatsCounter; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.script.api.ScriptType; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@Slf4j +class AbstractJsInvokeServiceTest { + + AbstractJsInvokeService service; + final UUID id = UUID.randomUUID(); + + @BeforeEach + void setUp() { + service = mock(AbstractJsInvokeService.class, Mockito.RETURNS_DEEP_STUBS); + + ReflectionTestUtils.setField(service, "requestsCounter", mock(StatsCounter.class)); + ReflectionTestUtils.setField(service, "evalCallback", mock(FutureCallback.class)); + + // Make sure core checks always pass + doReturn(true).when(service).isExecEnabled(any()); + doReturn(false).when(service).scriptBodySizeExceeded(anyString()); + doReturn(Futures.immediateFuture(id)).when(service).doEvalScript(any(), any(), anyString(), any(), any(String[].class)); + + // Use real implementation of eval() + doCallRealMethod().when(service).eval(any(), any(), any(), any(String[].class)); + doCallRealMethod().when(service).error(anyString()); + } + + @Test + void shouldReturnValidationErrorFromJsValidator() throws ExecutionException, InterruptedException { + String scriptWithAsync = "async function test() {}"; + + var future = service.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, scriptWithAsync, "a", "b"); + ExecutionException ex = assertThrows(ExecutionException.class, future::get); + assertTrue(ex.getCause().getMessage().contains("Script must not contain 'async' keyword.")); + assertThat(ex.getCause()).isInstanceOf(RuntimeException.class); + verify(service).isExecEnabled(any()); + verify(service).scriptBodySizeExceeded(any()); + } + + @Test + void shouldPassValidationAndCallSuperEval() throws ExecutionException, InterruptedException, TimeoutException { + String validScript = "function test() { return 42; }"; + var result = service.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, validScript, "x", "y"); + + assertThat(result.get(30, TimeUnit.SECONDS)).isEqualTo(id); + // two times, non-optimal + verify(service, times(2)).isExecEnabled(any()); + verify(service, times(2)).scriptBodySizeExceeded(any()); + } + +} diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/js/JsValidatorTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/js/JsValidatorTest.java new file mode 100644 index 0000000000..c1dbf7a58b --- /dev/null +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/js/JsValidatorTest.java @@ -0,0 +1,88 @@ +/** + * 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.js; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +class JsValidatorTest { + + @ParameterizedTest(name = "should return error for script \"{0}\"") + @ValueSource(strings = { + "async function test() {}", + "const result = await someFunc();", + "const result =\nawait\tsomeFunc();", + "setTimeout(1000);", + "new Promise((resolve) => {});", + "function test() { return 42; } \n\t await test()", + """ + function init() { + await doSomething(); + } + """, + }) + void shouldReturnErrorForInvalidScripts(String script) { + assertNotNull(JsValidator.validate(script)); + } + + @ParameterizedTest(name = "should pass validation for script: \"{0}\"") + @ValueSource(strings = { + "function test() { return 42; }", + "const result = 10 * 2;", + "// async is a keyword but not used: 'const word = \"async\";'", + "let note = 'setTimeout tight';", + + "const word = \"async\";", + "const word = \"setTimeout\";", + "const word = \"Promise\";", + "const word = \"await\";", + + "const word = 'async';", + "const word = 'setTimeout';", + "const word = 'Promise';", + "const word = 'await';", + + "//function test() { return 42; }", + "// const result = 10 * 2;", + "// async is a keyword but not used: 'const word = \"async\";'", + "//setTimeout(1);", + + "a=b+c; // await for a day", + "return new // Promise((resolve) => {", + "hello(); // async is a keyword but not used: 'const word = \"async\";'", + "setGoal(a); //setTimeout(1);", + + " /* new Promise((resolve) => {}); // */ return 'await';", + " /* async */ function calc() {", + "/* async function abc() { \n await new Promise ( \t setTimeout () ) \n } \n*/", + }) + void shouldReturnNullForValidScripts(String script) { + assertNull(JsValidator.validate(script)); + } + + @ParameterizedTest(name = "should return 'Script body is empty' for input: \"{0}\"") + @NullAndEmptySource + @ValueSource(strings = {" ", "\t", "\n"}) + void shouldReturnErrorForEmptyOrNullScripts(String script) { + assertEquals("Script body is empty", JsValidator.validate(script)); + } + +} From 2a50e2eaa53b445488154297ab67bdcc9a672b30 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Tue, 22 Apr 2025 16:38:52 +0200 Subject: [PATCH 2/2] AbstractScriptInvokeService: validate script refactored --- .../api/AbstractScriptInvokeService.java | 26 ++-- .../api/js/AbstractJsInvokeService.java | 16 +-- .../api/AbstractScriptInvokeServiceTest.java | 122 ++++++++++++++++++ .../api/js/AbstractJsInvokeServiceTest.java | 8 +- 4 files changed, 149 insertions(+), 23 deletions(-) create mode 100644 common/script/script-api/src/test/java/org/thingsboard/script/api/AbstractScriptInvokeServiceTest.java diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java index f7b640d3af..0b3890b4bc 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java @@ -134,19 +134,29 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService } } - @Override - public ListenableFuture eval(TenantId tenantId, ScriptType scriptType, String scriptBody, String... argNames) { + public String validate(TenantId tenantId, String scriptBody) { if (isExecEnabled(tenantId)) { if (scriptBodySizeExceeded(scriptBody)) { - return error(format("Script body exceeds maximum allowed size of %s symbols", getMaxScriptBodySize())); + return format("Script body exceeds maximum allowed size of %s symbols", getMaxScriptBodySize()); } - UUID scriptId = UUID.randomUUID(); - requestsCounter.increment(); - return withTimeoutAndStatsCallback(scriptId, null, - doEvalScript(tenantId, scriptType, scriptBody, scriptId, argNames), evalCallback, getMaxEvalRequestsTimeout()); } else { - return error("Script Execution is disabled due to API limits!"); + return "Script Execution is disabled due to API limits!"; } + + return null; + } + + @Override + public ListenableFuture eval(TenantId tenantId, ScriptType scriptType, String scriptBody, String... argNames) { + String validationError = validate(tenantId, scriptBody); + if (validationError != null) { + return error(validationError); + } + + UUID scriptId = UUID.randomUUID(); + requestsCounter.increment(); + return withTimeoutAndStatsCallback(scriptId, null, + doEvalScript(tenantId, scriptType, scriptBody, scriptId, argNames), evalCallback, getMaxEvalRequestsTimeout()); } @Override diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java index d8fae31290..c859f49629 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java @@ -96,18 +96,12 @@ public abstract class AbstractJsInvokeService extends AbstractScriptInvokeServic } @Override - public ListenableFuture eval(TenantId tenantId, ScriptType scriptType, String scriptBody, String... argNames) { - if (!isExecEnabled(tenantId)) { - return error("Script Execution is disabled due to API limits!"); + public String validate(TenantId tenantId, String scriptBody) { + String errorMessage = super.validate(tenantId, scriptBody); + if (errorMessage == null) { + return JsValidator.validate(scriptBody); } - if (scriptBodySizeExceeded(scriptBody)) { - return error(format("Script body exceeds maximum allowed size of %s symbols", getMaxScriptBodySize())); - } - final String validationIssue = JsValidator.validate(scriptBody); - if (validationIssue != null ) { - return error(validationIssue); - } - return super.eval(tenantId, scriptType, scriptBody, argNames); + return errorMessage; } protected abstract ListenableFuture doEval(UUID scriptId, JsScriptInfo jsInfo, String scriptBody); diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/AbstractScriptInvokeServiceTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/AbstractScriptInvokeServiceTest.java new file mode 100644 index 0000000000..35f201ed3b --- /dev/null +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/AbstractScriptInvokeServiceTest.java @@ -0,0 +1,122 @@ +/** + * 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 com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.stats.StatsCounter; + +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +class AbstractScriptInvokeServiceTest { + + AbstractScriptInvokeService service; + final UUID id = UUID.randomUUID(); + final String scriptBody = "return true;"; + final TenantId tenantId = TenantId.fromUUID(UUID.fromString("2ed9a658-45a5-4812-b212-9931f5749f30")); + + @BeforeEach + void setUp() { + service = mock(AbstractScriptInvokeService.class, Mockito.RETURNS_DEEP_STUBS); + + // Make sure core checks always pass + doReturn(true).when(service).isExecEnabled(any()); + doReturn(50000L).when(service).getMaxScriptBodySize(); + + // Use real implementations + doCallRealMethod().when(service).scriptBodySizeExceeded(anyString()); + doCallRealMethod().when(service).eval(any(), any(), any(), any(String[].class)); + doCallRealMethod().when(service).error(anyString()); + doCallRealMethod().when(service).validate(any(), anyString()); + } + + @Test + void evalWithValidationCallTest() throws ExecutionException, InterruptedException, TimeoutException { + ReflectionTestUtils.setField(service, "requestsCounter", mock(StatsCounter.class)); + ReflectionTestUtils.setField(service, "evalCallback", mock(FutureCallback.class)); + + doReturn(Futures.immediateFuture(id)).when(service).doEvalScript(any(), any(), anyString(), any(), any(String[].class)); + + var future = service.eval(tenantId, ScriptType.RULE_NODE_SCRIPT, scriptBody, "x", "y"); + + assertThat(future.get(30, TimeUnit.SECONDS)).isEqualTo(id); + verify(service).validate(any(), anyString()); + verify(service).validate(tenantId, scriptBody); + verify(service, never()).error(anyString()); + } + + @Test + void evalWithValidationCallErrorTest() throws ExecutionException, InterruptedException, TimeoutException { + doReturn(false).when(service).isExecEnabled(any()); + var future = service.eval(tenantId, ScriptType.RULE_NODE_SCRIPT, scriptBody, "x", "y"); + + ExecutionException ex = assertThrows(ExecutionException.class, future::get); + assertThat(ex.getCause().getMessage()).isEqualTo("Script Execution is disabled due to API limits!"); + assertThat(ex.getCause()).isInstanceOf(RuntimeException.class); + + verify(service).validate(any(), anyString()); + verify(service).validate(tenantId, scriptBody); + verify(service).error(anyString()); + } + + @Test + void validateScriptBodyTestExecEnabledTest() { + assertNull(service.validate(tenantId, scriptBody)); + verify(service).isExecEnabled(tenantId); + } + + @Test + void validateScriptBodyTestExecDisabledTest() { + doReturn(false).when(service).isExecEnabled(tenantId); + assertThat(service.validate(tenantId, scriptBody)).isEqualTo("Script Execution is disabled due to API limits!"); + verify(service).isExecEnabled(tenantId); + } + + @Test + void validateScriptBodySizeOKTest() { + assertNull(service.validate(tenantId, scriptBody)); + verify(service).isExecEnabled(tenantId); + verify(service).scriptBodySizeExceeded(scriptBody); + } + + @Test + void validateScriptBodySizeExceededTest() { + doReturn(10L).when(service).getMaxScriptBodySize(); + assertThat(service.validate(tenantId, scriptBody)).isEqualTo("Script body exceeds maximum allowed size of 10 symbols"); + verify(service).isExecEnabled(tenantId); + verify(service).scriptBodySizeExceeded(scriptBody); + } + +} diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/js/AbstractJsInvokeServiceTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/js/AbstractJsInvokeServiceTest.java index 760ca9e3fb..3f7d61919d 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/js/AbstractJsInvokeServiceTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/js/AbstractJsInvokeServiceTest.java @@ -60,9 +60,10 @@ class AbstractJsInvokeServiceTest { doReturn(false).when(service).scriptBodySizeExceeded(anyString()); doReturn(Futures.immediateFuture(id)).when(service).doEvalScript(any(), any(), anyString(), any(), any(String[].class)); - // Use real implementation of eval() + // Use real implementations doCallRealMethod().when(service).eval(any(), any(), any(), any(String[].class)); doCallRealMethod().when(service).error(anyString()); + doCallRealMethod().when(service).validate(any(), anyString()); } @Test @@ -83,9 +84,8 @@ class AbstractJsInvokeServiceTest { var result = service.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, validScript, "x", "y"); assertThat(result.get(30, TimeUnit.SECONDS)).isEqualTo(id); - // two times, non-optimal - verify(service, times(2)).isExecEnabled(any()); - verify(service, times(2)).scriptBodySizeExceeded(any()); + verify(service, times(1)).isExecEnabled(any()); + verify(service, times(1)).scriptBodySizeExceeded(any()); } }