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


##########
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:
   Every bundle compiled here is a `bundle.tar.gz`, so the bare `.wasm` branch 
of `OpaWasmEvaluator.loadPolicy` (what `classpath:authz.wasm` used to cover) is 
no longer exercised by any test. Since the tarball is already in hand, one test 
could untar `policy.wasm` from `authzBundle` here and load it via 
`file:.../authz.wasm` so both accepted formats stay covered.



##########
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:
   This test needs Docker but runs under surefire during a plain `mvn install` 
of `camel-test-infra-opa`. The `skipITs` guard only fires when someone 
explicitly passes `-DskipITs`, so a developer without Docker gets a failing 
build rather than a skipped test. Convention in this tree is to make such tests 
`*IT` under failsafe - see `camel-test-infra-jaeger` (`JaegerServiceIT` plus 
the failsafe execution in its pom). Suggest renaming to 
`OpaWasmBundleBuilderIT` and adding the same failsafe config to 
`camel-test-infra-opa/pom.xml`.
   
   Also a nit on line 105: `assertEquals(true, x)` reads better as 
`assertTrue(x, message)`.



##########
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:
   The `@Timeout(60)` that guarded this test is gone. Its whole point is to 
detect a wedged single-instance pool - without the timeout a regression hangs 
the build until surefire's own fork timeout instead of failing this test. Was 
dropping it intentional?



-- 
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