This is an automated email from the ASF dual-hosted git repository.
ashishvijaywargiya pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git
The following commit(s) were added to refs/heads/trunk by this push:
new cb6be2b75b Implemented: per-component override of test.api.enabled
REST test-run API (#1698)
cb6be2b75b is described below
commit cb6be2b75bde55569c45bf50863552a297ca2af3
Author: Ashish Vijaywargiya <[email protected]>
AuthorDate: Sat Aug 22 00:39:32 2026 +0530
Implemented: per-component override of test.api.enabled REST test-run API
(#1698)
Implemented: per-component override of test.api.enabled REST test-run API
Explanation
test.api.enabled (framework/testtools/config/testtools.properties,
checked in TestRunServices.runTestSuite) is a single global on/off switch
for the
REST-triggered test execution API. There was no way to disable just one
component's
test-run REST endpoint while leaving the API on for every other component -
the only
workarounds were removing the TestRunResource block from a component's
*.rest.xml (code
change + redeploy) or revoking the TESTEXEC/TESTEXEC_ADMIN permission (not
component-specific, affects any component sharing the same permission).
This adds a new property, test.api.enabled.<componentName>, read through
the same EntityUtilProperties/SystemProperty-backed mechanism the global
flag
already uses - so it can be flipped live via a SystemProperty row, without
a restart, the
same way the global flag already can. Defaults to enabled ("true") when
unset, so there is
no behavior change for any component that doesn't use it. Only consulted
once the global
flag is already true, and only gates triggering new runs (runTestSuite) -
not status
polling (getTestRunStatus/getScopedTestRunStatus).
Because runScopedTestSuite (used by every component's REST wrapper, e.g.
plugins/example's runExampleTestSuite) only forces componentName and
delegates to
runTestSuite, this single check covers every component's REST endpoint
automatically, with zero
changes needed to any wrapper script or *.rest.xml.
No new entity, admin screen, or service - an admin sets/clears the
override through the existing generic webtools SystemProperty screen (or an
entity XML data
import), the same way the global flag is already managed.
Tested manually against a running server: default-enabled behavior with
no override, disabling one component live via a SystemProperty data import,
confirming the global flag and every other component stay unaffected, and
re-enabling live - all
without a restart.
A companion PR against ofbiz-plugins updates plugins/example's
runExampleTestSuite service description to mention this gate (no behavior
change - it already
inherited the gate automatically).
---
framework/testtools/config/testtools.properties | 12 ++
framework/testtools/servicedef/services.xml | 4 +-
.../apache/ofbiz/testtools/TestRunServices.java | 17 +++
.../ofbiz/testtools/TestRunServicesTest.java | 130 +++++++++++++++++++--
4 files changed, 154 insertions(+), 9 deletions(-)
diff --git a/framework/testtools/config/testtools.properties
b/framework/testtools/config/testtools.properties
index e2791ba466..e8cb785b70 100644
--- a/framework/testtools/config/testtools.properties
+++ b/framework/testtools/config/testtools.properties
@@ -57,3 +57,15 @@ test.history.days=7
# isolated sandbox - see TestRunServices' class javadoc before enabling
this in any
# shared/long-lived environment.
test.api.enabled=false
+
+# -- Per-component override of test.api.enabled above:
test.api.enabled.<componentName> (e.g.
+# test.api.enabled.example) disables - or re-enables - just that
component's test-run REST
+# endpoint, without touching any other component's access. Only consulted
once the global flag
+# above is already true; defaults to enabled ("true") when unset, so a
component that never sets
+# this is unaffected. Like the global flag, read via EntityUtilProperties
(delegator-aware) - the
+# normal way to use this is a live SystemProperty row
(systemResourceId=testtools,
+# systemPropertyId=test.api.enabled.<componentName>,
systemPropertyValue=false to disable, true
+# to re-enable - same true/false convention as this file's own values;
deleting the row also
+# re-enables), not a properties-file entry here, though one can be added
below for a fixed
+# per-environment default if that's ever needed. Only the literal value
true enables - any other
+# value disables.
diff --git a/framework/testtools/servicedef/services.xml
b/framework/testtools/servicedef/services.xml
index b21535ad7f..626ff5cce4 100644
--- a/framework/testtools/servicedef/services.xml
+++ b/framework/testtools/servicedef/services.xml
@@ -53,7 +53,9 @@ under the License.
location="org.apache.ofbiz.testtools.TestRunServices"
invoke="runTestSuite">
<description>Kicks off a testdef test-suite run asynchronously via the
in-JVM Jupiter test
engine and returns a runId immediately; poll getTestRunStatus for
progress/results.
- Gated by the test.api.enabled config flag and the TESTEXEC_ADMIN
permission. Not
+ Gated by the test.api.enabled config flag and the TESTEXEC_ADMIN
permission. When
+ componentName is set, the per-component
test.api.enabled.<componentName>
+ override (SystemProperty-overridable, defaults to enabled) is also
consulted. Not
intended for direct *.rest.xml exposure - a component-branded REST
endpoint must wrap
this with a component-scoped service (see
TestRunServices.runScopedTestSuite and
plugins/example's ExampleTestRunServices for the pattern), or the
endpoint can trigger
diff --git
a/framework/testtools/src/main/java/org/apache/ofbiz/testtools/TestRunServices.java
b/framework/testtools/src/main/java/org/apache/ofbiz/testtools/TestRunServices.java
index a3f6ffc8af..15ea1334fc 100644
---
a/framework/testtools/src/main/java/org/apache/ofbiz/testtools/TestRunServices.java
+++
b/framework/testtools/src/main/java/org/apache/ofbiz/testtools/TestRunServices.java
@@ -155,6 +155,23 @@ public final class TestRunServices {
return ServiceUtil.returnError("The test execution API is disabled
in this environment (test.api.enabled=false)");
}
+ // Per-component override of the global flag above: lets one
component's REST-triggered test
+ // run be disabled (or re-enabled) live via a SystemProperty row,
without touching every other
+ // component's access. Defaults to enabled ("true") when unset, so a
component that never sets
+ // this behaves exactly as it did before this check existed. Skipped
when componentName is
+ // blank - an unscoped multi-component suite-name lookup isn't
attributable to one component's
+ // flag. See
plugins/supporting-docs/specs/2026-08-21-per-component-test-api-toggle-design.md.
+ if (UtilValidate.isNotEmpty(componentName)) {
+ boolean componentEnabled = "true".equalsIgnoreCase(
+ readStringProperty(dctx.getDelegator(),
"test.api.enabled." + componentName, "true"));
+ if (!componentEnabled) {
+ Debug.logWarning("runTestSuite: rejected for user '" +
userLoginId + "', suite '" + suiteName + "'"
+ + " - test.api.enabled." + componentName + " is
false", MODULE);
+ return ServiceUtil.returnError("The test execution API is
disabled for component '" + componentName
+ + "' in this environment (test.api.enabled." +
componentName + "=false)");
+ }
+ }
+
// testMethodName reuses the exact same fail-closed validators the
ofbiz --test method=
// CLI path already built (TestRunContainer, same package - see its
javadoc for the full
// rationale). This first check needs only the two raw strings, not
any resolved suite, so
diff --git
a/framework/testtools/src/test/java/org/apache/ofbiz/testtools/TestRunServicesTest.java
b/framework/testtools/src/test/java/org/apache/ofbiz/testtools/TestRunServicesTest.java
index 28aeee062d..dc5ad8a20b 100644
---
a/framework/testtools/src/test/java/org/apache/ofbiz/testtools/TestRunServicesTest.java
+++
b/framework/testtools/src/test/java/org/apache/ofbiz/testtools/TestRunServicesTest.java
@@ -22,14 +22,22 @@ import java.util.Map;
import org.apache.ofbiz.entity.Delegator;
import org.apache.ofbiz.entity.GenericValue;
+import org.apache.ofbiz.entity.util.EntityUtilProperties;
import org.apache.ofbiz.security.Security;
import org.apache.ofbiz.service.DispatchContext;
import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.when;
class TestRunServicesTest {
@@ -54,10 +62,11 @@ class TestRunServicesTest {
@Test
void runTestSuiteReturnsErrorWhenApiDisabled() {
- // No stubbing of testtools.properties overrides: the real classpath
resource
- // framework/testtools/config/testtools.properties ships
test.api.enabled=false (Task 3),
- // and EntityUtilProperties falls through to it when
delegator.findOne("SystemProperty", ...)
- // - unstubbed on this mock - returns null.
+ // Mocks EntityUtilProperties directly (rather than relying on the
classpath testtools.properties
+ // file's actual value, which may legitimately vary between
environments) so this test asserts
+ // the exact global-disabled error message, not just a generic error
that could equally be the
+ // unrelated suite-resolution failure path. Same pattern as
+ // runTestSuiteReturnsErrorWhenComponentApiDisabled below.
DispatchContext dctx = mock(DispatchContext.class);
Security security = mock(Security.class);
Delegator delegator = mock(Delegator.class);
@@ -67,11 +76,18 @@ class TestRunServicesTest {
when(userLogin.getString("userLoginId")).thenReturn("admin");
when(security.hasPermission("TESTEXEC_ADMIN",
userLogin)).thenReturn(true);
- Map<String, Object> result = TestRunServices.runTestSuite(dctx,
- Map.of("suiteName", "example-tests", "userLogin", userLogin));
+ try (MockedStatic<EntityUtilProperties> entityUtilProperties =
+ Mockito.mockStatic(EntityUtilProperties.class,
Mockito.CALLS_REAL_METHODS)) {
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled",
delegator))
+ .thenReturn("false");
- assertThat(result.get("responseMessage"), is("error"));
- assertThat(result.get("runId"), nullValue());
+ Map<String, Object> result = TestRunServices.runTestSuite(dctx,
+ Map.of("suiteName", "example-tests", "userLogin",
userLogin));
+
+ assertThat(result.get("responseMessage"), is("error"));
+ assertThat(result.get("errorMessage"), is("The test execution API
is disabled in this environment (test.api.enabled=false)"));
+ assertThat(result.get("runId"), nullValue());
+ }
}
@Test
@@ -293,4 +309,102 @@ class TestRunServicesTest {
assertThat(result.get("responseMessage"), is("error"));
assertThat(result.get("errorMessage"), is("No such runId:
scoped-run-null-component"));
}
+
+ @Test
+ void runTestSuiteReturnsErrorWhenComponentApiDisabled() {
+ // Mocks EntityUtilProperties directly (rather than relying on the
classpath testtools.properties
+ // file, the way runTestSuiteReturnsErrorWhenApiDisabled does for the
global flag) because this
+ // test needs two different property values at once - the global flag
true, the per-component
+ // override false - which a single checked-in file can't express for
an arbitrary test-only
+ // component name. CALLS_REAL_METHODS means every other
EntityUtilProperties call not explicitly
+ // stubbed below still behaves normally.
+ DispatchContext dctx = mock(DispatchContext.class);
+ Security security = mock(Security.class);
+ Delegator delegator = mock(Delegator.class);
+ GenericValue userLogin = mock(GenericValue.class);
+ when(dctx.getSecurity()).thenReturn(security);
+ when(dctx.getDelegator()).thenReturn(delegator);
+ when(userLogin.getString("userLoginId")).thenReturn("admin");
+ when(security.hasPermission("TESTEXEC_ADMIN",
userLogin)).thenReturn(true);
+
+ try (MockedStatic<EntityUtilProperties> entityUtilProperties =
+ Mockito.mockStatic(EntityUtilProperties.class,
Mockito.CALLS_REAL_METHODS)) {
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled",
delegator))
+ .thenReturn("true");
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled.example",
delegator))
+ .thenReturn("false");
+
+ Map<String, Object> result = TestRunServices.runTestSuite(dctx,
+ Map.of("suiteName", "example-tests", "componentName",
"example", "userLogin", userLogin));
+
+ assertThat(result.get("responseMessage"), is("error"));
+ assertThat(result.get("errorMessage"), is("The test execution API
is disabled for component 'example' "
+ + "in this environment (test.api.enabled.example=false)"));
+ assertThat(result.get("runId"), nullValue());
+ }
+ }
+
+ @Test
+ void runTestSuiteDoesNotRejectWhenComponentApiIsNotDisabled() {
+ // Cannot verify a full successful run here - like
runScopedTestSuite's componentName-forcing
+ // behavior (see
runScopedTestSuitePassesThroughPermissionDenialUnchanged above), resolving a
+ // real suite needs a bootstrapped ComponentConfig this test module
doesn't have, so
+ // JunitSuiteWrapper's constructor still throws and this call still
ends in error - just not
+ // the new component-disabled error this test exists to rule out
(proving the gate was passed,
+ // not that a run actually completed). Full enabled-path behavior is
verified by manual/live
+ // validation (Task 3). Only the absence of the component-disabled
error is asserted below -
+ // asserting the error response itself would couple this test to the
unrelated
+ // un-bootstrapped-ComponentConfig failure mode, not the gate this
test exists to verify.
+ DispatchContext dctx = mock(DispatchContext.class);
+ Security security = mock(Security.class);
+ Delegator delegator = mock(Delegator.class);
+ GenericValue userLogin = mock(GenericValue.class);
+ when(dctx.getSecurity()).thenReturn(security);
+ when(dctx.getDelegator()).thenReturn(delegator);
+ when(userLogin.getString("userLoginId")).thenReturn("admin");
+ when(security.hasPermission("TESTEXEC_ADMIN",
userLogin)).thenReturn(true);
+
+ try (MockedStatic<EntityUtilProperties> entityUtilProperties =
+ Mockito.mockStatic(EntityUtilProperties.class,
Mockito.CALLS_REAL_METHODS)) {
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled",
delegator))
+ .thenReturn("true");
+ // test.api.enabled.example deliberately left unstubbed:
CALLS_REAL_METHODS falls through to
+ // the real EntityUtilProperties -> mock delegator (findList
unstubbed, returns null) ->
+ // properties-file fallback, which has no such key either, landing
on readStringProperty's own
+ // default ("true") - proving the opt-out default, not an explicit
override.
+
+ Map<String, Object> result = TestRunServices.runTestSuite(dctx,
+ Map.of("suiteName", "example-tests", "componentName",
"example", "userLogin", userLogin));
+
+ assertThat((String) result.get("errorMessage"),
not(containsString("is disabled for component")));
+ }
+ }
+
+ @Test
+ void runTestSuiteSkipsComponentCheckWhenComponentNameIsBlank() {
+ // No componentName in context at all - mirrors
runTestSuiteReturnsErrorWhenApiDisabled's own
+ // context map. The per-component override must never be consulted for
an unscoped call; this
+ // proves EntityUtilProperties.getPropertyValue is never invoked with
a "test.api.enabled."-
+ // prefixed property name (the global "test.api.enabled" key itself
does not match that prefix,
+ // since it has no trailing dot).
+ DispatchContext dctx = mock(DispatchContext.class);
+ Security security = mock(Security.class);
+ Delegator delegator = mock(Delegator.class);
+ GenericValue userLogin = mock(GenericValue.class);
+ when(dctx.getSecurity()).thenReturn(security);
+ when(dctx.getDelegator()).thenReturn(delegator);
+ when(userLogin.getString("userLoginId")).thenReturn("admin");
+ when(security.hasPermission("TESTEXEC_ADMIN",
userLogin)).thenReturn(true);
+
+ try (MockedStatic<EntityUtilProperties> entityUtilProperties =
+ Mockito.mockStatic(EntityUtilProperties.class,
Mockito.CALLS_REAL_METHODS)) {
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled",
delegator))
+ .thenReturn("true");
+
+ TestRunServices.runTestSuite(dctx, Map.of("suiteName",
"example-tests", "userLogin", userLogin));
+
+ entityUtilProperties.verify(() ->
EntityUtilProperties.getPropertyValue(eq("testtools"),
+ startsWith("test.api.enabled."), eq(delegator)), never());
+ }
+ }
}