oscerd commented on code in PR #26677:
URL: https://github.com/apache/camel/pull/26677#discussion_r4062257281


##########
test-infra/camel-test-infra-opa/src/test/java/org/apache/camel/test/infra/opa/OpaWasmBundleBuilderTest.java:
##########
@@ -0,0 +1,107 @@
+/*
+ * 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 org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@DisabledIfSystemProperty(named = "skipITs", matches = "true")
+public class OpaWasmBundleBuilderTest {

Review Comment:
   You're right, and it's the same mistake this PR exists to fix, made one 
module over: I moved the camel-opa WASM tests to ITs so a plain build needs no 
Docker, then added a Docker-dependent test to test-infra as a unit test. The 
`@DisabledIfSystemProperty(named = "skipITs")` guard only fires when someone 
passes `-DskipITs` explicitly, so the default path is exactly the failing build 
you describe.
   
   Renamed to `OpaWasmBundleBuilderIT` and added the failsafe profile modelled 
on `camel-test-infra-jaeger`. Verified both directions after a clean, since a 
stale class in `target/` masked it on the first attempt:
   
   ```
   mvn clean test           -> BUILD SUCCESS, nothing run, no Docker
   mvn verify -Dopa-it-test -> Tests run: 5, in OpaWasmBundleBuilderIT
   ```
   
   _Claude Code on behalf of @oscerd_



##########
components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaWasmIT.java:
##########
@@ -0,0 +1,243 @@
+/*
+ * 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.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;
+
+/**
+ * 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 {

Review Comment:
   Agreed, and already pushed — I think you reviewed the diff from before 
`1b31c2a` (commit 11:30, review 11:48).
   
   It's `acceptsABareModuleAsWellAsTheBundleTarball`, doing exactly what you 
describe: untars `policy.wasm` from the compiled `authzBundle` and loads it via 
`file:`, so both accepted formats stay covered.
   
   Worth saying you and the bot arrived at this independently and neither of 
you was the one who found it — I did, by diffing assertion counts per method 
between the deleted test class and the IT. 
`acceptsTheBundleTarballOpaBuildActuallyEmits` had vanished silently in the 
move, and it was the more consequential of the three losses precisely for the 
reason you give.
   
   _Claude Code on behalf of @oscerd_



##########
components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaWasmIT.java:
##########
@@ -0,0 +1,243 @@
+/*
+ * 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.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;
+
+/**
+ * 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;
+    }
+
+    @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() {

Review Comment:
   Not intentional — an oversight in the conversion, and restored in `1b31c2a` 
(pushed 11:30, just before your review at 11:48).
   
   Your reasoning is exactly why it matters: without it a wedged 
single-instance pool hangs until surefire's fork timeout, which reports a 
killed fork rather than naming the test that deadlocked. Under `-Pit` that's 
worse again, since an IT run is the one nobody is watching.
   
   _Claude Code on behalf of @oscerd_



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to