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 5220998ad575 CAMEL-24736: camel-opa - add a readiness check for the 
OpaSecurityPolicy (#26436)
5220998ad575 is described below

commit 5220998ad5750c2bd25900033106427d403184a8
Author: Andrea Cosentino <[email protected]>
AuthorDate: Wed Sep 16 12:45:24 2026 +0200

    CAMEL-24736: camel-opa - add a readiness check for the OpaSecurityPolicy 
(#26436)
    
    CAMEL-24644 gave the opa: producer a health check but left OpaSecurityPolicy
    without one, which is the wrong way round: a producer that cannot reach OPA
    records a deny verdict the route can inspect, while the policy throws
    CamelAuthorizationException and stops the exchange outright. The path that
    hard-fails every message had no health signal at all.
    
    A policy is a bean with no producer, so it registers through
    HealthCheckRegistry.get(route.getCamelContext()) from beforeWrap, guarded to
    fire once however many routes it wraps and only for a client it built itself
    from serverUrl. Both probes now share OpaHealthProbe rather than being two
    near-identical copies that could drift apart in security-relevant code.
    
    healthCheckEnabled (default true) turns it off for a route that should stay
    ready regardless. The shared probe also stops building //health from a
    serverUrl ending in a slash, which OPA answers with a redirect the client 
does
    not follow - reporting a healthy server DOWN.
    
    Co-authored-by: Claude Opus 5 <[email protected]>
---
 .../apache/camel/catalog/docs/opa-component.adoc   |  12 +-
 .../camel-opa/src/main/docs/opa-component.adoc     |  12 +-
 ...roducerHealthCheck.java => OpaHealthProbe.java} |  53 ++++---
 .../component/opa/OpaProducerHealthCheck.java      |  45 +-----
 .../component/opa/security/OpaSecurityPolicy.java  |  45 ++++++
 .../opa/security/OpaSecurityPolicyHealthCheck.java |  53 +++++++
 .../security/OpaSecurityPolicyHealthCheckTest.java | 175 +++++++++++++++++++++
 7 files changed, 322 insertions(+), 73 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opa-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opa-component.adoc
index 52f995677920..4df6be0ac77e 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opa-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opa-component.adoc
@@ -304,8 +304,16 @@ component with `healthCheckProducerEnabled`. The check 
reports DOWN with the und
 server and a server answering its health endpoint with an error are reported 
differently, so a deny is never
 confused with an outage.
 
-The check is only registered when the endpoint was given a `serverUrl`. An 
injected `opaClient` may point anywhere
-and the endpoint has no way to ask it where, so no probe is registered in that 
case.
+`OpaSecurityPolicy` registers an equivalent check, under an id starting 
`security-policy:opa-`. It is arguably the
+more important of the two: a denied producer merely records a verdict the 
route can inspect, while the policy throws
+`CamelAuthorizationException` and stops the exchange, so an unreachable server 
there fails every message outright.
+That is why the policy's check is on by default rather than opt-in like the 
producer's. Set
+`healthCheckEnabled=false` on the policy for a route that should stay ready 
regardless — one running `failOpen`, say
+— in preference to hiding the check with `camel.health.exclude-pattern`.
+
+Neither check is registered when an `opaClient` was injected: that client may 
point anywhere and neither the
+endpoint nor the policy has a way to ask it where, so probing the configured 
`serverUrl` would report on a server
+they may never talk to. The endpoint check is also skipped when no `serverUrl` 
was given.
 
 == Security notes
 
diff --git a/components/camel-opa/src/main/docs/opa-component.adoc 
b/components/camel-opa/src/main/docs/opa-component.adoc
index 52f995677920..4df6be0ac77e 100644
--- a/components/camel-opa/src/main/docs/opa-component.adoc
+++ b/components/camel-opa/src/main/docs/opa-component.adoc
@@ -304,8 +304,16 @@ component with `healthCheckProducerEnabled`. The check 
reports DOWN with the und
 server and a server answering its health endpoint with an error are reported 
differently, so a deny is never
 confused with an outage.
 
-The check is only registered when the endpoint was given a `serverUrl`. An 
injected `opaClient` may point anywhere
-and the endpoint has no way to ask it where, so no probe is registered in that 
case.
+`OpaSecurityPolicy` registers an equivalent check, under an id starting 
`security-policy:opa-`. It is arguably the
+more important of the two: a denied producer merely records a verdict the 
route can inspect, while the policy throws
+`CamelAuthorizationException` and stops the exchange, so an unreachable server 
there fails every message outright.
+That is why the policy's check is on by default rather than opt-in like the 
producer's. Set
+`healthCheckEnabled=false` on the policy for a route that should stay ready 
regardless — one running `failOpen`, say
+— in preference to hiding the check with `camel.health.exclude-pattern`.
+
+Neither check is registered when an `opaClient` was injected: that client may 
point anywhere and neither the
+endpoint nor the policy has a way to ask it where, so probing the configured 
`serverUrl` would report on a server
+they may never talk to. The endpoint check is also skipped when no `serverUrl` 
was given.
 
 == Security notes
 
diff --git 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaProducerHealthCheck.java
 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaHealthProbe.java
similarity index 58%
copy from 
components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaProducerHealthCheck.java
copy to 
components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaHealthProbe.java
index 31d754769dfd..b354052c0154 100644
--- 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaProducerHealthCheck.java
+++ 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaHealthProbe.java
@@ -21,50 +21,53 @@ import java.net.http.HttpClient;
 import java.net.http.HttpRequest;
 import java.net.http.HttpResponse;
 import java.time.Duration;
-import java.util.Map;
 
 import org.apache.camel.health.HealthCheckResultBuilder;
-import org.apache.camel.impl.health.AbstractHealthCheck;
+import org.apache.camel.util.FileUtil;
 import org.apache.camel.util.ObjectHelper;
 import org.apache.camel.util.URISupport;
 
 /**
- * Readiness check for the OPA server a producer sends its decisions to.
+ * The OPA readiness probe, shared by the producer health check and the one on 
{@code OpaSecurityPolicy}.
  * <p/>
- * The component fails closed, so an OPA server that cannot be reached fails 
every exchange through the route. This
- * check probes the server's {@code /health} endpoint so that an unavailable 
policy decision point is visible before
- * traffic starts failing, rather than only in the error logs afterwards.
+ * Both ask the same question of the same endpoint, so the probe lives in one 
place: a fix here - a changed timeout, a
+ * new failure mode to report - applies to both rather than to whichever was 
remembered.
  */
-public class OpaProducerHealthCheck extends AbstractHealthCheck {
+public final class OpaHealthProbe {
 
     private static final Duration TIMEOUT = Duration.ofSeconds(5);
 
-    // java.net.http.HttpClient only became AutoCloseable in Java 21 (JEP 
480); on Camel's Java 17 baseline there
-    // is no way to shut down its internal executor/selector threads, so a 
per-instance client would leak a thread
-    // pool on every producer start. Share a single client across all checks - 
the per-request URL and bearer token
-    // are set on the HttpRequest, so nothing endpoint-specific needs to live 
on the client.
+    /**
+     * Shared across every OPA health check in the JVM. {@link HttpClient} 
only became {@link AutoCloseable} in Java 21,
+     * so on the Java 17 baseline one client per check would leak its selector 
thread with no way to shut it down. The
+     * client is immutable and thread-safe, so sharing is free; the 
per-request URL and token live on the
+     * {@link HttpRequest}.
+     */
     private static final HttpClient HTTP_CLIENT = 
HttpClient.newBuilder().connectTimeout(TIMEOUT).build();
 
-    private final String serverUrl;
-    private final String bearerToken;
-    private final String policyPath;
-
-    public OpaProducerHealthCheck(String serverUrl, String bearerToken, String 
policyPath, String id) {
-        // the id is built from the endpoint URI so that two endpoints sharing 
a policy path stay distinct, but that
-        // URI carries the bearerToken in the clear and the id is published in 
the health output, so sanitize it
-        super("camel", "producer:opa-" + URISupport.sanitizeUri(id));
-        this.serverUrl = serverUrl;
-        this.bearerToken = bearerToken;
-        this.policyPath = policyPath;
+    private OpaHealthProbe() {
     }
 
-    @Override
-    protected void doCall(HealthCheckResultBuilder builder, Map<String, 
Object> options) {
+    /**
+     * Probes the OPA server's health endpoint and records the outcome on the 
builder.
+     * <p/>
+     * An unreachable server and a server answering with an error are reported 
differently, so an outage is never
+     * mistaken for a policy that denied.
+     *
+     * @param builder     the result to populate
+     * @param serverUrl   base URL of the OPA server, without the /v1/data 
suffix
+     * @param bearerToken token for OPA API authentication, or null when OPA 
does not require one
+     * @param policyPath  the policy this check is reporting for, recorded as 
a detail
+     */
+    public static void probe(
+            HealthCheckResultBuilder builder, String serverUrl, String 
bearerToken, String policyPath) {
         builder.detail("opa.serverUrl", URISupport.sanitizeUri(serverUrl));
         builder.detail("opa.policyPath", policyPath);
 
         HttpRequest.Builder request = HttpRequest.newBuilder()
-                .uri(URI.create(serverUrl + "/health"))
+                // a serverUrl with a trailing slash would build //health, 
which OPA's router answers with a
+                // redirect the client is not configured to follow - reporting 
a healthy server DOWN on HTTP 301
+                .uri(URI.create(FileUtil.stripTrailingSeparator(serverUrl) + 
"/health"))
                 .timeout(TIMEOUT)
                 .GET();
         if (ObjectHelper.isNotEmpty(bearerToken)) {
diff --git 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaProducerHealthCheck.java
 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaProducerHealthCheck.java
index 31d754769dfd..d91946850d99 100644
--- 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaProducerHealthCheck.java
+++ 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaProducerHealthCheck.java
@@ -16,16 +16,10 @@
  */
 package org.apache.camel.component.opa;
 
-import java.net.URI;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.time.Duration;
 import java.util.Map;
 
 import org.apache.camel.health.HealthCheckResultBuilder;
 import org.apache.camel.impl.health.AbstractHealthCheck;
-import org.apache.camel.util.ObjectHelper;
 import org.apache.camel.util.URISupport;
 
 /**
@@ -37,14 +31,6 @@ import org.apache.camel.util.URISupport;
  */
 public class OpaProducerHealthCheck extends AbstractHealthCheck {
 
-    private static final Duration TIMEOUT = Duration.ofSeconds(5);
-
-    // java.net.http.HttpClient only became AutoCloseable in Java 21 (JEP 
480); on Camel's Java 17 baseline there
-    // is no way to shut down its internal executor/selector threads, so a 
per-instance client would leak a thread
-    // pool on every producer start. Share a single client across all checks - 
the per-request URL and bearer token
-    // are set on the HttpRequest, so nothing endpoint-specific needs to live 
on the client.
-    private static final HttpClient HTTP_CLIENT = 
HttpClient.newBuilder().connectTimeout(TIMEOUT).build();
-
     private final String serverUrl;
     private final String bearerToken;
     private final String policyPath;
@@ -60,35 +46,6 @@ public class OpaProducerHealthCheck extends 
AbstractHealthCheck {
 
     @Override
     protected void doCall(HealthCheckResultBuilder builder, Map<String, 
Object> options) {
-        builder.detail("opa.serverUrl", URISupport.sanitizeUri(serverUrl));
-        builder.detail("opa.policyPath", policyPath);
-
-        HttpRequest.Builder request = HttpRequest.newBuilder()
-                .uri(URI.create(serverUrl + "/health"))
-                .timeout(TIMEOUT)
-                .GET();
-        if (ObjectHelper.isNotEmpty(bearerToken)) {
-            request.header("Authorization", "Bearer " + bearerToken);
-        }
-
-        try {
-            HttpResponse<Void> response = HTTP_CLIENT.send(request.build(), 
HttpResponse.BodyHandlers.discarding());
-            if (response.statusCode() == 200) {
-                builder.up();
-            } else {
-                builder.down();
-                builder.message("OPA server answered its health endpoint with 
HTTP " + response.statusCode());
-                builder.detail("opa.statusCode", response.statusCode());
-            }
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            builder.down();
-            builder.message("Interrupted while checking the OPA server");
-            builder.error(e);
-        } catch (Exception e) {
-            builder.down();
-            builder.message("Cannot reach the OPA server: " + e.getMessage());
-            builder.error(e);
-        }
+        OpaHealthProbe.probe(builder, serverUrl, bearerToken, policyPath);
     }
 }
diff --git 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/security/OpaSecurityPolicy.java
 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/security/OpaSecurityPolicy.java
index d26ef07ab575..3c3a60c907a1 100644
--- 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/security/OpaSecurityPolicy.java
+++ 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/security/OpaSecurityPolicy.java
@@ -22,7 +22,9 @@ import org.apache.camel.Processor;
 import org.apache.camel.Route;
 import org.apache.camel.component.opa.OpaPolicyEvaluator;
 import org.apache.camel.component.opa.OpaRestEvaluator;
+import org.apache.camel.health.HealthCheckRegistry;
 import org.apache.camel.spi.AuthorizationPolicy;
+import org.apache.camel.util.ObjectHelper;
 import org.apache.camel.util.StringHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -56,7 +58,11 @@ public class OpaSecurityPolicy implements 
AuthorizationPolicy {
     private boolean failOpen;
     private OPAClient opaClient;
 
+    private boolean healthCheckEnabled = true;
+
     private volatile OpaPolicyEvaluator evaluator;
+    private volatile OpaSecurityPolicyHealthCheck healthCheck;
+    private volatile boolean ownsClient;
 
     public OpaSecurityPolicy() {
     }
@@ -71,11 +77,36 @@ public class OpaSecurityPolicy implements 
AuthorizationPolicy {
         if (evaluator == null) {
             StringHelper.notEmpty(policyPath, "policyPath", this);
             if (opaClient == null) {
+                // createClient moved to OpaRestEvaluator when the evaluator 
became an abstract base
                 opaClient = OpaRestEvaluator.createClient(serverUrl, 
bearerToken);
+                ownsClient = true;
             }
             evaluator = new OpaRestEvaluator(
                     opaClient, policyPath, allowKey, includeHeaders, 
includeProperties, includeBody, failOpen);
         }
+        // after validation, so a policy that is missing its policyPath fails 
without leaving a ".../null" check
+        // behind in the registry
+        registerHealthCheck(route);
+    }
+
+    /**
+     * Registers a readiness check for the OPA server, once per policy however 
many routes it wraps.
+     * <p/>
+     * Only for a client this policy built itself from {@code serverUrl}. An 
injected {@code opaClient} can point
+     * anywhere and this policy has no way to ask it where, so probing the 
configured URL would report on a server it
+     * may never talk to - hence {@code ownsClient} rather than a null check 
on {@code opaClient}, which by the time
+     * this runs is set either way.
+     */
+    private void registerHealthCheck(Route route) {
+        if (!healthCheckEnabled || healthCheck != null || !ownsClient || 
ObjectHelper.isEmpty(serverUrl)) {
+            return;
+        }
+        HealthCheckRegistry registry = 
HealthCheckRegistry.get(route.getCamelContext());
+        if (registry == null) {
+            return;
+        }
+        healthCheck = new OpaSecurityPolicyHealthCheck(serverUrl, bearerToken, 
policyPath);
+        registry.register(healthCheck);
     }
 
     @Override
@@ -180,6 +211,20 @@ public class OpaSecurityPolicy implements 
AuthorizationPolicy {
         this.failOpen = failOpen;
     }
 
+    public boolean isHealthCheckEnabled() {
+        return healthCheckEnabled;
+    }
+
+    /**
+     * Whether to register a readiness check for the OPA server this policy 
queries. Enabled by default: the policy
+     * denies every exchange it guards while the server is unreachable, so a 
route that is up but cannot reach OPA is
+     * not ready. Disable it for a policy whose route should stay ready 
regardless - for example one wrapped in
+     * {@code failOpen} - rather than excluding the check by pattern.
+     */
+    public void setHealthCheckEnabled(boolean healthCheckEnabled) {
+        this.healthCheckEnabled = healthCheckEnabled;
+    }
+
     public OPAClient getOpaClient() {
         return opaClient;
     }
diff --git 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/security/OpaSecurityPolicyHealthCheck.java
 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/security/OpaSecurityPolicyHealthCheck.java
new file mode 100644
index 000000000000..75e4aad3a7cf
--- /dev/null
+++ 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/security/OpaSecurityPolicyHealthCheck.java
@@ -0,0 +1,53 @@
+/*
+ * 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.security;
+
+import java.util.Map;
+
+import org.apache.camel.component.opa.OpaHealthProbe;
+import org.apache.camel.health.HealthCheckResultBuilder;
+import org.apache.camel.impl.health.AbstractHealthCheck;
+import org.apache.camel.util.URISupport;
+
+/**
+ * Readiness check for the OPA server behind an {@link OpaSecurityPolicy}.
+ * <p/>
+ * The policy is the stricter of the component's two paths: a denied producer 
merely records a verdict the route can
+ * inspect, while this one throws {@link 
org.apache.camel.CamelAuthorizationException} and stops the exchange. So an
+ * unreachable server here fails every message outright, which is exactly the 
condition worth surfacing before traffic
+ * arrives rather than after.
+ */
+public class OpaSecurityPolicyHealthCheck extends AbstractHealthCheck {
+
+    private final String serverUrl;
+    private final String bearerToken;
+    private final String policyPath;
+
+    public OpaSecurityPolicyHealthCheck(String serverUrl, String bearerToken, 
String policyPath) {
+        // serverUrl and policyPath together identify the decision this policy 
enforces, so two policies pointing at
+        // different servers stay distinct; sanitized because the id is 
published in the health output
+        super("camel", "security-policy:opa-" + 
URISupport.sanitizeUri(serverUrl + "/" + policyPath));
+        this.serverUrl = serverUrl;
+        this.bearerToken = bearerToken;
+        this.policyPath = policyPath;
+    }
+
+    @Override
+    protected void doCall(HealthCheckResultBuilder builder, Map<String, 
Object> options) {
+        OpaHealthProbe.probe(builder, serverUrl, bearerToken, policyPath);
+    }
+}
diff --git 
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/security/OpaSecurityPolicyHealthCheckTest.java
 
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/security/OpaSecurityPolicyHealthCheckTest.java
new file mode 100644
index 000000000000..123f9fe90a0c
--- /dev/null
+++ 
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/security/OpaSecurityPolicyHealthCheckTest.java
@@ -0,0 +1,175 @@
+/*
+ * 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.security;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.util.List;
+import java.util.Map;
+
+import com.styra.opa.OPAClient;
+import com.sun.net.httpserver.HttpServer;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.health.HealthCheck;
+import org.apache.camel.health.HealthCheckRegistry;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+
+/**
+ * The security policy hard-fails every exchange when OPA is unreachable, so 
it needs the same readiness signal the
+ * producer got.
+ */
+public class OpaSecurityPolicyHealthCheckTest extends CamelTestSupport {
+
+    private static final String TOKEN = "s3cr3t-token";
+
+    private static HttpServer server;
+    private static String serverUrl;
+
+    private final OpaSecurityPolicy policy = new OpaSecurityPolicy();
+
+    @AfterEach
+    void stopServer() {
+        if (server != null) {
+            server.stop(0);
+            server = null;
+        }
+    }
+
+    private static String startHealthyServer() throws IOException {
+        server = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
+        server.createContext("/health", exchange -> {
+            exchange.sendResponseHeaders(200, -1);
+            try (OutputStream out = exchange.getResponseBody()) {
+                out.flush();
+            }
+        });
+        server.start();
+        return "http://localhost:"; + server.getAddress().getPort();
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() throws Exception {
+        serverUrl = startHealthyServer();
+        policy.setPolicyPath("authz/allow");
+        policy.setServerUrl(serverUrl);
+        policy.setBearerToken(TOKEN);
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:start").policy(policy).to("mock:result");
+            }
+        };
+    }
+
+    private List<HealthCheck> registered() {
+        HealthCheckRegistry registry = HealthCheckRegistry.get(context);
+        assertThat(registry).isNotNull();
+        return registry.stream()
+                .filter(hc -> hc.getId().startsWith("security-policy:opa-"))
+                .toList();
+    }
+
+    @Test
+    void registersAReadinessCheckForTheServerItEnforces() {
+        assertThat(registered()).hasSize(1);
+        HealthCheck check = registered().get(0);
+
+        HealthCheck.Result result = check.call(Map.of());
+        assertThat(result.getState()).isEqualTo(HealthCheck.State.UP);
+        assertThat(result.getDetails()).containsEntry("opa.policyPath", 
"authz/allow");
+    }
+
+    @Test
+    void neverPublishesTheBearerTokenInTheCheckId() {
+        // the id reaches the health output, and serverUrl/policyPath are 
enough to identify the decision
+        assertThat(registered().get(0).getId()).doesNotContain(TOKEN);
+    }
+
+    @Test
+    void toleratesATrailingSlashOnTheServerUrl() {
+        // concatenating "/health" onto a base that already ends in one builds 
//health. OPA's router answers a
+        // non-canonical path with a redirect, and the probe's client does not 
follow redirects, so a healthy
+        // server was reported DOWN. Every other test here uses a slash-free 
URL, which is how it went unnoticed.
+        OpaSecurityPolicyHealthCheck check
+                = new OpaSecurityPolicyHealthCheck(serverUrl + "/", null, 
"authz/allow");
+        check.setEnabled(true);
+
+        HealthCheck.Result result = check.call(Map.of());
+
+        assertThat(result.getState()).isEqualTo(HealthCheck.State.UP);
+    }
+
+    @Test
+    void reportsDownWhenTheServerCannotBeReached() {
+        OpaSecurityPolicyHealthCheck check
+                = new OpaSecurityPolicyHealthCheck("http://localhost:1";, null, 
"authz/allow");
+        check.setEnabled(true);
+
+        HealthCheck.Result result = check.call(Map.of());
+
+        assertThat(result.getState()).isEqualTo(HealthCheck.State.DOWN);
+        assertThat(result.getMessage()).get().asString().contains("Cannot 
reach the OPA server");
+    }
+
+    @Test
+    void registersNothingWhenTheCheckIsDisabled() {
+        OpaSecurityPolicy disabled = new OpaSecurityPolicy();
+        disabled.setPolicyPath("authz/allow");
+        disabled.setServerUrl("http://unused:8181";);
+        disabled.setHealthCheckEnabled(false);
+
+        disabled.beforeWrap(context.getRoutes().get(0), null);
+
+        // still only the one the route under test registered
+        assertThat(registered()).hasSize(1);
+    }
+
+    @Test
+    void registersNothingForAPolicyThatFailsValidation() {
+        // registration used to run before notEmpty(policyPath), which left a 
".../null" check in the registry
+        // of a policy whose route then never started
+        OpaSecurityPolicy misconfigured = new OpaSecurityPolicy();
+        misconfigured.setServerUrl("http://unused:8181";);
+
+        assertThatThrownBy(() -> 
misconfigured.beforeWrap(context.getRoutes().get(0), null))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("policyPath");
+
+        assertThat(registered()).hasSize(1);
+        assertThat(registered().get(0).getId()).doesNotContain("null");
+    }
+
+    @Test
+    void registersNothingWhenAnOpaClientWasInjected() {
+        OpaSecurityPolicy injected = new OpaSecurityPolicy();
+        injected.setPolicyPath("authz/allow");
+        injected.setServerUrl("http://unused:8181";);
+        injected.setOpaClient(mock(OPAClient.class));
+
+        injected.beforeWrap(context.getRoutes().get(0), null);
+
+        // an injected client may point anywhere, so probing serverUrl would 
report on the wrong server
+        assertThat(registered()).hasSize(1);
+    }
+}

Reply via email to