with every @Component contribution, no explicit lookup.
- given().when()
- .get(INJECTING_CONSUMER_BASE + "/injected-contributions")
- .then()
- .statusCode(200)
- .body(containsString("Hello from SampleContribution!"));
- });
- }
-
-}
diff --git a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/JavaJobDecoratorSampleProjectIT.java b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/JavaJobDecoratorSampleProjectIT.java
deleted file mode 100644
index e1936a801ee..00000000000
--- a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/JavaJobDecoratorSampleProjectIT.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright (c) 2010-2026 Eclipse Dirigible contributors
- *
- * All rights reserved. This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
- * http://www.eclipse.org/legal/epl-v20.html
- *
- * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0
- */
-package org.eclipse.dirigible.integration.tests.ui.tests.sample;
-
-import static io.restassured.RestAssured.given;
-import static org.awaitility.Awaitility.await;
-import static org.hamcrest.Matchers.greaterThanOrEqualTo;
-
-import java.util.concurrent.TimeUnit;
-
-import org.eclipse.dirigible.tests.framework.logging.LogsAsserter;
-import io.restassured.http.ContentType;
-import org.eclipse.dirigible.tests.framework.util.SynchronizationUtil;
-import org.junit.jupiter.api.BeforeEach;
-
-import ch.qos.logback.classic.Level;
-
-public class JavaJobDecoratorSampleProjectIT extends SampleProjectRepositoryIT {
-
- private LogsAsserter consoleLogAsserter;
-
- @BeforeEach
- void setUp() {
- consoleLogAsserter = new LogsAsserter("app.out", Level.INFO);
- }
-
- @Override
- protected String getRepositoryURL() {
- return "https://github.com/dirigiblelabs/sample-java-job-decorator.git";
- }
-
- @Override
- protected void verifyProject() {
- // Self-describing interface style — CleanupJob implements JobHandler (schedule from cron()).
- await().atMost(20, TimeUnit.SECONDS)
- .pollInterval(1, TimeUnit.SECONDS)
- .until(() -> consoleLogAsserter.containsMessage("CleanupJob executed!", Level.INFO));
-
- // Method-level annotation style — Maintenance's @Scheduled method.
- await().atMost(20, TimeUnit.SECONDS)
- .pollInterval(1, TimeUnit.SECONDS)
- .until(() -> consoleLogAsserter.containsMessage("Maintenance.purgeTempFiles executed", Level.INFO));
-
- // Client-Java jobs are now real Job definitions on the shared scheduler, so they are VISIBLE
- // and MONITORED in the Jobs perspective (which reads /services/jobs) with engine "java" -
- // not hidden on a private in-JVM scheduler as before.
- restAssuredExecutor.execute(() -> given().when()
- .get("/services/jobs")
- .then()
- .statusCode(200)
- .body("findAll { it.engine == 'java' }.size()", greaterThanOrEqualTo(2)));
-
- // And they SURVIVE a registry synchronization pass - the synchronizer must not reap the
- // runtime-registered rows (they are not backed by a registry artefact).
- synchronizationProcessor.forceProcessSynchronizers();
- SynchronizationUtil.waitForStableSynchronization();
- restAssuredExecutor.execute(() -> given().when()
- .get("/services/jobs")
- .then()
- .statusCode(200)
- .body("findAll { it.engine == 'java' }.size()", greaterThanOrEqualTo(2)));
-
- verifyTriggerNowRunsTheClientJavaJob();
- }
-
- /**
- * Trigger-now (the Jobs perspective's play button) must run a client-Java job on the Java engine.
- * The manual path used to be JavaScript-only, so it tried to run the job's CLASS NAME as a
- * repository path to a JS module and answered 500 (dirigible #6305).
- *
- *
- * The status is the whole assertion: the Java dispatch either resolves the client bean and invokes
- * it, or throws (an unknown bean, a job body that fails) - and either way the endpoint surfaces
- * that as a 500. It cannot answer 200 without having run the job.
- */
- private void verifyTriggerNowRunsTheClientJavaJob() {
- String name = restAssuredExecutor.executeWithResult(() -> given().when()
- .get("/services/jobs")
- .then()
- .statusCode(200)
- .extract()
- .path("find { it.engine == 'java' }.name"));
-
- restAssuredExecutor.execute(() -> given().contentType(ContentType.JSON)
- .body("[]")
- .when()
- .post("/services/jobs/trigger/" + name)
- .then()
- .statusCode(200));
- }
-
-}
diff --git a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/JavaListenerDecoratorSampleProjectIT.java b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/JavaListenerDecoratorSampleProjectIT.java
deleted file mode 100644
index ed22c4c1e3b..00000000000
--- a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/JavaListenerDecoratorSampleProjectIT.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (c) 2010-2026 Eclipse Dirigible contributors
- *
- * All rights reserved. This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
- * http://www.eclipse.org/legal/epl-v20.html
- *
- * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0
- */
-package org.eclipse.dirigible.integration.tests.ui.tests.sample;
-
-import static io.restassured.RestAssured.given;
-import static org.awaitility.Awaitility.await;
-
-import java.util.concurrent.TimeUnit;
-
-import org.eclipse.dirigible.tests.framework.logging.LogsAsserter;
-import org.junit.jupiter.api.BeforeEach;
-
-import ch.qos.logback.classic.Level;
-
-public class JavaListenerDecoratorSampleProjectIT extends SampleProjectRepositoryIT {
-
- private static final String PROJECT = "sample-java-listener-decorator";
- private static final String LISTENER_TRIGGER = "/services/js/" + PROJECT + "/demo/listener/trigger.mjs";
-
- private LogsAsserter consoleLogAsserter;
-
- @BeforeEach
- void setUp() {
- consoleLogAsserter = new LogsAsserter("app.out", Level.INFO);
- }
-
- @Override
- protected String getRepositoryURL() {
- return "https://github.com/dirigiblelabs/sample-java-listener-decorator.git";
- }
-
- @Override
- protected void verifyProject() {
- restAssuredExecutor.execute(() -> given().when()
- .get(LISTENER_TRIGGER)
- .then()
- .statusCode(200));
-
- // Self-describing interface style — OrderListener implements MessageHandler.
- await().atMost(15, TimeUnit.SECONDS)
- .pollInterval(1, TimeUnit.SECONDS)
- .until(() -> consoleLogAsserter.containsMessage("OrderListener received:", Level.INFO));
-
- // Method-level annotation style — InvoiceListener's @Listener method records via the injected
- // Auditor.
- await().atMost(15, TimeUnit.SECONDS)
- .pollInterval(1, TimeUnit.SECONDS)
- .until(() -> consoleLogAsserter.containsMessage("Auditor: invoice received:", Level.INFO));
- }
-
-}
diff --git a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/JavaSampleProjectsIT.java b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/JavaSampleProjectsIT.java
new file mode 100644
index 00000000000..c1c03813851
--- /dev/null
+++ b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/JavaSampleProjectsIT.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (c) 2010-2026 Eclipse Dirigible contributors
+ *
+ * All rights reserved. This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v20.html
+ *
+ * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.dirigible.integration.tests.ui.tests.sample;
+
+import static io.restassured.RestAssured.given;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.dirigible.tests.framework.logging.LogsAsserter;
+import org.eclipse.dirigible.tests.framework.util.SynchronizationUtil;
+import org.junit.jupiter.api.Test;
+
+import ch.qos.logback.classic.Level;
+import io.restassured.http.ContentType;
+
+/**
+ * The client-Java samples, published together into one instance and verified one sample per test.
+ *
+ *
+ * Publishing them together also covers something the per-sample runs could not: the five projects
+ * are compiled in ONE {@code javac} batch into ONE {@code ClientClassLoader}, so they exercise the
+ * bean container with the whole family's {@code @Component}s registered side by side.
+ *
+ *
+ * Counterpart of {@link TypeScriptSampleProjectsIT} - the two families cannot share an instance
+ * because their entity samples both own the {@code SAMPLE_COUNTRY} table (see
+ * {@link SampleProjectsIT}).
+ */
+class JavaSampleProjectsIT extends SampleProjectsIT {
+
+ private static final String ENTITY_PROJECT = "sample-java-entity-decorators";
+ private static final String COUNTRY_CONTROLLER_BASE = "/services/java/" + ENTITY_PROJECT + "/demo/CountryController";
+ private static final String GREETING_BASE = "/services/java/" + ENTITY_PROJECT + "/demo/GreetingController";
+
+ private static final String EXTENSION_PROJECT = "sample-java-extension-decorator";
+ private static final String EXTENSION_CONSUMER_BASE = "/services/java/" + EXTENSION_PROJECT + "/demo/extension/ExtensionConsumer";
+ private static final String INJECTING_CONSUMER_BASE = "/services/java/" + EXTENSION_PROJECT + "/demo/extension/InjectingConsumer";
+
+ private static final String LISTENER_TRIGGER = "/services/js/sample-java-listener-decorator/demo/listener/trigger.mjs";
+
+ private static final String WEBSOCKET_STATUS_BASE = "/services/java/sample-java-websocket-decorator/demo/websocket/WebsocketStatus";
+
+ @Override
+ protected List getRepositoryUrls() {
+ return List.of( //
+ "https://github.com/dirigiblelabs/sample-java-entity-decorators.git", //
+ "https://github.com/dirigiblelabs/sample-java-extension-decorator.git", //
+ "https://github.com/dirigiblelabs/sample-java-job-decorator.git", //
+ "https://github.com/dirigiblelabs/sample-java-listener-decorator.git", //
+ "https://github.com/dirigiblelabs/sample-java-websocket-decorator.git");
+ }
+
+ /**
+ * The {@code @Entity} / {@code @Repository} / {@code @Controller} annotation stack with
+ * CSVIM-seeded country CRUD and OpenAPI registration, plus the Spring-style DI showcase
+ * (constructor injection and the {@code Beans} facade).
+ */
+ @Test
+ void entityDecorators() {
+ restAssuredExecutor.execute(() -> {
+ given().when()
+ .get(COUNTRY_CONTROLLER_BASE)
+ .then()
+ .statusCode(200)
+ .body(containsString("Afghanistan"))
+ .body(containsString("Albania"))
+ .body(containsString("Algeria"));
+
+ given().when()
+ .get(COUNTRY_CONTROLLER_BASE + "/1")
+ .then()
+ .statusCode(200)
+ .body(containsString("Afghanistan"));
+
+ given().when()
+ .get("/services/openapi")
+ .then()
+ .statusCode(200)
+ .body(containsString(COUNTRY_CONTROLLER_BASE))
+ .body(containsString(COUNTRY_CONTROLLER_BASE + "/{id}"));
+
+ // DI showcase — constructor injection of the @Component GreetingService.
+ given().when()
+ .get(GREETING_BASE + "/greet/World")
+ .then()
+ .statusCode(200)
+ .body(containsString("Hello, World!"));
+
+ // DI showcase — the Beans facade for programmatic lookup.
+ given().when()
+ .get(GREETING_BASE + "/greet-via-beans/World")
+ .then()
+ .statusCode(200)
+ .body(containsString("Hello, World!"));
+ });
+ }
+
+ @Test
+ void extensionDecorator() {
+ restAssuredExecutor.execute(() -> {
+ // Style 1 — Extensions.find(SampleExtensionPoint.class) returns the SampleContribution
+ // instances; the consumer maps each via describe(), so the body carries the contribution's
+ // own string. Asserting on it also verifies the cast — only a real implementor reaches it.
+ given().when()
+ .get(EXTENSION_CONSUMER_BASE + "/contributions")
+ .then()
+ .statusCode(200)
+ .body(containsString("Hello from SampleContribution!"));
+
+ // Style 2 — collection injection: the container populates the controller's
+ // List with every @Component contribution, no explicit lookup.
+ given().when()
+ .get(INJECTING_CONSUMER_BASE + "/injected-contributions")
+ .then()
+ .statusCode(200)
+ .body(containsString("Hello from SampleContribution!"));
+ });
+ }
+
+ @Test
+ void jobDecorator() {
+ LogsAsserter consoleLogAsserter = new LogsAsserter("app.out", Level.INFO);
+
+ // Both jobs fire on a sub-5s cron, so they log again regardless of how long ago the sample
+ // was published - the asserter only sees messages logged after it attached.
+
+ // Self-describing interface style — CleanupJob implements JobHandler (schedule from cron()).
+ await().atMost(20, TimeUnit.SECONDS)
+ .pollInterval(1, TimeUnit.SECONDS)
+ .until(() -> consoleLogAsserter.containsMessage("CleanupJob executed!", Level.INFO));
+
+ // Method-level annotation style — Maintenance's @Scheduled method.
+ await().atMost(20, TimeUnit.SECONDS)
+ .pollInterval(1, TimeUnit.SECONDS)
+ .until(() -> consoleLogAsserter.containsMessage("Maintenance.purgeTempFiles executed", Level.INFO));
+
+ // Client-Java jobs are now real Job definitions on the shared scheduler, so they are VISIBLE
+ // and MONITORED in the Jobs perspective (which reads /services/jobs) with engine "java" -
+ // not hidden on a private in-JVM scheduler as before.
+ restAssuredExecutor.execute(() -> given().when()
+ .get("/services/jobs")
+ .then()
+ .statusCode(200)
+ .body("findAll { it.engine == 'java' }.size()", greaterThanOrEqualTo(2)));
+
+ // And they SURVIVE a registry synchronization pass - the synchronizer must not reap the
+ // runtime-registered rows (they are not backed by a registry artefact).
+ synchronizationProcessor.forceProcessSynchronizers();
+ SynchronizationUtil.waitForStableSynchronization();
+ restAssuredExecutor.execute(() -> given().when()
+ .get("/services/jobs")
+ .then()
+ .statusCode(200)
+ .body("findAll { it.engine == 'java' }.size()", greaterThanOrEqualTo(2)));
+
+ verifyTriggerNowRunsTheClientJavaJob();
+ }
+
+ /**
+ * Trigger-now (the Jobs perspective's play button) must run a client-Java job on the Java engine.
+ * The manual path used to be JavaScript-only, so it tried to run the job's CLASS NAME as a
+ * repository path to a JS module and answered 500 (dirigible #6305).
+ *
+ *
+ * The status is the whole assertion: the Java dispatch either resolves the client bean and invokes
+ * it, or throws (an unknown bean, a job body that fails) - and either way the endpoint surfaces
+ * that as a 500. It cannot answer 200 without having run the job.
+ */
+ private void verifyTriggerNowRunsTheClientJavaJob() {
+ String name = restAssuredExecutor.executeWithResult(() -> given().when()
+ .get("/services/jobs")
+ .then()
+ .statusCode(200)
+ .extract()
+ .path("find { it.engine == 'java' }.name"));
+
+ restAssuredExecutor.execute(() -> given().contentType(ContentType.JSON)
+ .body("[]")
+ .when()
+ .post("/services/jobs/trigger/" + name)
+ .then()
+ .statusCode(200));
+ }
+
+ @Test
+ void listenerDecorator() {
+ LogsAsserter consoleLogAsserter = new LogsAsserter("app.out", Level.INFO);
+
+ restAssuredExecutor.execute(() -> given().when()
+ .get(LISTENER_TRIGGER)
+ .then()
+ .statusCode(200));
+
+ // Self-describing interface style — OrderListener implements MessageHandler.
+ await().atMost(15, TimeUnit.SECONDS)
+ .pollInterval(1, TimeUnit.SECONDS)
+ .until(() -> consoleLogAsserter.containsMessage("OrderListener received:", Level.INFO));
+
+ // Method-level annotation style — InvoiceListener's @Listener method records via the injected
+ // Auditor.
+ await().atMost(15, TimeUnit.SECONDS)
+ .pollInterval(1, TimeUnit.SECONDS)
+ .until(() -> consoleLogAsserter.containsMessage("Auditor: invoice received:", Level.INFO));
+ }
+
+ @Test
+ void websocketDecorator() {
+ restAssuredExecutor.execute(() -> given().when()
+ .get(WEBSOCKET_STATUS_BASE + "/status")
+ .then()
+ .statusCode(200)
+ // Self-describing interface style — ChatHandler implements WebsocketHandler.
+ .body(containsString("\"chat\":true"))
+ // Method-level annotation style — TickerHandler is @Websocket + @OnX.
+ .body(containsString("\"ticker\":true")));
+ }
+
+}
diff --git a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/JavaWebsocketDecoratorSampleProjectIT.java b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/JavaWebsocketDecoratorSampleProjectIT.java
deleted file mode 100644
index 3b3f83e30ca..00000000000
--- a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/JavaWebsocketDecoratorSampleProjectIT.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (c) 2010-2026 Eclipse Dirigible contributors
- *
- * All rights reserved. This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
- * http://www.eclipse.org/legal/epl-v20.html
- *
- * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0
- */
-package org.eclipse.dirigible.integration.tests.ui.tests.sample;
-
-import static io.restassured.RestAssured.given;
-import static org.hamcrest.Matchers.containsString;
-
-public class JavaWebsocketDecoratorSampleProjectIT extends SampleProjectRepositoryIT {
-
- private static final String PROJECT = "sample-java-websocket-decorator";
- private static final String WEBSOCKET_STATUS_BASE = "/services/java/" + PROJECT + "/demo/websocket/WebsocketStatus";
-
- @Override
- protected String getRepositoryURL() {
- return "https://github.com/dirigiblelabs/sample-java-websocket-decorator.git";
- }
-
- @Override
- protected void verifyProject() {
- restAssuredExecutor.execute(() -> given().when()
- .get(WEBSOCKET_STATUS_BASE + "/status")
- .then()
- .statusCode(200)
- // Self-describing interface style — ChatHandler implements WebsocketHandler.
- .body(containsString("\"chat\":true"))
- // Method-level annotation style — TickerHandler is @Websocket + @OnX.
- .body(containsString("\"ticker\":true")));
- }
-
-}
diff --git a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/JobDecoratorSampleProjectIT.java b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/JobDecoratorSampleProjectIT.java
deleted file mode 100644
index 69af9c4acca..00000000000
--- a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/JobDecoratorSampleProjectIT.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (c) 2022 codbex or an codbex affiliate company and contributors
- *
- * All rights reserved. This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
- * http://www.eclipse.org/legal/epl-v20.html
- *
- * SPDX-FileCopyrightText: 2022 codbex or an codbex affiliate company and contributors
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.eclipse.dirigible.integration.tests.ui.tests.sample;
-
-import ch.qos.logback.classic.Level;
-import org.eclipse.dirigible.tests.framework.logging.LogsAsserter;
-import org.junit.jupiter.api.BeforeEach;
-
-import java.util.concurrent.TimeUnit;
-
-import static org.awaitility.Awaitility.await;
-
-public class JobDecoratorSampleProjectIT extends SampleProjectRepositoryIT {
-
- private LogsAsserter consoleLogAsserter;
-
- @BeforeEach
- void setUp() {
- this.consoleLogAsserter = new LogsAsserter("app.out", Level.INFO);
- }
-
- @Override
- protected void verifyProject() {
- await().atMost(60, TimeUnit.SECONDS)
- .pollInterval(3, TimeUnit.SECONDS)
- .until(() -> consoleLogAsserter.containsMessage("MyJob executed!", Level.INFO));
- }
-
- @Override
- protected String getRepositoryURL() {
- return "https://github.com/dirigiblelabs/sample-job-decorator.git";
- }
-
-}
diff --git a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/ListenerDecoratorSampleProjectIT.java b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/ListenerDecoratorSampleProjectIT.java
deleted file mode 100644
index 7de02819113..00000000000
--- a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/ListenerDecoratorSampleProjectIT.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (c) 2022 codbex or an codbex affiliate company and contributors
- *
- * All rights reserved. This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
- * http://www.eclipse.org/legal/epl-v20.html
- *
- * SPDX-FileCopyrightText: 2022 codbex or an codbex affiliate company and contributors
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.eclipse.dirigible.integration.tests.ui.tests.sample;
-
-import ch.qos.logback.classic.Level;
-import org.eclipse.dirigible.tests.framework.logging.LogsAsserter;
-import org.junit.jupiter.api.BeforeEach;
-
-import static io.restassured.RestAssured.given;
-
-public class ListenerDecoratorSampleProjectIT extends SampleProjectRepositoryIT {
-
- private LogsAsserter consoleLogAsserter;
-
- @BeforeEach
- void setUp() {
- this.consoleLogAsserter = new LogsAsserter("app.out", Level.INFO);
- }
-
- @Override
- protected void verifyProject() {
- restAssuredExecutor.execute( //
- () -> given().get("/services/js/sample-listener-decorator/OrderListenerTrigger.js")
- .then()
- .statusCode(200));
-
- consoleLogAsserter.containsMessage("Hello from the OrderListener Trigger! Message: [ I am a message created at:", Level.INFO);
- consoleLogAsserter.containsMessage("Processing message event: [ I am a message created at:", Level.INFO);
- }
-
- @Override
- protected String getRepositoryURL() {
- return "https://github.com/dirigiblelabs/sample-listener-decorator.git";
- }
-
-}
diff --git a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/RolesDecoratorSampleProjectIT.java b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/RolesDecoratorSampleProjectIT.java
deleted file mode 100644
index b319185f285..00000000000
--- a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/RolesDecoratorSampleProjectIT.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright (c) 2022 codbex or an codbex affiliate company and contributors
- *
- * All rights reserved. This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
- * http://www.eclipse.org/legal/epl-v20.html
- *
- * SPDX-FileCopyrightText: 2022 codbex or an codbex affiliate company and contributors
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.eclipse.dirigible.integration.tests.ui.tests.sample;
-
-import ch.qos.logback.classic.Level;
-import io.restassured.parsing.Parser;
-import org.eclipse.dirigible.components.base.http.roles.Roles;
-import org.eclipse.dirigible.tests.framework.logging.LogsAsserter;
-import org.eclipse.dirigible.tests.framework.security.SecurityUtil;
-import org.junit.jupiter.api.BeforeEach;
-import org.springframework.beans.factory.annotation.Autowired;
-
-import static io.restassured.RestAssured.given;
-import static org.hamcrest.Matchers.equalTo;
-
-public class RolesDecoratorSampleProjectIT extends SampleProjectRepositoryIT {
-
- private static final String ADMIN_USERNAME = "adm1";
- private static final String ADMIN_PASS = "adm1-pass";
-
- private static final String UNAUTHORIZED_USER_USERNAME = "unathorized-usr";
- private static final String UNAUTHORIZED_USER_PASS = "unathorized-usr-pass";
-
- @Autowired
- private SecurityUtil securityUtil;
-
- private LogsAsserter consoleErrorLogAsserter;
-
- @Override
- protected String getRepositoryURL() {
- return "https://github.com/dirigiblelabs/sample-roles-decorator.git";
- }
-
- @BeforeEach
- void setUp() {
- this.consoleErrorLogAsserter = new LogsAsserter("app.err", Level.INFO);
-
- }
-
- @Override
- protected void verifyProject() {
- securityUtil.createUserInDefaultTenant(ADMIN_USERNAME, ADMIN_PASS, Roles.ADMINISTRATOR.getRoleName());
- restAssuredExecutor.execute(this::verifyAuthorizedUserAccess, ADMIN_USERNAME, ADMIN_PASS);
-
- securityUtil.createUserInDefaultTenant(UNAUTHORIZED_USER_USERNAME, UNAUTHORIZED_USER_PASS);
- restAssuredExecutor.execute(this::verifyUnauthorizedUserAccess, UNAUTHORIZED_USER_USERNAME, UNAUTHORIZED_USER_PASS);
- }
-
- private void verifyAuthorizedUserAccess() {
- // set default parser to enforce restassured json body validations
- // since the response doesn't specify the content type
- given().when()
- .get("/services/ts/sample-roles-decorator/RolesCheck.ts")
- .then()
- .statusCode(200)
- .using()
- .defaultParser(Parser.JSON)
- .body("message", equalTo("Roles Check"))
- .body("user", equalTo(ADMIN_USERNAME));
- }
-
- private void verifyUnauthorizedUserAccess() {
- given().when()
- .get("/services/ts/sample-roles-decorator/RolesCheck.ts")
- .then()
- .statusCode(500);
-
- consoleErrorLogAsserter.assertLoggedMessage("Current user [" + UNAUTHORIZED_USER_USERNAME
- + "] is not allowed to call module [RolesCheck]. Required some of roles [ADMINISTRATOR]", Level.ERROR);
- }
-
-}
diff --git a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/SampleLibraryLocalNativeAppIT.java b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/SampleLibraryLocalNativeAppIT.java
index d8bd28d3a0a..dca330ff26f 100644
--- a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/SampleLibraryLocalNativeAppIT.java
+++ b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/SampleLibraryLocalNativeAppIT.java
@@ -12,11 +12,14 @@
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.containsString;
+import java.util.List;
+
import io.restassured.http.ContentType;
import org.eclipse.dirigible.commons.config.DirigibleConfig;
import org.eclipse.dirigible.tests.framework.security.SecurityUtil;
import org.eclipse.dirigible.tests.framework.tenant.DirigibleTestTenant;
import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
/**
@@ -33,8 +36,14 @@
*
*
* Counterpart to {@code RemoteNativeAppIT} which exercises the {@code remote} kind.
+ *
+ *
+ * Stays a family of its own rather than joining {@link TypeScriptSampleProjectsIT} or
+ * {@link JavaSampleProjectsIT}: it spawns and reaps a real OS process (an {@code npm install} on
+ * first run) and grants a role on the fly, so sharing its instance would make the other samples pay
+ * for that and would leave live processes around their verifications.
*/
-public class SampleLibraryLocalNativeAppIT extends SampleProjectRepositoryIT {
+public class SampleLibraryLocalNativeAppIT extends SampleProjectsIT {
private static final String API_ROOT = "/services/native-apps-proxy/v1/library-native-app-nodejs/rest/api/v1";
@@ -65,16 +74,21 @@ static void allowTimeForFirstRunBootstrap() {
// first spawn, which dwarfs the platform's 30 s default ready timeout when node_modules has
// to be fetched. Five minutes is comfortably above worst-case cold-cache install + tsc on
// CI runners; the lazy-start filter polls every 200 ms so a fast warm run still wins early.
+ //
+ // Runs after the base class's clone-and-publish @BeforeAll, which is soon enough: the
+ // artefact declares `"mode": "lazy"`, and only a StartMode.ALWAYS app is spawned by the
+ // bootstrap, the monitor job or the synchronizer - this one waits for the first proxied
+ // request, which the test method below makes.
DirigibleConfig.NATIVE_APP_READY_TIMEOUT_MS.setIntValue(5 * 60 * 1000);
}
@Override
- protected String getRepositoryURL() {
- return "https://github.com/dirigiblelabs/sample-library-local-native-app.git";
+ protected List getRepositoryUrls() {
+ return List.of("https://github.com/dirigiblelabs/sample-library-local-native-app.git");
}
- @Override
- protected void verifyProject() {
+ @Test
+ void localNativeApp() {
// The default 'admin' user holds DEVELOPER + ADMINISTRATOR, which short-circuits the scope
// check in ExposedPathFilter — so we can't use it to exercise the negative path. Create a
// dedicated user with no privileged roles, then watch the proxy flip from 403 to 200/201
diff --git a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/SampleProjectRepositoryIT.java b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/SampleProjectRepositoryIT.java
deleted file mode 100644
index dc3e61ac0f1..00000000000
--- a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/SampleProjectRepositoryIT.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (c) 2022 codbex or an codbex affiliate company and contributors
- *
- * All rights reserved. This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
- * http://www.eclipse.org/legal/epl-v20.html
- *
- * SPDX-FileCopyrightText: 2022 codbex or an codbex affiliate company and contributors
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.eclipse.dirigible.integration.tests.ui.tests.sample;
-
-import org.eclipse.dirigible.components.initializers.synchronizer.SynchronizationProcessor;
-import org.eclipse.dirigible.tests.base.UserInterfaceIntegrationTest;
-import org.eclipse.dirigible.tests.framework.ide.GitPerspective;
-import org.eclipse.dirigible.tests.framework.ide.Workbench;
-import org.eclipse.dirigible.tests.framework.restassured.RestAssuredExecutor;
-import org.eclipse.dirigible.tests.framework.util.SynchronizationUtil;
-import org.junit.jupiter.api.Tag;
-import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-
-// "sample" (on top of the inherited "ui") routes the whole sample-project family into its own CI
-// shard - see the integration-tests matrix in .github/workflows/build.yml.
-@Tag("sample")
-abstract class SampleProjectRepositoryIT extends UserInterfaceIntegrationTest {
-
- @Autowired
- protected RestAssuredExecutor restAssuredExecutor;
-
- @Autowired
- protected SynchronizationProcessor synchronizationProcessor;
-
- @Test
- final void testSampleProject() {
- cloneProject();
-
- Workbench workbench = ide.openWorkbench();
- workbench.publishAll(true);
-
- synchronizationProcessor.forceProcessSynchronizers();
-
- // The registry watcher may deliver trailing publish events seconds later, scheduling one
- // more sync cycle that can re-register data-store entities (rebuilding their tables) while
- // the verification is already inserting data. Proceed only after the sync stays idle.
- SynchronizationUtil.waitForStableSynchronization();
-
- verifyProject();
- }
-
- protected abstract void verifyProject();
-
- private void cloneProject() {
- ide.openHomePage();
-
- GitPerspective gitPerspective = ide.openGitPerspective();
- String repositoryUrl = getRepositoryURL();
- gitPerspective.cloneRepository(repositoryUrl);
- }
-
- protected abstract String getRepositoryURL();
-
-}
diff --git a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/SampleProjectsIT.java b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/SampleProjectsIT.java
new file mode 100644
index 00000000000..0c781df5e78
--- /dev/null
+++ b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/SampleProjectsIT.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2010-2026 Eclipse Dirigible contributors
+ *
+ * All rights reserved. This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v20.html
+ *
+ * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.dirigible.integration.tests.ui.tests.sample;
+
+import java.util.List;
+
+import org.eclipse.dirigible.components.initializers.synchronizer.SynchronizationProcessor;
+import org.eclipse.dirigible.tests.base.UserInterfaceIntegrationTest;
+import org.eclipse.dirigible.tests.framework.ide.GitPerspective;
+import org.eclipse.dirigible.tests.framework.ide.Workbench;
+import org.eclipse.dirigible.tests.framework.restassured.RestAssuredExecutor;
+import org.eclipse.dirigible.tests.framework.util.SynchronizationUtil;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.annotation.DirtiesContext;
+
+/**
+ * Base for the sample-project ITs: clones a family of {@code dirigiblelabs/sample-*} repositories
+ * into one workspace, publishes them together, and lets each subclass verify one sample per
+ * {@code @Test} method.
+ *
+ *
+ * The clone-and-publish journey is identical for every sample, and it is the expensive part - a
+ * Dirigible boot, a Chrome session and a full publish + synchronization cycle. Running it once per
+ * sample cost the {@code samples} CI shard ~18 minutes for 14 near-identical journeys, so the
+ * family shares one boot ({@link DirtiesContext.ClassMode#AFTER_CLASS} overrides the per-method
+ * context reset inherited from the base) and one publish.
+ *
+ *
+ * Verifications must stay independent of each other and of their order: they run against the same
+ * live instance, so a method may not rely on state another method leaves behind.
+ *
+ *
+ * The families are split by what can share a runtime, not by taste:
+ * {@code sample-entity-decorators} and {@code sample-java-entity-decorators} both own the
+ * {@code SAMPLE_COUNTRY} table and both seed it from their own CSVIM, so they cannot be published
+ * side by side.
+ */
+// "sample" (on top of the inherited "ui") routes the whole sample-project family into its own CI
+// shard - see the integration-tests matrix in .github/workflows/build.yml.
+@Tag("sample")
+@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
+abstract class SampleProjectsIT extends UserInterfaceIntegrationTest {
+
+ /**
+ * The test class whose family is already cloned and published, or {@code null}.
+ *
+ *
+ * Static because the test instance is not: JUnit's default per-method lifecycle builds a fresh
+ * instance for every test, while the Spring context - and the published instance behind it - lives
+ * for the whole class. Keyed by class rather than a plain flag because this field is shared by
+ * every subclass, and a flag would let the second family skip its own publish and verify against
+ * the first family's instance.
+ */
+ private static Class> publishedFamily;
+
+ @Autowired
+ protected RestAssuredExecutor restAssuredExecutor;
+
+ @Autowired
+ protected SynchronizationProcessor synchronizationProcessor;
+
+ /**
+ * Clones and publishes the family once, before the first verification.
+ *
+ *
+ * Deliberately a guarded {@code @BeforeEach} and NOT a {@code @BeforeAll} on a
+ * {@code @TestInstance(PER_CLASS)} class: PER_CLASS creates and autowires the test instance BEFORE
+ * the {@code @BeforeAll} methods run, so the Spring context - and with it the whole platform -
+ * starts before {@code IntegrationTest.cleanBeforeTestClassExecution()} deletes the Dirigible
+ * folder. The next synchronization pass then reaps the platform's own registry
+ * ({@code Definition deleted: /shell-ide/extensions/shell.extension}) and the IDE renders with no
+ * perspectives at all. The default lifecycle keeps the cleaner ahead of the boot.
+ */
+ @BeforeEach
+ final void cloneAndPublishSamplesOnce() {
+ if (getClass().equals(publishedFamily)) {
+ return;
+ }
+
+ ide.openHomePage();
+
+ GitPerspective gitPerspective = ide.openGitPerspective();
+ getRepositoryUrls().forEach(gitPerspective::cloneRepository);
+
+ Workbench workbench = ide.openWorkbench();
+ workbench.publishAll(true);
+
+ synchronizationProcessor.forceProcessSynchronizers();
+
+ // The registry watcher may deliver trailing publish events seconds later, scheduling one
+ // more sync cycle that can re-register data-store entities (rebuilding their tables) while
+ // the verification is already inserting data. Proceed only after the sync stays idle.
+ SynchronizationUtil.waitForStableSynchronization();
+
+ publishedFamily = getClass();
+ }
+
+ /**
+ * The sample repositories to clone and publish together, in clone order.
+ *
+ * @return the repository URLs
+ */
+ protected abstract List getRepositoryUrls();
+
+}
diff --git a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/StoreAPISampleProjectIT.java b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/StoreAPISampleProjectIT.java
deleted file mode 100644
index 18f6391432d..00000000000
--- a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/StoreAPISampleProjectIT.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * Copyright (c) 2022 codbex or an codbex affiliate company and contributors
- *
- * All rights reserved. This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
- * http://www.eclipse.org/legal/epl-v20.html
- *
- * SPDX-FileCopyrightText: 2022 codbex or an codbex affiliate company and contributors
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.eclipse.dirigible.integration.tests.ui.tests.sample;
-
-import static io.restassured.RestAssured.given;
-import static org.hamcrest.Matchers.equalToCompressingWhiteSpace;
-
-public class StoreAPISampleProjectIT extends SampleProjectRepositoryIT {
-
- private static final String LIST_CUSTOMERS_RESPONSE_BODY = """
- List all customers:
- [
- {
- "address": "Sofia, Bulgaria",
- "name": "John",
- "id": 1,
- "$type$": "Customer"
- },
- {
- "address": "Varna, Bulgaria",
- "name": "Jane",
- "id": 2,
- "$type$": "Customer"
- },
- {
- "address": "Berlin, Germany",
- "name": "Matthias",
- "id": 3,
- "$type$": "Customer"
- }
- ]""";
- private static final String COMPLEX_CUSTOMERS_RESPONSE_BODY = """
-
- Select customers with first name John:
- [
- {
- "address": "Sofia, Bulgaria",
- "name": "John",
- "id": 1,
- "$type$": "Customer"
- }
- ]
-
- Select native customers with first name John:
- [
- {
- "customer_id": 1,
- "customer_address": "Sofia, Bulgaria",
- "customer_name": "John"
- }
- ]
-
- Find customers by Example:
- [
- {
- "address": "Sofia, Bulgaria",
- "name": "John",
- "id": 1,
- "$type$": "Customer"
- }
- ]
-
- List customers with filter options:
- [
- {
- "address": "Varna, Bulgaria",
- "name": "Jane",
- "id": 2,
- "$type$": "Customer"
- },
- {
- "address": "Sofia, Bulgaria",
- "name": "John",
- "id": 1,
- "$type$": "Customer"
- }
- ]
-
- Select customers with first name starts with J:
- [
- {
- "address": "Sofia, Bulgaria",
- "name": "John",
- "id": 1,
- "$type$": "Customer"
- },
- {
- "address": "Varna, Bulgaria",
- "name": "Jane",
- "id": 2,
- "$type$": "Customer"
- }
- ]
-
- Select customers with first name starts with M with typed query:
- [
- {
- "address": "Berlin, Germany",
- "name": "Matthias",
- "id": 3,
- "$type$": "Customer"
- }
- ]
-
- Select customers with first name starts with M with named query:
- [
- {
- "address": "Berlin, Germany",
- "name": "Matthias",
- "id": 3,
- "$type$": "Customer"
- }
- ]
-
- Select customers with first name in ['John', 'Jane'] with named query:
- [
- {
- "address": "Sofia, Bulgaria",
- "name": "John",
- "id": 1,
- "$type$": "Customer"
- },
- {
- "address": "Varna, Bulgaria",
- "name": "Jane",
- "id": 2,
- "$type$": "Customer"
- }
- ]""";
-
- @Override
- protected void verifyProject() {
- // Retry-on-AssertionError: if a late sync cycle re-registers the Customer entity and
- // rebuilds its table between the init and the list (dropping the just-inserted rows and
- // resetting the identity counter), the next attempt re-inits and converges.
- restAssuredExecutor.execute( //
- () -> {
- given().when()
- .get("/services/ts/sample-store-api/InitCustomers.ts")
- .then()
- .statusCode(200);
-
- given().when()
- .get("/services/ts/sample-store-api/ListCustomers.ts")
- .then()
- .statusCode(200)
- .body(equalToCompressingWhiteSpace(LIST_CUSTOMERS_RESPONSE_BODY));
-
- given().when()
- .get("/services/ts/sample-store-api/ComplexQueries.ts")
- .then()
- .statusCode(200)
- .body(equalToCompressingWhiteSpace(COMPLEX_CUSTOMERS_RESPONSE_BODY));
- }, 90);
- }
-
- @Override
- protected String getRepositoryURL() {
- return "https://github.com/dirigiblelabs/sample-store-api.git";
- }
-
-}
-
diff --git a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/TypeScriptSampleProjectsIT.java b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/TypeScriptSampleProjectsIT.java
new file mode 100644
index 00000000000..a04ae147ab3
--- /dev/null
+++ b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/TypeScriptSampleProjectsIT.java
@@ -0,0 +1,383 @@
+/*
+ * Copyright (c) 2010-2026 Eclipse Dirigible contributors
+ *
+ * All rights reserved. This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v20.html
+ *
+ * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.dirigible.integration.tests.ui.tests.sample;
+
+import static io.restassured.RestAssured.given;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.equalToCompressingWhiteSpace;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+
+import org.eclipse.dirigible.components.base.http.roles.Roles;
+import org.eclipse.dirigible.tests.framework.logging.LogsAsserter;
+import org.eclipse.dirigible.tests.framework.security.SecurityUtil;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import ch.qos.logback.classic.Level;
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.parsing.Parser;
+import io.restassured.specification.RequestSpecification;
+
+/**
+ * The TypeScript / JavaScript decorator samples, published together into one instance and verified
+ * one sample per test. Counterpart of {@link JavaSampleProjectsIT}, which covers the client-Java
+ * samples - the two families are kept apart because their entity samples share a table name (see
+ * {@link SampleProjectsIT}).
+ */
+class TypeScriptSampleProjectsIT extends SampleProjectsIT {
+
+ private static final String COUNTRIES_RESPONSE_BODY =
+ """
+ [{"Code2":"AF","Numeric":"004","Code3":"AFG","Id":1,"$type$":"CountryEntity","Name":"Afghanistan"},{"Code2":"AL","Numeric":"008","Code3":"ALB","Id":2,"$type$":"CountryEntity","Name":"Albania"},{"Code2":"DZ","Numeric":"012","Code3":"DZA","Id":3,"$type$":"CountryEntity","Name":"Algeria"}]
+ """;
+
+ /**
+ * The expected OpenAPI document with the instance's own version replaced by
+ * {@link #VERSION_PLACEHOLDER}. The served {@code info.version} is the real build version, which
+ * changes on every release and every development bump - so it is normalised out of BOTH sides of
+ * the comparison rather than pinned here. Pinning it made this test fail on both CI databases the
+ * first night after the version stopped being served as a literal {@code ${project.version}}
+ * (#6644).
+ *
+ *
+ * The document stays an exact match even though seven more samples share the instance:
+ * {@code CountryController.ts} is the only {@code @Controller} in the family, and only a controller
+ * contributes an OpenAPI fragment.
+ */
+ private static final String VERSION_PLACEHOLDER = "";
+
+ private static final String OPENAPI_RESPONSE_BODY =
+ """
+ {"openapi":"3.0.1","info":{"title":"Applications Services Open API","description":"Services Open API provided by the applications","contact":{"name":"Eclipse Dirigible","url":"https://www.dirigible.io","email":"dirigible-dev@eclipse.org"},"license":{"name":"Eclipse Public License - v 2.0","url":"https://www.eclipse.org/legal/epl-v20.html"},"version":""},"servers":[{"url":"/services/ts"},{"url":"/services/ts"}],"security":[],"tags":[],"paths":{"/sample-entity-decorators/CountryController.ts/":{"get":{"tags":["CountryController"],"summary":"getAll CountryController ","operationId":"getAll","parameters":[],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CountryEntity"}}}}}}}}},"components":{"schemas":{"number":{"type":"number"},"CountryEntity":{"type":"object","properties":{"Code2":{"type":"string"},"Numeric":{"type":"string"},"Code3":{"type":"string"},"Id":{"type":"number","description":"My Id"},"Name":{"type":"string","description":"My Name"}},"description":"Sample Country Entity"},"string":{"type":"string"},"any":{"type":"object"}},"responses":{},"parameters":{},"examples":{},"requestBodies":{},"headers":{},"securitySchemes":{},"links":{},"callbacks":{}}}
+ """;
+
+ /** {@code "version":""} inside the OpenAPI {@code info} block. */
+ private static final Pattern OPENAPI_VERSION = Pattern.compile("\"version\":\"[^\"]*\"");
+
+ private static final String LIST_CUSTOMERS_RESPONSE_BODY = """
+ List all customers:
+ [
+ {
+ "address": "Sofia, Bulgaria",
+ "name": "John",
+ "id": 1,
+ "$type$": "Customer"
+ },
+ {
+ "address": "Varna, Bulgaria",
+ "name": "Jane",
+ "id": 2,
+ "$type$": "Customer"
+ },
+ {
+ "address": "Berlin, Germany",
+ "name": "Matthias",
+ "id": 3,
+ "$type$": "Customer"
+ }
+ ]""";
+
+ private static final String COMPLEX_CUSTOMERS_RESPONSE_BODY = """
+
+ Select customers with first name John:
+ [
+ {
+ "address": "Sofia, Bulgaria",
+ "name": "John",
+ "id": 1,
+ "$type$": "Customer"
+ }
+ ]
+
+ Select native customers with first name John:
+ [
+ {
+ "customer_id": 1,
+ "customer_address": "Sofia, Bulgaria",
+ "customer_name": "John"
+ }
+ ]
+
+ Find customers by Example:
+ [
+ {
+ "address": "Sofia, Bulgaria",
+ "name": "John",
+ "id": 1,
+ "$type$": "Customer"
+ }
+ ]
+
+ List customers with filter options:
+ [
+ {
+ "address": "Varna, Bulgaria",
+ "name": "Jane",
+ "id": 2,
+ "$type$": "Customer"
+ },
+ {
+ "address": "Sofia, Bulgaria",
+ "name": "John",
+ "id": 1,
+ "$type$": "Customer"
+ }
+ ]
+
+ Select customers with first name starts with J:
+ [
+ {
+ "address": "Sofia, Bulgaria",
+ "name": "John",
+ "id": 1,
+ "$type$": "Customer"
+ },
+ {
+ "address": "Varna, Bulgaria",
+ "name": "Jane",
+ "id": 2,
+ "$type$": "Customer"
+ }
+ ]
+
+ Select customers with first name starts with M with typed query:
+ [
+ {
+ "address": "Berlin, Germany",
+ "name": "Matthias",
+ "id": 3,
+ "$type$": "Customer"
+ }
+ ]
+
+ Select customers with first name starts with M with named query:
+ [
+ {
+ "address": "Berlin, Germany",
+ "name": "Matthias",
+ "id": 3,
+ "$type$": "Customer"
+ }
+ ]
+
+ Select customers with first name in ['John', 'Jane'] with named query:
+ [
+ {
+ "address": "Sofia, Bulgaria",
+ "name": "John",
+ "id": 1,
+ "$type$": "Customer"
+ },
+ {
+ "address": "Varna, Bulgaria",
+ "name": "Jane",
+ "id": 2,
+ "$type$": "Customer"
+ }
+ ]""";
+
+ private static final String ADMIN_USERNAME = "adm1";
+ private static final String ADMIN_PASS = "adm1-pass";
+
+ private static final String UNAUTHORIZED_USER_USERNAME = "unathorized-usr";
+ private static final String UNAUTHORIZED_USER_PASS = "unathorized-usr-pass";
+
+ @Autowired
+ private SecurityUtil securityUtil;
+
+ @Override
+ protected List getRepositoryUrls() {
+ return List.of( //
+ "https://github.com/dirigiblelabs/sample-component-decorators.git", //
+ "https://github.com/dirigiblelabs/sample-entity-decorators.git", //
+ "https://github.com/dirigiblelabs/sample-extension-decorator.git", //
+ "https://github.com/dirigiblelabs/sample-job-decorator.git", //
+ "https://github.com/dirigiblelabs/sample-listener-decorator.git", //
+ "https://github.com/dirigiblelabs/sample-roles-decorator.git", //
+ "https://github.com/dirigiblelabs/sample-store-api.git", //
+ "https://github.com/dirigiblelabs/sample-websocket-decorator.git");
+ }
+
+ @Test
+ void componentDecorator() {
+ restAssuredExecutor.execute( //
+ () -> given().when()
+ .get("/services/ts/sample-component-decorators/OrderProcessor.ts")
+ .then()
+ .statusCode(200)
+ .body(equalToCompressingWhiteSpace("Do Payment: {\"status\":\"OK\",\"data\":\"123.45\"}")));
+ }
+
+ @Test
+ void entityDecorators() {
+ restAssuredExecutor.execute( //
+ () -> {
+ RequestSpecification requestSpec = new RequestSpecBuilder().setUrlEncodingEnabled(false)
+ .build();
+
+ // Use the spec where encoding is disabled otherwise limit param is encoded and skipped by the code
+ given().spec(requestSpec)
+ .queryParam("$limit", 3)
+ .get("/services/ts/sample-entity-decorators/CountryController.ts")
+ .then()
+ .statusCode(200)
+ .body(equalToCompressingWhiteSpace(COUNTRIES_RESPONSE_BODY));
+
+ // TODO: documentation texts from @Document annotations are not added to the open api response. Fix
+ // this issue and adapt the test.
+ String openApi = given().when()
+ .get("/services/openapi")
+ .then()
+ .statusCode(200)
+ .extract()
+ .asString();
+ assertThat("the served OpenAPI document should match, ignoring the instance's own version", //
+ withoutVersion(openApi), equalToCompressingWhiteSpace(OPENAPI_RESPONSE_BODY));
+ }, 60);
+ }
+
+ /**
+ * The OpenAPI document with the instance's own {@code info.version} normalised to
+ * {@link #VERSION_PLACEHOLDER}, so the assertion survives every release and development version
+ * bump.
+ *
+ * @param openApi the served document
+ * @return the document with its version normalised
+ */
+ private static String withoutVersion(String openApi) {
+ return OPENAPI_VERSION.matcher(openApi)
+ .replaceFirst("\"version\":\"" + VERSION_PLACEHOLDER + "\"");
+ }
+
+ @Test
+ void extensionDecorator() {
+ restAssuredExecutor.execute( //
+ () -> given().when()
+ .get("/services/ts/sample-extension-decorator/OrderDiscount.ts")
+ .then()
+ .statusCode(200)
+ .body(equalToCompressingWhiteSpace("\"Discount: 5\"")));
+ }
+
+ @Test
+ void jobDecorator() {
+ LogsAsserter consoleLogAsserter = new LogsAsserter("app.out", Level.INFO);
+
+ // The job's cron fires every 10 seconds, so it logs again regardless of how long ago the
+ // sample was published - the asserter only sees messages logged after it attached.
+ await().atMost(60, TimeUnit.SECONDS)
+ .pollInterval(3, TimeUnit.SECONDS)
+ .until(() -> consoleLogAsserter.containsMessage("MyJob executed!", Level.INFO));
+ }
+
+ @Test
+ void listenerDecorator() {
+ LogsAsserter consoleLogAsserter = new LogsAsserter("app.out", Level.INFO);
+
+ restAssuredExecutor.execute( //
+ () -> given().get("/services/js/sample-listener-decorator/OrderListenerTrigger.js")
+ .then()
+ .statusCode(200));
+
+ consoleLogAsserter.containsMessage("Hello from the OrderListener Trigger! Message: [ I am a message created at:", Level.INFO);
+ consoleLogAsserter.containsMessage("Processing message event: [ I am a message created at:", Level.INFO);
+ }
+
+ @Test
+ void rolesDecorator() {
+ LogsAsserter consoleErrorLogAsserter = new LogsAsserter("app.err", Level.INFO);
+
+ securityUtil.ensureUserInDefaultTenant(ADMIN_USERNAME, ADMIN_PASS, Roles.ADMINISTRATOR.getRoleName());
+ restAssuredExecutor.execute(this::verifyAuthorizedUserAccess, ADMIN_USERNAME, ADMIN_PASS);
+
+ securityUtil.ensureUserInDefaultTenant(UNAUTHORIZED_USER_USERNAME, UNAUTHORIZED_USER_PASS);
+ restAssuredExecutor.execute(() -> verifyUnauthorizedUserAccess(consoleErrorLogAsserter), UNAUTHORIZED_USER_USERNAME,
+ UNAUTHORIZED_USER_PASS);
+ }
+
+ private void verifyAuthorizedUserAccess() {
+ // set default parser to enforce restassured json body validations
+ // since the response doesn't specify the content type
+ given().when()
+ .get("/services/ts/sample-roles-decorator/RolesCheck.ts")
+ .then()
+ .statusCode(200)
+ .using()
+ .defaultParser(Parser.JSON)
+ .body("message", equalTo("Roles Check"))
+ .body("user", equalTo(ADMIN_USERNAME));
+ }
+
+ private void verifyUnauthorizedUserAccess(LogsAsserter consoleErrorLogAsserter) {
+ given().when()
+ .get("/services/ts/sample-roles-decorator/RolesCheck.ts")
+ .then()
+ .statusCode(500);
+
+ consoleErrorLogAsserter.assertLoggedMessage("Current user [" + UNAUTHORIZED_USER_USERNAME
+ + "] is not allowed to call module [RolesCheck]. Required some of roles [ADMINISTRATOR]", Level.ERROR);
+ }
+
+ @Test
+ void storeApi() {
+ // Retry-on-AssertionError: if a late sync cycle re-registers the Customer entity and
+ // rebuilds its table between the init and the list (dropping the just-inserted rows and
+ // resetting the identity counter), the next attempt re-inits and converges.
+ restAssuredExecutor.execute( //
+ () -> {
+ given().when()
+ .get("/services/ts/sample-store-api/InitCustomers.ts")
+ .then()
+ .statusCode(200);
+
+ given().when()
+ .get("/services/ts/sample-store-api/ListCustomers.ts")
+ .then()
+ .statusCode(200)
+ .body(equalToCompressingWhiteSpace(LIST_CUSTOMERS_RESPONSE_BODY));
+
+ given().when()
+ .get("/services/ts/sample-store-api/ComplexQueries.ts")
+ .then()
+ .statusCode(200)
+ .body(equalToCompressingWhiteSpace(COMPLEX_CUSTOMERS_RESPONSE_BODY));
+ }, 90);
+ }
+
+ @Test
+ void websocketDecorator() {
+ LogsAsserter consoleLogAsserter = new LogsAsserter("app.out", Level.INFO);
+
+ // The only verification in the family that drives the browser. Every test gets a fresh Chrome
+ // (the base closes the driver after each), so the session has to be established here - the
+ // sample's page is behind authentication and would otherwise render the login form.
+ ide.openHomePage();
+
+ browser.openPath("/services/web/sample-websocket-decorator/order-websocket-page.html");
+
+ browser.enterTextInElementById("fromInput", "Test user");
+ browser.clickOnElementById("connectBtn");
+
+ browser.enterTextInElementById("textInput", "A test message");
+ browser.clickOnElementById("sendMessage");
+
+ browser.assertElementExistsByIdAndContainsText("response", "Test user: Hello from OrderWebsocket! [A test message]");
+
+ await().atMost(10, TimeUnit.SECONDS)
+ .pollInterval(1, TimeUnit.SECONDS)
+ .until(() -> consoleLogAsserter.containsMessage("Message received: A test message, from: Test user", Level.INFO));
+ }
+
+}
diff --git a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/WebsocketDecoratorSampleProjectIT.java b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/WebsocketDecoratorSampleProjectIT.java
deleted file mode 100644
index a8894943a0c..00000000000
--- a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/sample/WebsocketDecoratorSampleProjectIT.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (c) 2025 codbex or an codbex affiliate company and contributors
- *
- * All rights reserved. This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
- * http://www.eclipse.org/legal/epl-v20.html
- *
- * SPDX-FileCopyrightText: 2025 codbex or an codbex affiliate company and contributors
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.eclipse.dirigible.integration.tests.ui.tests.sample;
-
-import ch.qos.logback.classic.Level;
-import org.eclipse.dirigible.tests.framework.logging.LogsAsserter;
-import org.junit.jupiter.api.BeforeEach;
-
-import java.util.concurrent.TimeUnit;
-
-import static org.awaitility.Awaitility.await;
-
-public class WebsocketDecoratorSampleProjectIT extends SampleProjectRepositoryIT {
-
- private LogsAsserter consoleLogAsserter;
-
- @BeforeEach
- void setUp() {
- this.consoleLogAsserter = new LogsAsserter("app.out", Level.INFO);
- }
-
- @Override
- protected void verifyProject() {
- browser.openPath("/services/web/sample-websocket-decorator/order-websocket-page.html");
-
- browser.enterTextInElementById("fromInput", "Test user");
- browser.clickOnElementById("connectBtn");
-
- browser.enterTextInElementById("textInput", "A test message");
- browser.clickOnElementById("sendMessage");
-
- browser.assertElementExistsByIdAndContainsText("response", "Test user: Hello from OrderWebsocket! [A test message]");
-
- await().atMost(10, TimeUnit.SECONDS)
- .pollInterval(1, TimeUnit.SECONDS)
- .until(() -> consoleLogAsserter.containsMessage("Message received: A test message, from: Test user", Level.INFO));
- }
-
- @Override
- protected String getRepositoryURL() {
- return "https://github.com/dirigiblelabs/sample-websocket-decorator.git";
- }
-
-}