This is an automated email from the ASF dual-hosted git repository.

oscerd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 097ee856bbda CAMEL-24742: camel-opa - compile the WASM test fixtures 
instead of committing them (#26677)
097ee856bbda is described below

commit 097ee856bbdab4451aef158e13044858b7839e66
Author: Andrea Cosentino <[email protected]>
AuthorDate: Fri Sep 25 11:05:45 2026 +0200

    CAMEL-24742: camel-opa - compile the WASM test fixtures instead of 
committing them (#26677)
    
    * CAMEL-24742: camel-opa - compile the WASM fixtures instead of committing 
them
    
    authz.wasm was a compiled binary committed beside authz.rego with nothing
    tying the two together, and they came apart: the module predated the second
    decision rule in the Rego, so the WASM suite asserted that input was 
undefined
    while OpaIT asserted it returns deny reasons - about the same source file, 
both
    green. A suite whose purpose is "both engines decide the same way" was
    comparing two different policies. A compiled binary also has no place in a
    source release.
    
    camel-test-infra-opa gains OpaWasmBundleBuilder, a one-shot container run of
    opa build -t wasm using the image already pinned in container.properties, so
    the compiler and the server the REST ITs talk to cannot disagree about their
    OPA version - they did, as it happens: the fixtures were built with 1.9.0 
while
    the module pins 1.20.2.
    
    OpaWasmIT compiles the very authz.rego OpaIT uploads to a real server, so 
the
    parity claim is tested rather than asserted. Deleting the mallory branch 
from
    that Rego now fails the WASM test; before this it changed nothing.
    
    The ten tests that evaluate a policy move to ITs as a result. The two that
    reject a configuration before any bundle is loaded stay unit tests, needing
    neither an artifact nor a container.
    
    One trap worth recording: opa build cannot write into the working directory
    Testcontainers creates, because the image runs as a non-root user and the
    directory is root-owned - so output goes to /tmp via -o. It is invisible to 
a
    manual docker run, where a bind-mounted host directory is writable, and the
    failure surfaces only as "container did not start correctly" unless the
    container's own stderr is attached to the exception, which the builder now
    does.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    * CAMEL-24742: camel-opa - restore what the move to an IT quietly dropped
    
    Three assertions went missing converting the WASM tests, two spotted in 
review
    and one by diffing assertion counts per method against the deleted class:
    
    - keepsTheEntrypointAcrossPooledReuse lost containsEntry("allow", true). The
      type check alone passes for any rule returning an object, so the pair is 
what
      pins the entrypoint rather than its shape.
    - keepsThePoolUsableAfterRepeatedEvaluationFailures lost @Timeout(60), 
which is
      the one test where a mishandled permit wedges template.request() and hangs
      the run with no indication of where.
    - acceptsTheBundleTarballOpaBuildActuallyEmits disappeared entirely. That is
      the worst of the three: every bundle in the IT is a tarball, so the bare
      .wasm branch of loadPolicy had silently lost all coverage. It is back as
      acceptsABareModuleAsWellAsTheBundleTarball, extracting policy.wasm from 
the
      compiled bundle rather than committing one.
    
    Also name the resource when getResourceAsStream returns null; the class 
loads
    four of them and an NPE inside readAllBytes says which of them none.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    * CAMEL-24742: camel-opa - run the bundle-builder test under failsafe, not 
surefire
    
    OpaWasmBundleBuilderTest needs a container but ran under surefire, so a 
plain
    mvn install of camel-test-infra-opa failed for anyone without Docker. The
    @DisabledIfSystemProperty(skipITs) guard only fires when that property is
    passed explicitly, which is not the default path.
    
    That is the same thing this PR exists to fix, made one module over: the
    camel-opa WASM tests moved to ITs so a plain build needs no Docker, and 
then a
    Docker-dependent test arrived in test-infra as a unit test.
    
    Renamed to OpaWasmBundleBuilderIT with the failsafe profile modelled on
    camel-test-infra-jaeger. Verified both ways after a clean, since a stale 
class
    left in target by the rename masked it on the first attempt: mvn clean test
    runs nothing and needs no Docker, mvn verify -Dopa-it-test runs all five.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    * CAMEL-24742: use assertTrue for a boolean assertion in the bundle-builder 
IT
    
    Addresses review feedback on #26677.
    
    reportsWhatFailedWhenTheRegoIsNotValid used assertEquals(true, ...) where
    every other assertion in the file uses assertTrue. It was the only
    assertEquals in the class, so the import goes with it.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    * CAMEL-24742: move the wasm-mode validation tests to the IT that builds a 
bundle
    
    Addresses review feedback on #26677.
    
    CAMEL-24743 merged OpaWasmModeValidationTest to main after this branch
    was cut. Both of its tests start a route on classpath:authz.wasm, the
    committed fixture this PR deletes, so the merged result failed:
    
      FailedToStartRouteException: Failed to start route: route2 because:
      java.io.FileNotFoundException: Cannot find resource: classpath:authz.wasm
    
    Rebasing alone would only have moved that failure into the branch, which
    it did: cherry-picking onto current main reproduced it locally, 2 tests,
    2 errors.
    
    registersTheCheckForTheRestRouteButNotTheWasmRoute and
    acceptsFailOpenInWasmMode now live in OpaWasmIT, where compileBundles
    produces a bundle that actually loads. Neither asserts anything about
    wasm evaluation - one counts producer health checks, the other that a
    failOpen route starts - so they sit with the other IT cases. The cost is
    that they no longer run in a plain build; with the committed fixtures
    gone there is no bundle to start a wasm route from without Docker.
    
    OpaWasmIT had no createRouteBuilder of its own - it drives producers
    through template.request with explicit URIs - so the health-check test
    needed one adding for the rest/wasm pair it compares.
    
    The relocated assertion still guards what it did before: disabling the
    wasm-mode skip in OpaProducer reddens it alone, "Expected size: 1 but
    was: 2". 79 unit tests and 12 ITs green.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    ---------
    
    Signed-off-by: Andrea Cosentino <[email protected]>
    Co-authored-by: Claude Opus 5 <[email protected]>
---
 .../component/opa/OpaWasmConfigurationTest.java    |  52 ++++
 .../camel/component/opa/OpaWasmEvaluatorTest.java  | 205 --------------
 .../org/apache/camel/component/opa/OpaWasmIT.java  | 298 +++++++++++++++++++++
 .../component/opa/OpaWasmModeValidationTest.java   |  80 ------
 .../src/test/resources/README-wasm-fixtures.md     |  28 --
 .../src/test/resources/authz-bundle.tar.gz         | Bin 57234 -> 0 bytes
 components/camel-opa/src/test/resources/authz.wasm | Bin 140391 -> 0 bytes
 .../src/test/resources/roles-bundle.tar.gz         | Bin 56415 -> 0 bytes
 test-infra/camel-test-infra-opa/pom.xml            |  30 +++
 .../infra/opa/services/OpaWasmBundleBuilder.java   | 142 ++++++++++
 .../test/infra/opa/OpaWasmBundleBuilderIT.java     | 106 ++++++++
 11 files changed, 628 insertions(+), 313 deletions(-)

diff --git 
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaWasmConfigurationTest.java
 
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaWasmConfigurationTest.java
new file mode 100644
index 000000000000..660a09ac04c2
--- /dev/null
+++ 
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaWasmConfigurationTest.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.camel.component.opa;
+
+import org.apache.camel.ResolveEndpointFailedException;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * The {@code wasm} checks that reject a configuration before any bundle is 
loaded.
+ * <p/>
+ * These stay unit tests deliberately: they are the only two that never reach 
{@code loadPolicy}, so they need no
+ * compiled artifact and no container. Everything that evaluates a policy 
lives in {@link OpaWasmIT}, where the bundle
+ * is compiled from the Rego under test rather than committed beside it.
+ */
+public class OpaWasmConfigurationTest extends CamelTestSupport {
+
+    @Test
+    void requiresAPolicyBundle() {
+        // the check runs when the endpoint starts, so a misconfiguration 
fails fast rather than once per message
+        assertThatThrownBy(() -> 
template.request("opa:authz/allow?evaluationMode=wasm", e -> {
+        }))
+                .isInstanceOf(ResolveEndpointFailedException.class)
+                .hasMessageContaining("policyBundle is required");
+    }
+
+    @Test
+    void rejectsAPoolSizeBelowOne() {
+        // rejected before the bundle is resolved, so the location here is 
never opened
+        assertThatThrownBy(() -> template.request(
+                
"opa:authz/allow?evaluationMode=wasm&policyBundle=file:unused.wasm&poolSize=0", 
e -> {
+                }))
+                .isInstanceOf(ResolveEndpointFailedException.class)
+                .hasMessageContaining("poolSize must be at least 1");
+    }
+}
diff --git 
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaWasmEvaluatorTest.java
 
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaWasmEvaluatorTest.java
deleted file mode 100644
index 06d4e8ea49f7..000000000000
--- 
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaWasmEvaluatorTest.java
+++ /dev/null
@@ -1,205 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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.apache.camel.component.opa;
-
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
-
-import org.apache.camel.Exchange;
-import org.apache.camel.ResolveEndpointFailedException;
-import org.apache.camel.test.junit6.CamelTestSupport;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.Timeout;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-
-/**
- * In-process evaluation of a bundle produced by {@code opa build -t wasm}.
- * <p/>
- * The bundle here was compiled from the same {@code authz.rego} the REST 
tests use, so the assertions double as a check
- * that a route sees the same decision whichever engine evaluated it.
- */
-public class OpaWasmEvaluatorTest extends CamelTestSupport {
-
-    private static final String WASM = 
"opa:authz/allow?evaluationMode=wasm&policyBundle=classpath:authz.wasm";
-
-    @Test
-    void allowsWhenThePolicyMatches() {
-        Exchange out = template.request(WASM, e -> 
e.getMessage().setHeader("user", "alice"));
-
-        assertThat(out.getException()).isNull();
-        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_ALLOW)).isEqualTo(true);
-    }
-
-    @Test
-    void deniesWhenThePolicyDoesNotMatch() {
-        Exchange out = template.request(WASM, e -> 
e.getMessage().setHeader("user", "mallory"));
-
-        assertThat(out.getException()).isNull();
-        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_ALLOW)).isEqualTo(false);
-    }
-
-    @Test
-    void readsAVerdictOutOfADecisionObject() {
-        Exchange out = template.request(
-                
"opa:authz/decision?evaluationMode=wasm&policyBundle=classpath:authz.wasm",
-                e -> e.getMessage().setHeader("user", "alice"));
-
-        assertThat(out.getException()).isNull();
-        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_ALLOW)).isEqualTo(true);
-        assertThat(out.getMessage().getHeader(OpaConstants.DECISION, 
Map.class)).containsEntry("allow", true);
-    }
-
-    @Test
-    void keepsTheDenyReasonsJustLikeTheRestEngine() {
-        Exchange out = template.request(
-                
"opa:authz/decision?evaluationMode=wasm&policyBundle=classpath:authz.wasm",
-                e -> e.getMessage().setHeader("user", "mallory"));
-
-        assertThat(out.getException()).isNull();
-        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_ALLOW)).isEqualTo(false);
-        assertThat(out.getMessage().getHeader(OpaConstants.DECISION, 
Map.class))
-                .containsEntry("reasons", List.of("not the owner"));
-    }
-
-    @Test
-    void keepsTheEntrypointAcrossPooledReuse() {
-        // Returning a borrowed OpaPolicy resets it, and a reset puts the 
entrypoint back to 0 - so an instance
-        // configured only where it was built answers the first exchange from 
authz/decision and every later one
-        // from whatever rule happens to be entrypoint 0 (here authz/allow, a 
bare boolean). A single-instance
-        // pool and more than one message is what makes that visible.
-        String decision = 
"opa:authz/decision?evaluationMode=wasm&policyBundle=classpath:authz.wasm&poolSize=1";
-
-        for (int i = 0; i < 5; i++) {
-            Exchange out = template.request(decision, e -> 
e.getMessage().setHeader("user", "alice"));
-
-            assertThat(out.getException()).as("exchange %d", i).isNull();
-            assertThat(out.getMessage().getHeader(OpaConstants.DECISION))
-                    .as("message %d was still decided by authz/decision", i)
-                    .isInstanceOf(Map.class);
-            assertThat(out.getMessage().getHeader(OpaConstants.DECISION, 
Map.class)).containsEntry("allow", true);
-        }
-    }
-
-    @Test
-    void appliesTheDataDocumentPackedInTheBundle() {
-        // roles.rego decides from data.admins, which opa build packs into the 
bundle as data.json rather than
-        // into the module. A reset clears the data as well as the entrypoint, 
so it too has to be re-applied on
-        // every borrow - without it the policy sees an empty data document 
and denies everyone.
-        String roles = 
"opa:roles/allow?evaluationMode=wasm&policyBundle=classpath:roles-bundle.tar.gz&poolSize=1";
-
-        for (int i = 0; i < 3; i++) {
-            Exchange allowed = template.request(roles, e -> 
e.getMessage().setHeader("user", "carol"));
-            Exchange denied = template.request(roles, e -> 
e.getMessage().setHeader("user", "alice"));
-
-            assertThat(allowed.getException()).as("exchange %d", i).isNull();
-            
assertThat(allowed.getMessage().getHeader(OpaConstants.DECISION_ALLOW))
-                    .as("data.admins was still visible on message %d", i)
-                    .isEqualTo(true);
-            
assertThat(denied.getMessage().getHeader(OpaConstants.DECISION_ALLOW)).isEqualTo(false);
-        }
-    }
-
-    @Test
-    void failsClosedOnAnUndefinedDecisionJustLikeTheRestEngine() {
-        // authz/strict_allow has no default, so for mallory the rule is 
undefined. The WASM ABI returns an
-        // empty array where the REST client raises an error; both must reach 
the route the same way.
-        Exchange out = template.request(
-                
"opa:authz/strict_allow?evaluationMode=wasm&policyBundle=classpath:authz.wasm",
-                e -> e.getMessage().setHeader("user", "mallory"));
-
-        
assertThat(out.getException()).isInstanceOf(OpaPolicyEvaluationException.class);
-        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_ALLOW)).isNull();
-    }
-
-    @Test
-    void acceptsTheBundleTarballOpaBuildActuallyEmits() {
-        Exchange out = template.request(
-                
"opa:authz/allow?evaluationMode=wasm&policyBundle=classpath:authz-bundle.tar.gz",
-                e -> e.getMessage().setHeader("user", "alice"));
-
-        assertThat(out.getException()).isNull();
-        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_ALLOW)).isEqualTo(true);
-    }
-
-    @Test
-    void requiresAPolicyBundle() {
-        // the check runs when the endpoint starts, so a misconfiguration 
fails fast rather than once per message
-        assertThatThrownBy(() -> 
template.request("opa:authz/allow?evaluationMode=wasm", e -> {
-        }))
-                .isInstanceOf(ResolveEndpointFailedException.class)
-                .hasMessageContaining("policyBundle is required");
-    }
-
-    @Test
-    void rejectsAPoolSizeBelowOne() {
-        // the pool rejects it as well, but as "maxSize must be positive" - 
its own parameter rather than the
-        // option that was set, which is what the operator has to go looking 
for
-        assertThatThrownBy(() -> template.request(
-                
"opa:authz/allow?evaluationMode=wasm&policyBundle=classpath:authz.wasm&poolSize=0",
 e -> {
-                }))
-                .isInstanceOf(ResolveEndpointFailedException.class)
-                .hasMessageContaining("poolSize must be at least 1");
-    }
-
-    @Test
-    @Timeout(60)
-    void keepsThePoolUsableAfterRepeatedEvaluationFailures() {
-        // a failed evaluation discards its instance. Mishandle the pool's 
permit while doing so and a
-        // single-instance pool either wedges on the next borrow or quietly 
stops bounding anything - neither of
-        // which a single failing exchange would show.
-        String strict = 
"opa:authz/strict_allow?evaluationMode=wasm&policyBundle=classpath:authz.wasm&poolSize=1";
-
-        for (int i = 0; i < 5; i++) {
-            Exchange failed = template.request(strict, e -> 
e.getMessage().setHeader("user", "mallory"));
-            assertThat(failed.getException()).as("failure %d", 
i).isInstanceOf(OpaPolicyEvaluationException.class);
-        }
-
-        Exchange out = template.request(strict, e -> 
e.getMessage().setHeader("user", "alice"));
-
-        assertThat(out.getException()).isNull();
-        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_ALLOW)).isEqualTo(true);
-    }
-
-    @Test
-    void evaluatesCorrectlyFromManyThreadsAtOnce() throws Exception {
-        // OpaPolicy is not thread-safe; without pooling a concurrent route 
would interleave input and data
-        int threads = 16;
-        ExecutorService pool = Executors.newFixedThreadPool(threads);
-        try {
-            var tasks = IntStream.range(0, threads * 8).mapToObj(i -> 
(Callable<Boolean>) () -> {
-                String user = i % 2 == 0 ? "alice" : "mallory";
-                Exchange out = template.request(WASM, e -> 
e.getMessage().setHeader("user", user));
-                assertThat(out.getException()).isNull();
-                return Boolean.valueOf("alice".equals(user))
-                        
.equals(out.getMessage().getHeader(OpaConstants.DECISION_ALLOW));
-            }).collect(Collectors.toList());
-
-            for (var future : pool.invokeAll(tasks)) {
-                assertThat(future.get()).as("verdict matched the user on every 
thread").isTrue();
-            }
-        } finally {
-            pool.shutdownNow();
-        }
-    }
-}
diff --git 
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaWasmIT.java
 
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaWasmIT.java
new file mode 100644
index 000000000000..45d7cf211232
--- /dev/null
+++ 
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaWasmIT.java
@@ -0,0 +1,298 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.camel.component.opa;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.health.HealthCheck;
+import org.apache.camel.health.HealthCheckHelper;
+import org.apache.camel.health.WritableHealthCheckRepository;
+import org.apache.camel.test.infra.opa.services.OpaWasmBundleBuilder;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+/**
+ * In-process evaluation of a bundle compiled from the very {@code authz.rego} 
that {@link OpaIT} uploads to a real OPA
+ * server.
+ * <p/>
+ * Sharing one policy between the two classes is the point rather than a 
convenience: the component promises that a
+ * route sees the same decision whichever engine evaluated it, and that 
promise is only tested if both engines are asked
+ * about the same rules. It also closes the way that promise was broken before 
- a compiled bundle committed beside the
+ * Rego drifted from it, and the two suites asserted opposite things about 
{@code authz/decision} while both stayed
+ * green (CAMEL-24741). Nothing is committed now; the bundle is built from the 
policy under test.
+ */
+public class OpaWasmIT extends CamelTestSupport {
+
+    @TempDir
+    static Path bundles;
+
+    private static String authz;
+    private static String module;
+    private static String roles;
+
+    private static String resource(String name) throws Exception {
+        try (InputStream in = OpaWasmIT.class.getResourceAsStream(name)) {
+            if (in == null) {
+                throw new IllegalStateException("Test resource not found on 
the classpath: " + name);
+            }
+            return new String(in.readAllBytes(), StandardCharsets.UTF_8);
+        }
+    }
+
+    @BeforeAll
+    static void compileBundles() throws Exception {
+        byte[] authzBundle = OpaWasmBundleBuilder.build(
+                "authz.rego", resource("/authz.rego"),
+                "authz/allow", "authz/decision", "authz/strict_allow");
+        authz = write("authz-bundle.tar.gz", authzBundle);
+        module = extractModule(authzBundle);
+
+        // roles.rego decides from data.admins, which opa build packs beside 
it as data.json
+        byte[] rolesBundle = OpaWasmBundleBuilder.build(
+                Map.of("roles.rego", 
resource("/wasm-data/roles.rego").getBytes(StandardCharsets.UTF_8),
+                        "data.json", 
resource("/wasm-data/data.json").getBytes(StandardCharsets.UTF_8)),
+                "roles/allow");
+        roles = write("roles-bundle.tar.gz", rolesBundle);
+    }
+
+    /** The /policy.wasm inside a bundle, so the bare-module branch of 
loadPolicy keeps its coverage. */
+    private static String extractModule(byte[] bundle) throws Exception {
+        try (TarArchiveInputStream tar
+                = new TarArchiveInputStream(new GzipCompressorInputStream(new 
ByteArrayInputStream(bundle)))) {
+            TarArchiveEntry entry;
+            while ((entry = tar.getNextEntry()) != null) {
+                if (!entry.isDirectory() && 
entry.getName().endsWith("policy.wasm")) {
+                    return write("authz.wasm", tar.readAllBytes());
+                }
+            }
+        }
+        throw new IllegalStateException("opa build emitted no policy.wasm");
+    }
+
+    private static String write(String name, byte[] bundle) throws Exception {
+        Path path = bundles.resolve(name);
+        Files.write(path, bundle);
+        return "file:" + path.toAbsolutePath();
+    }
+
+    private String wasm(String policyPath) {
+        return "opa:" + policyPath + "?evaluationMode=wasm&policyBundle=" + 
authz;
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                // a rest-mode route registers a producer readiness check 
(positive control), a wasm-mode route
+                // sharing the same policy path must not - the difference is 
exactly what the health-check test
+                // asserts. These moved here from OpaWasmModeValidationTest 
when the committed authz.wasm went
+                // away (CAMEL-24742): both routes have to start, so both need 
a bundle that actually loads.
+                
from("direct:rest").to("opa:authz/allow?serverUrl=http://opa-rest:8181";);
+                from("direct:wasm").to(wasm("authz/allow"));
+            }
+        };
+    }
+
+    private List<HealthCheck> producerChecks() {
+        WritableHealthCheckRepository repository = 
HealthCheckHelper.getHealthCheckRepository(
+                context, "producers", WritableHealthCheckRepository.class);
+        assertThat(repository).isNotNull();
+        // producer health checks are disabled globally by default, so enable 
the repository to read them back
+        repository.setEnabled(true);
+        return repository.stream().toList();
+    }
+
+    /**
+     * In {@code wasm} mode the policy is evaluated in-process, so there is no 
OPA server to probe and no producer
+     * health check is registered (CAMEL-24743).
+     */
+    @Test
+    void registersTheCheckForTheRestRouteButNotTheWasmRoute() {
+        List<HealthCheck> checks = producerChecks();
+        // exactly one check, and it is the rest route's - the wasm route 
evaluates in-process with no server to probe
+        assertThat(checks).hasSize(1);
+        assertThat(checks.get(0).getId()).contains("opa-rest");
+    }
+
+    /**
+     * {@code failOpen} still governs an evaluation failure (a busy pool, a 
bad bundle) in {@code wasm} mode, so it must
+     * not be rejected (CAMEL-24743).
+     */
+    @Test
+    void acceptsFailOpenInWasmMode() {
+        assertThatCode(() -> context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:failopen").to(wasm("authz/allow") + 
"&failOpen=true");
+            }
+        })).doesNotThrowAnyException();
+    }
+
+    @Test
+    void allowsWhenThePolicyMatches() {
+        Exchange out = template.request(wasm("authz/allow"), e -> 
e.getMessage().setHeader("user", "alice"));
+
+        assertThat(out.getException()).isNull();
+        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_ALLOW)).isEqualTo(true);
+    }
+
+    @Test
+    void acceptsABareModuleAsWellAsTheBundleTarball() {
+        // every other test here loads the tarball opa build emits, so without 
this the other half of loadPolicy -
+        // a bare .wasm, which is what an operator extracting the module by 
hand would have - goes untested
+        Exchange out = template.request(
+                "opa:authz/allow?evaluationMode=wasm&policyBundle=" + module,
+                e -> e.getMessage().setHeader("user", "alice"));
+
+        assertThat(out.getException()).isNull();
+        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_ALLOW)).isEqualTo(true);
+    }
+
+    @Test
+    void deniesWhenThePolicyDoesNotMatch() {
+        Exchange out = template.request(wasm("authz/allow"), e -> 
e.getMessage().setHeader("user", "mallory"));
+
+        assertThat(out.getException()).isNull();
+        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_ALLOW)).isEqualTo(false);
+    }
+
+    @Test
+    void readsAVerdictOutOfADecisionObject() {
+        Exchange out = template.request(wasm("authz/decision"), e -> 
e.getMessage().setHeader("user", "alice"));
+
+        assertThat(out.getException()).isNull();
+        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_ALLOW)).isEqualTo(true);
+        assertThat(out.getMessage().getHeader(OpaConstants.DECISION, 
Map.class)).containsEntry("allow", true);
+    }
+
+    @Test
+    void keepsTheDenyReasonsJustLikeTheRestEngine() {
+        // OpaIT.keepsTheDenyReasonsFromADecisionObject asserts exactly this 
against the server
+        Exchange out = template.request(wasm("authz/decision"), e -> 
e.getMessage().setHeader("user", "mallory"));
+
+        assertThat(out.getException()).isNull();
+        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_ALLOW)).isEqualTo(false);
+        assertThat(out.getMessage().getHeader(OpaConstants.DECISION, 
Map.class))
+                .containsEntry("reasons", List.of("not the owner"));
+    }
+
+    @Test
+    void failsClosedOnAnUndefinedDecisionJustLikeTheRestEngine() {
+        // authz/strict_allow has no default, so it is undefined for mallory. 
The WASM ABI reports that as an empty
+        // result array where the REST client raises an error; 
OpaIT.failsClosedOnAnUndefinedDecision is the twin
+        Exchange out = template.request(wasm("authz/strict_allow"), e -> 
e.getMessage().setHeader("user", "mallory"));
+
+        
assertThat(out.getException()).isInstanceOf(OpaPolicyEvaluationException.class);
+        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_ALLOW)).isNull();
+    }
+
+    @Test
+    void keepsTheEntrypointAcrossPooledReuse() {
+        // returning a borrowed instance resets it, putting the entrypoint 
back to 0 - so an instance configured
+        // only where it was built answers the first exchange from 
authz/decision and every later one from
+        // whatever rule is entrypoint 0. A single-instance pool and several 
messages is what shows it
+        String decision = wasm("authz/decision") + "&poolSize=1";
+
+        for (int i = 0; i < 5; i++) {
+            Exchange out = template.request(decision, e -> 
e.getMessage().setHeader("user", "alice"));
+
+            assertThat(out.getException()).as("exchange %d", i).isNull();
+            assertThat(out.getMessage().getHeader(OpaConstants.DECISION))
+                    .as("message %d was still decided by authz/decision", i)
+                    .isInstanceOf(Map.class);
+            // the type alone would pass for any rule returning an object; the 
content is what pins the entrypoint
+            assertThat(out.getMessage().getHeader(OpaConstants.DECISION, 
Map.class)).containsEntry("allow", true);
+        }
+    }
+
+    @Test
+    void appliesTheDataDocumentPackedInTheBundle() {
+        String policy = "opa:roles/allow?evaluationMode=wasm&policyBundle=" + 
roles + "&poolSize=1";
+
+        for (int i = 0; i < 3; i++) {
+            Exchange allowed = template.request(policy, e -> 
e.getMessage().setHeader("user", "carol"));
+            Exchange denied = template.request(policy, e -> 
e.getMessage().setHeader("user", "alice"));
+
+            assertThat(allowed.getException()).as("exchange %d", i).isNull();
+            
assertThat(allowed.getMessage().getHeader(OpaConstants.DECISION_ALLOW))
+                    .as("data.admins was still visible on message %d", i)
+                    .isEqualTo(true);
+            
assertThat(denied.getMessage().getHeader(OpaConstants.DECISION_ALLOW)).isEqualTo(false);
+        }
+    }
+
+    @Test
+    @Timeout(60)
+    void keepsThePoolUsableAfterRepeatedEvaluationFailures() {
+        String strict = wasm("authz/strict_allow") + "&poolSize=1";
+
+        for (int i = 0; i < 5; i++) {
+            Exchange failed = template.request(strict, e -> 
e.getMessage().setHeader("user", "mallory"));
+            assertThat(failed.getException()).as("failure %d", 
i).isInstanceOf(OpaPolicyEvaluationException.class);
+        }
+
+        Exchange out = template.request(strict, e -> 
e.getMessage().setHeader("user", "alice"));
+
+        assertThat(out.getException()).isNull();
+        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_ALLOW)).isEqualTo(true);
+    }
+
+    @Test
+    void evaluatesCorrectlyFromManyThreadsAtOnce() throws Exception {
+        // OpaPolicy is not thread-safe; without pooling a concurrent route 
would interleave input and data
+        int threads = 16;
+        ExecutorService pool = Executors.newFixedThreadPool(threads);
+        try {
+            var tasks = IntStream.range(0, threads * 8).mapToObj(i -> 
(Callable<Boolean>) () -> {
+                String user = i % 2 == 0 ? "alice" : "mallory";
+                Exchange out = template.request(wasm("authz/allow"), e -> 
e.getMessage().setHeader("user", user));
+                assertThat(out.getException()).isNull();
+                return Boolean.valueOf("alice".equals(user))
+                        
.equals(out.getMessage().getHeader(OpaConstants.DECISION_ALLOW));
+            }).collect(Collectors.toList());
+
+            for (var future : pool.invokeAll(tasks)) {
+                assertThat(future.get()).as("verdict matched the user on every 
thread").isTrue();
+            }
+        } finally {
+            pool.shutdownNow();
+        }
+    }
+}
diff --git 
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaWasmModeValidationTest.java
 
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaWasmModeValidationTest.java
deleted file mode 100644
index e4661972652d..000000000000
--- 
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaWasmModeValidationTest.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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.apache.camel.component.opa;
-
-import java.util.List;
-
-import org.apache.camel.builder.RouteBuilder;
-import org.apache.camel.health.HealthCheck;
-import org.apache.camel.health.HealthCheckHelper;
-import org.apache.camel.health.WritableHealthCheckRepository;
-import org.apache.camel.test.junit6.CamelTestSupport;
-import org.junit.jupiter.api.Test;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatCode;
-
-/**
- * In {@code wasm} mode the policy is evaluated in-process, so there is no OPA 
server to probe and no producer health
- * check is registered. {@code failOpen}, on the other hand, still governs an 
evaluation failure (a busy pool, a bad
- * bundle) in {@code wasm} mode too, so it must not be rejected (CAMEL-24743).
- */
-public class OpaWasmModeValidationTest extends CamelTestSupport {
-
-    @Override
-    protected RouteBuilder createRouteBuilder() {
-        return new RouteBuilder() {
-            @Override
-            public void configure() {
-                // a rest-mode route registers a producer readiness check 
(positive control), a wasm-mode route
-                // sharing the same policy path must not - the difference is 
exactly what this test asserts
-                
from("direct:rest").to("opa:authz/allow?serverUrl=http://opa-rest:8181";);
-                
from("direct:wasm").to("opa:authz/allow?evaluationMode=wasm&policyBundle=classpath:authz.wasm");
-            }
-        };
-    }
-
-    private List<HealthCheck> producerChecks() {
-        WritableHealthCheckRepository repository = 
HealthCheckHelper.getHealthCheckRepository(
-                context, "producers", WritableHealthCheckRepository.class);
-        assertThat(repository).isNotNull();
-        // producer health checks are disabled globally by default, so enable 
the repository to read them back
-        repository.setEnabled(true);
-        return repository.stream().toList();
-    }
-
-    @Test
-    void registersTheCheckForTheRestRouteButNotTheWasmRoute() {
-        List<HealthCheck> checks = producerChecks();
-        // exactly one check, and it is the rest route's - the wasm route 
evaluates in-process with no server to probe
-        assertThat(checks).hasSize(1);
-        assertThat(checks.get(0).getId()).contains("opa-rest");
-    }
-
-    @Test
-    void acceptsFailOpenInWasmMode() {
-        // failOpen governs an evaluation failure (a busy pool, a bad bundle), 
which happens in wasm too, so it is a
-        // valid option here and starting the route must not throw
-        assertThatCode(() -> context.addRoutes(new RouteBuilder() {
-            @Override
-            public void configure() {
-                from("direct:failopen")
-                        
.to("opa:authz/allow?evaluationMode=wasm&policyBundle=classpath:authz.wasm&failOpen=true");
-            }
-        })).doesNotThrowAnyException();
-    }
-}
diff --git a/components/camel-opa/src/test/resources/README-wasm-fixtures.md 
b/components/camel-opa/src/test/resources/README-wasm-fixtures.md
deleted file mode 100644
index ee0ce62e21c4..000000000000
--- a/components/camel-opa/src/test/resources/README-wasm-fixtures.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# WASM test fixtures
-
-`authz.wasm`, `authz-bundle.tar.gz` and `roles-bundle.tar.gz` are **generated 
artifacts**, compiled from the
-Rego sources next to them. They are committed only so the 
`evaluationMode=wasm` tests can run before
-CAMEL-24742 adds a build step that compiles them, at which point all three 
should be deleted.
-
-Regenerate after any change to the sources — nothing currently checks that 
they agree, which is exactly why
-CAMEL-24742 exists. Use the pinned version below: a different OPA release can 
emit a module built against a
-different WebAssembly ABI than `opa-java-wasm` supports.
-
-```sh
-OPA='docker run --rm -v "$PWD":/w:Z -w /w 
mirror.gcr.io/openpolicyagent/opa:1.9.0-static'
-
-# authz.wasm + authz-bundle.tar.gz, from authz.rego
-eval $OPA build -t wasm -e authz/allow -e authz/decision -e authz/strict_allow 
authz.rego
-tar xzf bundle.tar.gz --wildcards '*policy.wasm' && mv policy.wasm authz.wasm
-mv bundle.tar.gz authz-bundle.tar.gz
-
-# roles-bundle.tar.gz, from wasm-data/ (roles.rego plus the data.json that opa 
build packs beside it)
-(cd wasm-data && eval $OPA build -t wasm -e roles/allow .)
-mv wasm-data/bundle.tar.gz roles-bundle.tar.gz
-```
-
-`-e` names the entrypoints. An entrypoint is fixed at build time and is not 
the same thing as a data path,
-which is why `camel-opa` lets `entrypoint` be set separately from `policyPath`.
-
-`authz.rego` is shared with `OpaIT`, which uploads it to a real OPA server: 
the two engines must decide the
-same way, so a rule added here should be exercised from both test classes.
diff --git a/components/camel-opa/src/test/resources/authz-bundle.tar.gz 
b/components/camel-opa/src/test/resources/authz-bundle.tar.gz
deleted file mode 100644
index 1174e3398077..000000000000
Binary files a/components/camel-opa/src/test/resources/authz-bundle.tar.gz and 
/dev/null differ
diff --git a/components/camel-opa/src/test/resources/authz.wasm 
b/components/camel-opa/src/test/resources/authz.wasm
deleted file mode 100644
index 44d056602484..000000000000
Binary files a/components/camel-opa/src/test/resources/authz.wasm and /dev/null 
differ
diff --git a/components/camel-opa/src/test/resources/roles-bundle.tar.gz 
b/components/camel-opa/src/test/resources/roles-bundle.tar.gz
deleted file mode 100644
index 75ac2992c094..000000000000
Binary files a/components/camel-opa/src/test/resources/roles-bundle.tar.gz and 
/dev/null differ
diff --git a/test-infra/camel-test-infra-opa/pom.xml 
b/test-infra/camel-test-infra-opa/pom.xml
index 8cb5cbab748f..5a9c628cae14 100644
--- a/test-infra/camel-test-infra-opa/pom.xml
+++ b/test-infra/camel-test-infra-opa/pom.xml
@@ -38,4 +38,34 @@
         </dependency>
     </dependencies>
 
