diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index cddd569..1e2793b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,7 +7,7 @@ on:
env:
WILDFLY_VERSION: 39.0.1.Final
- TEST_CLASS: StickySessionTest,SslFailoverTest,SslCrlTest
+ TEST_CLASS: StickySessionTest,SslFailoverTest,SslCrlTest,ModClusterAjpTest
MOD_PROXY_CLUSTER_REPO: https://github.com/modcluster/mod_proxy_cluster.git
jobs:
@@ -44,12 +44,12 @@ jobs:
- name: Run tests (Docker)
if: runner.os != 'Windows'
shell: bash
- run: mvn -B test -Dtest=${{ env.TEST_CLASS }} -DexcludedGroups=none -Dbalancer.type=${{ matrix.balancer }} -Dwildfly.version=${{ env.WILDFLY_VERSION }}
+ run: mvn -B test -Dtest=${{ env.TEST_CLASS }} -Dbalancer.type=${{ matrix.balancer }} -Dwildfly.version=${{ env.WILDFLY_VERSION }}
- name: Run tests (Native)
if: runner.os == 'Windows'
shell: bash
- run: mvn -B test -Pnative -Dtest=${{ env.TEST_CLASS }} -DexcludedGroups=none -Dbalancer.type=${{ matrix.balancer }} -Dwildfly.version=${{ env.WILDFLY_VERSION }}
+ run: mvn -B test -Pnative -Dtest=${{ env.TEST_CLASS }} -Dbalancer.type=${{ matrix.balancer }} -Dwildfly.version=${{ env.WILDFLY_VERSION }}
- name: Publish test results
uses: mikepenz/action-junit-report@v6
@@ -101,7 +101,6 @@ jobs:
run: |
mvn -B test -Pnative \
-Dtest=${{ env.TEST_CLASS }} \
- -DexcludedGroups=none \
-Dbalancer.type=httpd \
-Dhttpd.home=/usr \
-Dhttpd.modules.path=${{ github.workspace }}/target/mod_proxy_cluster/native/build/modules \
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 9ab00db..0771641 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -211,7 +211,14 @@ DockerImageName.parse("quay.io/modcluster/mod_cluster-httpd:latest")
**To be implemented**
-### 8. Integration (`org.jboss.modcluster.test.integration`)
+### 8. AJP Protocol (`org.jboss.modcluster.test.ajp`)
+- AJP data path through mod_cluster
+- Worker registration with AJP scheme
+- End-to-end request proxying via mod_proxy_ajp
+
+**Example**: `ModClusterAjpTest.java` (httpd only — `@Tag("httpd")`)
+
+### 9. Integration (`org.jboss.modcluster.test.integration`)
- EJB over HTTP
- WebSockets
- Full application scenarios
@@ -267,6 +274,7 @@ Jenkins Pipeline
| LoadBalancingGroupFailoverTest | ✓ | ✓ |
| SSLTest | ✓ | ✓ |
| DynamicReconfTest | ✓ | ✓ |
+| ModClusterAjpTest | — | ✓ |
## Configuration Points
diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc
index e456e97..c12b722 100644
--- a/CONTRIBUTING.adoc
+++ b/CONTRIBUTING.adoc
@@ -110,6 +110,7 @@ public class MyModClusterTest {
Choose the appropriate package for your test:
+* `/ajp/` — AJP protocol data path tests (httpd only, `@Tag("httpd")`)
* `/cli/` — Management and configuration via CLI/Creaper
* `/failover/` — Failover scenarios, session migration, worker failure
* `/loadbalancing/` — Load distribution, metrics, algorithms
diff --git a/README.md b/README.md
index 456c5fe..2495a9b 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,8 @@ This test suite uses:
```
src/test/java/org/jboss/modcluster/test/
+├── ajp/ # AJP protocol tests (httpd only)
+│ └── ModClusterAjpTest.java
├── apps/ # Test application endpoints
│ ├── ejb/ # EJB beans, client, and builders
│ └── ... # WebSocket, demo app, etc.
@@ -308,6 +310,9 @@ String result = worker.executeCli("/subsystem=modcluster:read-resource");
### EJB over HTTP Tests
- **EjbViaHttpTest** - HTTP invoker endpoint registration, stateful EJB stickiness with failover, stateless EJB invocation
+### AJP Protocol Tests
+- **ModClusterAjpTest** - AJP data path through mod_cluster (httpd only)
+
### High Availability Tests
- **HighAvailabilityTest** - Hot standby, multiple balancers
- **SoakTest** - Long-running stability testing
@@ -333,14 +338,15 @@ This test suite aims for feature parity with `noe-tests/modcluster` (64 test fil
| Initial Load | Yes | InitialLoadTest |
| EJB over HTTP | Yes | EjbViaHttpTest |
| Soak/Stress Testing | Yes | SoakTest |
+| AJP Protocol | Partial | ModClusterAjpTest |
### Not Yet Implemented
-| Area | noe-tests Reference |
-|------|-------------------|
-| AJP Protocol | ModClusterAJP.groovy |
-| mod_proxy / mod_rewrite | ModProxyTest.groovy, ModRewriteTest.groovy |
-| Bug-specific regressions | JBCS*, JBQA* test files |
+| Area | noe-tests Reference | Notes |
+|------|-------------------|-------|
+| AJP Secret Validation | ModClusterAJP.groovy | WildFly lacks native AJP secret support (Tomcat-only feature) |
+| mod_proxy / mod_rewrite | ModProxyTest.groovy, ModRewriteTest.groovy | |
+| Bug-specific regressions | JBCS*, JBQA* test files | |
## How It Works
diff --git a/TESTING.md b/TESTING.md
index 34dc0f6..911112b 100644
--- a/TESTING.md
+++ b/TESTING.md
@@ -257,6 +257,17 @@ mvn test -Dtest=org.jboss.modcluster.test.ssl.*
mvn test -Dtest=StickySessionTest,SSLTest,LoadBalancingGroupFailoverTest
```
+### httpd-Only Tests
+
+Some tests require the httpd balancer and are annotated with `@Tag("httpd")`.
+These are automatically excluded when running with `-Dbalancer.type=undertow`
+(the default) via the Maven undertow profile's `excludedGroups`.
+
+```bash
+# Run AJP protocol tests (requires httpd)
+mvn test -Dtest=ModClusterAjpTest -Dbalancer.type=httpd
+```
+
## Matrix Testing
### Both Balancers Sequentially
diff --git a/pom.xml b/pom.xml
index 4fa03a9..14f5a45 100644
--- a/pom.xml
+++ b/pom.xml
@@ -304,10 +304,28 @@
By default, mod_cluster registers the worker's HTTP listener with the balancer + * and all proxied traffic flows over HTTP. This test reconfigures the mod_cluster + * subsystem to register an AJP listener instead, verifying the full AJP data path: + * client → httpd → {@code mod_proxy_ajp} → Undertow AJP listener → response.
+ * + *Requires the httpd balancer because Undertow-based balancers do not support + * AJP backend connections (the {@code @Tag("httpd")} annotation causes this test + * to be skipped when {@code -Dbalancer.type=undertow}).
+ * + *Ported from noe-tests {@code ModClusterAJP.groovy}. The original test also + * validates AJP secret matching ({@code AJPSecret} directive vs. Tomcat's + * {@code secretRequired}/{@code secret} connector attributes). Those tests are + * omitted here because WildFly's Undertow AJP listener does not support native + * secret validation — the equivalent WildFly mechanism uses Undertow + * expression filters, which is a different code path.
+ * + * @see org.jboss.modcluster.test.utils.WildFlyUndertowManager#addAjpListener(String, String, String, int) + * @see org.jboss.modcluster.test.utils.WildFlyModClusterManager#setListener(String) + */ +@Tag("httpd") +@ExtendWith(ModClusterTestExtension.class) +public class ModClusterAjpTest { + + private static final Logger log = LoggerFactory.getLogger(ModClusterAjpTest.class); + + private static final int AJP_PORT = 8009; + private static final String AJP_LISTENER = "ajp"; + private static final String AJP_SOCKET_BINDING = "ajp"; + + /** + * Verifies that mod_cluster registers the worker over AJP and that HTTP requests + * are correctly proxied through the AJP data path. + * + *The test adds an AJP listener on port {@value AJP_PORT}, switches the
+ * mod_cluster proxy's {@code listener} attribute from {@code "default"} (HTTP)
+ * to {@code "ajp"}, and waits for the worker to re-register. It then asserts
+ * that the balancer's MCMP INFO reports the worker with {@code Type: ajp} and
+ * URI {@code ajp://
Passes if the balancer's MCMP INFO shows {@code ajp://} scheme and the + * proxied request returns HTTP 200.
+ */ + @Test + public void testTrafficFlowsThroughAjp(final TestCluster cluster, + final HttpClient httpClient) throws Exception { + cluster.startWorkers(1); + WildFlyWorker worker = cluster.getWorker1(); + + worker.undertow().addAjpListener(AJP_LISTENER, "default-server", AJP_SOCKET_BINDING, AJP_PORT); + worker.reload(); + worker.modCluster().setListener(AJP_LISTENER); + + log.info("Waiting for worker to re-register with AJP scheme on balancer"); + await().atMost(TestTimeouts.CLUSTER_FORMATION) + .pollInterval(ofSeconds(5)) + .untilAsserted(() -> { + MapThe mod_cluster subsystem advertises one Undertow listener to the balancer + * via MCMP CONFIG messages. By default this is {@code "default"} (the HTTP listener + * on port 8080). Changing it to an AJP listener causes the worker to register with + * {@code Type: ajp} and the balancer to proxy traffic via {@code mod_proxy_ajp} + * instead of {@code mod_proxy_http}.
+ * + *The AJP listener must already exist on the worker + * (see {@link WildFlyUndertowManager#addAjpListener(String, String, String)}). + * This method triggers a server reload to apply the change.
+ * + * @param listenerName the Undertow listener name (e.g., {@code "default"} for HTTP, + * or {@code "ajp"} for an AJP listener) + * @throws IOException if there's a connection error + * @throws OperationException if the management operation fails + * @see WildFlyUndertowManager#addAjpListener(String, String, String, int) + */ + public void setListener(String listenerName) throws IOException, OperationException { + writeModClusterAttribute("listener", listenerName); + log.info("Set mod_cluster listener to '{}' on worker '{}'", listenerName, container.getName()); + } + /** * Set the balancer name this worker registers under on the balancer. * Controls which load-balancing group the worker belongs to. diff --git a/src/test/java/org/jboss/modcluster/test/utils/WildFlyUndertowManager.java b/src/test/java/org/jboss/modcluster/test/utils/WildFlyUndertowManager.java index a469160..3667f80 100644 --- a/src/test/java/org/jboss/modcluster/test/utils/WildFlyUndertowManager.java +++ b/src/test/java/org/jboss/modcluster/test/utils/WildFlyUndertowManager.java @@ -125,18 +125,41 @@ public void setHttpListenerEnableHttp2(final String serverName, final String lis /** * Add an AJP listener to a given Undertow server. * - * @param listenerName name of the AJP listener - * @param serverName name of the Undertow server to add the listener to - * @param socketBindingName name of the socket binding to use + *Creates a socket binding on the given port (if it does not already exist), + * then adds an AJP listener bound to it on the specified server (if it does not + * already exist).
+ * + *Idempotent — safe to call multiple times; existing resources are skipped. + * Does not reload the server; call {@link WildFlyWorker#reload()} after all + * listeners have been added.
+ * + * @param listenerName name of the AJP listener (e.g., {@code "ajp"}) + * @param serverName the Undertow server to add the listener to (e.g., {@code "default-server"}) + * @param socketBindingName name of the socket binding (e.g., {@code "ajp"}) + * @param port the port for the socket binding (e.g., 8009) * @throws Exception if the management operation fails + * @see WildFlyModClusterManager#setListener(String) */ public void addAjpListener(final String listenerName, final String serverName, - final String socketBindingName) throws Exception { - OnlineManagementClient client = container.getManagementClient(); + final String socketBindingName, final int port) throws Exception { + Operations ops = container.getOperations(); - client.apply(new AddUndertowListener.AjpBuilder(listenerName, serverName, socketBindingName) - .build()); - log.info("Added AJP listener '{}' on server '{}' with socket binding '{}' on worker '{}'", - listenerName, serverName, socketBindingName, container.getName()); + Address sbAddr = Address.of("socket-binding-group", "standard-sockets") + .and("socket-binding", socketBindingName); + if (!ops.exists(sbAddr)) { + addSocketBinding(socketBindingName, port); + } + + Address listenerAddr = Address.subsystem("undertow") + .and("server", serverName) + .and("ajp-listener", listenerName); + if (!ops.exists(listenerAddr)) { + OnlineManagementClient client = container.getManagementClient(); + client.apply(new AddUndertowListener.AjpBuilder(listenerName, serverName, socketBindingName) + .build()); + } + + log.info("AJP listener '{}' on port {} on server '{}' added on worker '{}'", + listenerName, port, serverName, container.getName()); } }