+    <profiles>
+        <profile>
+            <id>opa-it-test</id>
+            <activation>
+                <activeByDefault>false</activeByDefault>
+                <property>
+                    <name>opa-it-test</name>
+                </property>
+            </activation>
+            <properties>
+                <skipITs>false</skipITs>
+            </properties>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-failsafe-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <goals>
+                                    <goal>integration-test</goal>
+                                    <goal>verify</goal>
+                                </goals>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
 </project>
diff --git 
a/test-infra/camel-test-infra-opa/src/main/java/org/apache/camel/test/infra/opa/services/OpaWasmBundleBuilder.java
 
b/test-infra/camel-test-infra-opa/src/main/java/org/apache/camel/test/infra/opa/services/OpaWasmBundleBuilder.java
new file mode 100644
index 000000000000..703edffabf11
--- /dev/null
+++ 
b/test-infra/camel-test-infra-opa/src/main/java/org/apache/camel/test/infra/opa/services/OpaWasmBundleBuilder.java
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.camel.test.infra.opa.services;
+
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.test.infra.common.LocalPropertyResolver;
+import org.apache.camel.test.infra.opa.common.OpaProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy;
+import org.testcontainers.images.builder.Transferable;
+
+/**
+ * Compiles Rego into the WebAssembly bundle that {@code evaluationMode=wasm} 
evaluates.
+ * <p/>
+ * This is the other half of what this module is for. The service next door 
runs an OPA server so the REST tests can
+ * talk to it; in-process evaluation has no server to talk to, but the Rego 
still has to be compiled, and
+ * {@code opa build -t wasm} needs the OPA binary. Rather than commit the 
compiled artifact - which drifts silently
+ * against the {@code .rego} beside it, and has no business in a source 
release - a test asks for a bundle and gets one
+ * built from the policy it is actually asserting on.
+ * <p/>
+ * The image is the one already pinned in {@code container.properties}, so the 
compiler and the server the REST tests
+ * use never disagree about their OPA version.
+ */
+public final class OpaWasmBundleBuilder {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(OpaWasmBundleBuilder.class);
+    private static final String WORK_DIR = "/policy";
+    // the sources go to a directory the working-directory setting creates for 
us, but the bundle cannot be
+    // written there: that directory ends up owned by root while the OPA image 
runs as a non-root user, so
+    // "opa build" fails with "open bundle.tar.gz: permission denied". /tmp is 
writable, so -o points there.
+    private static final String BUNDLE = "/tmp/bundle.tar.gz";
+
+    private OpaWasmBundleBuilder() {
+    }
+
+    /**
+     * Compiles a single Rego policy.
+     *
+     * @param  fileName    the name to give the policy inside the build, for 
example {@code authz.rego}
+     * @param  rego        the policy source
+     * @param  entrypoints the rules to expose, as {@code opa build -e} names 
them - an entrypoint is fixed at build
+     *                     time and is not the same thing as a data path
+     * @return             the {@code bundle.tar.gz} {@code opa build} 
emitted, holding {@code /policy.wasm}
+     */
+    public static byte[] build(String fileName, String rego, String... 
entrypoints) {
+        return build(Map.of(fileName, rego.getBytes(StandardCharsets.UTF_8)), 
entrypoints);
+    }
+
+    /**
+     * Compiles a set of sources, so a policy that reads {@code data.*} can be 
built together with the {@code data.json}
+     * that {@code opa build} packs beside it.
+     *
+     * @param  sources     file name to content, for example {@code 
roles.rego} and {@code data.json}
+     * @param  entrypoints the rules to expose
+     * @return             the {@code bundle.tar.gz} {@code opa build} emitted
+     */
+    public static byte[] build(Map<String, byte[]> sources, String... 
entrypoints) {
+        if (sources == null || sources.isEmpty()) {
+            throw new IllegalArgumentException("At least one source is 
required to build a bundle");
+        }
+        if (entrypoints == null || entrypoints.length == 0) {
+            throw new IllegalArgumentException(
+                    "At least one entrypoint is required; opa build -t wasm 
emits nothing callable without one");
+        }
+
+        String image = 
LocalPropertyResolver.getProperty(OpaLocalContainerInfraService.class, 
OpaProperties.OPA_CONTAINER);
+        LOG.info("Compiling {} to WebAssembly with {}", sources.keySet(), 
image);
+
+        Map<String, byte[]> ordered = new LinkedHashMap<>(sources);
+        GenericContainer<?> compiler = compiler(image, ordered, entrypoints);
+        try {
+            compiler.start();
+            // the container has exited by now: OneShotStartupCheckStrategy 
waits for that rather than for a port,
+            // and the bundle is read back out of the stopped container's 
filesystem
+            return compiler.copyFileFromContainer(BUNDLE, 
InputStream::readAllBytes);
+        } catch (Exception e) {
+            // opa build reports what it disliked about the policy on stderr, 
and losing that leaves a caller with
+            // "container did not start correctly", which says nothing about 
their Rego
+            throw new IllegalStateException(
+                    "Could not compile " + sources.keySet() + " to a 
WebAssembly bundle. opa said: " + logsOf(compiler),
+                    e);
+        } finally {
+            compiler.stop();
+        }
+    }
+
+    private static String logsOf(GenericContainer<?> container) {
+        try {
+            String logs = container.getLogs();
+            return logs == null || logs.isBlank() ? "(nothing)" : logs.strip();
+        } catch (Exception e) {
+            return "(logs unavailable: " + e.getMessage() + ")";
+        }
+    }
+
+    @SuppressWarnings("resource")
+    private static GenericContainer<?> compiler(String image, Map<String, 
byte[]> sources, String[] entrypoints) {
+        GenericContainer<?> container = new GenericContainer<>(image) // 
NOSONAR
+                .withWorkingDirectory(WORK_DIR)
+                .withStartupCheckStrategy(new OneShotStartupCheckStrategy());
+
+        for (Map.Entry<String, byte[]> source : sources.entrySet()) {
+            container.withCopyToContainer(
+                    Transferable.of(source.getValue()), WORK_DIR + "/" + 
source.getKey());
+        }
+        return container.withCommand(command(entrypoints));
+    }
+
+    private static String[] command(String[] entrypoints) {
+        // "opa build -t wasm -e <ep> ... ." - building the directory rather 
than naming the files is what makes
+        // opa build pack a data.json sitting beside the policy into the bundle
+        List<String> command = new ArrayList<>(List.of("build", "-t", "wasm", 
"-o", BUNDLE));
+        for (String entrypoint : entrypoints) {
+            command.add("-e");
+            command.add(entrypoint);
+        }
+        command.add(".");
+        return command.toArray(new String[0]);
+    }
+}
diff --git 
a/test-infra/camel-test-infra-opa/src/test/java/org/apache/camel/test/infra/opa/OpaWasmBundleBuilderIT.java
 
b/test-infra/camel-test-infra-opa/src/test/java/org/apache/camel/test/infra/opa/OpaWasmBundleBuilderIT.java
new file mode 100644
index 000000000000..8fb94f3964be
--- /dev/null
+++ 
b/test-infra/camel-test-infra-opa/src/test/java/org/apache/camel/test/infra/opa/OpaWasmBundleBuilderIT.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.camel.test.infra.opa;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.test.infra.opa.services.OpaWasmBundleBuilder;
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class OpaWasmBundleBuilderIT {
+    // an IT rather than a Test on purpose: this needs a container, and the 
convention in this tree is that
+    // anything requiring Docker runs under failsafe so a plain build stays 
green without it
+
+    private static final String REGO = """
+            package authz
+
+            default allow := false
+
+            allow if {
+                input.user == "alice"
+            }
+            """;
+
+    private List<String> entries(byte[] bundle) throws Exception {
+        List<String> names = new ArrayList<>();
+        try (TarArchiveInputStream tar
+                = new TarArchiveInputStream(new GzipCompressorInputStream(new 
ByteArrayInputStream(bundle)))) {
+            TarArchiveEntry entry;
+            while ((entry = tar.getNextEntry()) != null) {
+                if (!entry.isDirectory()) {
+                    names.add(entry.getName().replaceFirst("^/", ""));
+                }
+            }
+        }
+        return names;
+    }
+
+    @Test
+    void buildsABundleCarryingTheCompiledModule() throws Exception {
+        byte[] bundle = OpaWasmBundleBuilder.build("authz.rego", REGO, 
"authz/allow");
+
+        assertTrue(entries(bundle).contains("policy.wasm"),
+                "opa build must emit /policy.wasm - that is the artifact the 
component loads");
+    }
+
+    @Test
+    void packsADataDocumentSittingBesideThePolicy() throws Exception {
+        // building the directory rather than naming the file is what makes 
opa build include data.json; a policy
+        // reading data.* is useless without it
+        byte[] bundle = OpaWasmBundleBuilder.build(
+                Map.of("roles.rego", REGO.getBytes(StandardCharsets.UTF_8),
+                        "data.json", 
"{\"admins\":[\"carol\"]}".getBytes(StandardCharsets.UTF_8)),
+                "authz/allow");
+
+        List<String> entries = entries(bundle);
+        assertTrue(entries.contains("policy.wasm"), entries.toString());
+        assertTrue(entries.contains("data.json"), entries.toString());
+    }
+
+    @Test
+    void refusesToBuildWithoutAnEntrypoint() {
+        // opa build -t wasm with no -e compiles happily and emits nothing 
callable, which would surface much
+        // later as an empty result array rather than as a build failure
+        IllegalArgumentException e = 
assertThrows(IllegalArgumentException.class,
+                () -> OpaWasmBundleBuilder.build("authz.rego", REGO));
+
+        assertTrue(e.getMessage().contains("entrypoint"), e.getMessage());
+    }
+
+    @Test
+    void refusesToBuildWithoutSources() {
+        assertThrows(IllegalArgumentException.class, () -> 
OpaWasmBundleBuilder.build(Map.of(), "authz/allow"));
+    }
+
+    @Test
+    void reportsWhatFailedWhenTheRegoIsNotValid() {
+        IllegalStateException e = assertThrows(IllegalStateException.class,
+                () -> OpaWasmBundleBuilder.build("broken.rego", "this is not 
rego at all", "authz/allow"));
+
+        assertTrue(e.getMessage().contains("broken.rego"), e.getMessage());
+    }
+}

Reply via email to