gnodet-bot commented on code in PR #29695:
URL: https://github.com/apache/camel/pull/29695#discussion_r4561190122


##########
components/camel-jdbc/src/test/java/org/apache/camel/component/jdbc/JdbcCredentialRotationIT.java:
##########
@@ -0,0 +1,268 @@
+/*
+ * 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.jdbc;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import com.zaxxer.hikari.HikariConfig;
+import com.zaxxer.hikari.HikariDataSource;
+import io.agroal.api.AgroalDataSource;
+import 
io.agroal.api.configuration.supplier.AgroalDataSourceConfigurationSupplier;
+import io.agroal.api.security.AgroalSecurityProvider;
+import io.agroal.api.security.NamePrincipal;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.Registry;
+import org.apache.camel.support.DefaultContextReloadStrategy;
+import org.apache.camel.test.infra.postgres.services.PostgresService;
+import org.apache.camel.test.infra.postgres.services.PostgresServiceFactory;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * End-to-end integration test for credential rotation in {@link 
JdbcComponent}.
+ *
+ * <p>
+ * Verifies the full rotation flow against a real PostgreSQL database:
+ * <ol>
+ * <li>Start the pool with credential A and exercise a Camel JDBC route.</li>
+ * <li>Change the database password to credential B via an admin 
connection.</li>
+ * <li>Update the pool's password and call {@link 
JdbcComponent#onSecretRotation(Object)}.</li>
+ * <li>Verify that subsequent queries succeed and use a new physical 
connection (different
+ * {@code pg_backend_pid()}).</li>
+ * </ol>
+ * HikariCP is tested with a direct pool credential update; Agroal is tested 
with a custom
+ * {@link AgroalSecurityProvider} that supplies the current password on each 
new connection.
+ */
+class JdbcCredentialRotationIT extends CamelTestSupport {
+
+    private static final String CRED_A = "password_a_initial"; // NOSONAR 
test-only credentials
+    private static final String CRED_B = "password_b_rotated"; // NOSONAR 
test-only credentials
+    private static final String TEST_USER = "camel_rotation_test";
+
+    @RegisterExtension
+    static PostgresService postgres = 
PostgresServiceFactory.createSingletonService();
+
+    private JdbcComponent jdbcComponent;
+    private HikariDataSource hikariDs;
+
+    /**
+     * Creates the test database role before the Camel context is started.
+     * <p>
+     * {@code setupResources()} runs before {@link #bindToRegistry(Registry)}, 
so the role exists when HikariCP
+     * initialises its connection pool.
+     */
+    @Override
+    protected void setupResources() throws Exception {
+        super.setupResources();
+        try (Connection adminConn = DriverManager.getConnection(
+                postgres.jdbcUrl(), postgres.userName(), postgres.password());
+             Statement st = adminConn.createStatement()) {
+            st.execute("DROP ROLE IF EXISTS " + TEST_USER);
+            st.execute("CREATE ROLE " + TEST_USER + " LOGIN PASSWORD '" + 
CRED_A + "'");
+            st.execute("GRANT CONNECT ON DATABASE " + postgres.database() + " 
TO " + TEST_USER);
+        }
+    }
+
+    @AfterEach
+    void dropTestUser() throws Exception {
+        if (hikariDs != null) {
+            hikariDs.close();
+        }
+        try (Connection adminConn = DriverManager.getConnection(
+                postgres.jdbcUrl(), postgres.userName(), postgres.password());
+             Statement st = adminConn.createStatement()) {
+            st.execute("REVOKE ALL PRIVILEGES ON DATABASE " + 
postgres.database() + " FROM " + TEST_USER);
+            st.execute("DROP ROLE IF EXISTS " + TEST_USER);
+        }
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:jdbc-query")
+                        .to("jdbc:rotationTestDs");
+            }
+        };
+    }
+
+    @Override
+    protected void bindToRegistry(Registry registry) throws Exception {
+        HikariConfig config = new HikariConfig();
+        config.setJdbcUrl(postgres.jdbcUrl());
+        config.setUsername(TEST_USER);
+        config.setPassword(CRED_A);
+        config.setMaximumPoolSize(2);
+        config.setMinimumIdle(1);
+        config.setConnectionTestQuery("SELECT 1");
+        hikariDs = new HikariDataSource(config);
+
+        registry.bind("rotationTestDs", hikariDs);
+    }
+
+    @Test
+    void hikariCp_credentialRotation_newConnectionUsesNewPassword() throws 
Exception {
+        jdbcComponent = context.getComponent("jdbc", JdbcComponent.class);
+
+        DefaultContextReloadStrategy reload = new 
DefaultContextReloadStrategy();
+        context.addService(reload);
+
+        // Step 1: verify the pool works with cred A
+        int pidBefore = queryBackendPid();
+        assertNotEquals(0, pidBefore, "pg_backend_pid() must return a non-zero 
value with credential A");
+
+        // Step 2: change the database password to cred B via the admin 
connection
+        try (Connection adminConn = DriverManager.getConnection(
+                postgres.jdbcUrl(), postgres.userName(), postgres.password());
+             Statement st = adminConn.createStatement()) {
+            st.execute("ALTER ROLE " + TEST_USER + " PASSWORD '" + CRED_B + 
"'");
+        }
+
+        // Verify old password no longer works for new connections
+        assertThrows(SQLException.class,
+                () -> DriverManager.getConnection(postgres.jdbcUrl(), 
TEST_USER, CRED_A),
+                "Old password must be rejected after ALTER ROLE");
+
+        // Step 3: update the pool's password to cred B, then trigger rotation 
via reload strategy
+        hikariDs.setPassword(CRED_B);
+        reload.onReload("test-rotation");
+
+        // Step 4: verify that a new query succeeds with cred B on a fresh 
connection
+        await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
+            int pidAfter = queryBackendPid();
+            assertNotEquals(0, pidAfter, "pg_backend_pid() must return a 
non-zero value after rotation");
+            assertNotEquals(pidBefore, pidAfter,
+                    "After rotation, pg_backend_pid() must differ — pool must 
have opened a fresh connection");
+        });
+    }
+
+    @Test
+    void agroal_credentialRotation_newConnectionUsesNewPassword() throws 
Exception {
+        // A marker credential object that our custom AgroalSecurityProvider 
recognises.
+        // The provider returns the current password from an AtomicReference, 
allowing
+        // the test to rotate the credential without rebuilding the DataSource.
+        record RotatingCredential() {
+        }
+
+        AtomicReference<String> currentPassword = new 
AtomicReference<>(CRED_A);
+
+        AgroalSecurityProvider rotating = credential -> {
+            if (credential instanceof RotatingCredential) {
+                Properties p = new Properties();
+                p.setProperty("password", currentPassword.get());
+                return p;
+            }
+            return null;
+        };
+
+        try (AgroalDataSource agroalDs = AgroalDataSource.from(
+                new AgroalDataSourceConfigurationSupplier()
+                        .metricsEnabled()
+                        .connectionPoolConfiguration(pool -> pool
+                                .maxSize(2)
+                                .minSize(1)
+                                .connectionFactoryConfiguration(factory -> 
factory
+                                        .jdbcUrl(postgres.jdbcUrl())
+                                        .principal(new 
NamePrincipal(TEST_USER))
+                                        .credential(new RotatingCredential())
+                                        .addSecurityProvider(rotating))))) {
+
+            // Register the Agroal DataSource in the registry so it can be 
discovered by the component
+            context.getRegistry().bind("agroalRotationDs", agroalDs);
+
+            // Build a temporary JdbcComponent wired to the Agroal DataSource
+            JdbcComponent agroalComponent = new JdbcComponent();
+            agroalComponent.setDataSource(agroalDs);
+            agroalComponent.setCamelContext(context);
+
+            // Step 1: verify the pool works with cred A — grab a PID
+            int pidBefore;
+            try (Connection conn = agroalDs.getConnection();
+                 var rs = conn.createStatement().executeQuery("SELECT 
pg_backend_pid() AS pid")) {

Review Comment:
   🔧 **Nit:** The `Statement` created by `conn.createStatement()` is not 
tracked in the try-with-resources header — only `Connection` and `ResultSet` 
are closed explicitly. While JDBC drivers typically close child statements when 
the connection closes, explicit resource tracking is safer and idiomatic.
   
   ```suggestion
               try (Connection conn = agroalDs.getConnection();
                    Statement st = conn.createStatement();
                    var rs = st.executeQuery("SELECT pg_backend_pid() AS pid")) 
{
   ```



##########
components/camel-sql/src/test/java/org/apache/camel/component/sql/SqlCredentialRotationIT.java:
##########
@@ -0,0 +1,267 @@
+/*
+ * 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.sql;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import com.zaxxer.hikari.HikariConfig;
+import com.zaxxer.hikari.HikariDataSource;
+import io.agroal.api.AgroalDataSource;
+import 
io.agroal.api.configuration.supplier.AgroalDataSourceConfigurationSupplier;
+import io.agroal.api.security.AgroalSecurityProvider;
+import io.agroal.api.security.NamePrincipal;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.Registry;
+import org.apache.camel.support.DefaultContextReloadStrategy;
+import org.apache.camel.test.infra.postgres.services.PostgresService;
+import org.apache.camel.test.infra.postgres.services.PostgresServiceFactory;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * End-to-end integration test for credential rotation in {@link SqlComponent}.
+ *
+ * <p>
+ * Verifies the full rotation flow against a real PostgreSQL database:
+ * <ol>
+ * <li>Start the pool with credential A and exercise a Camel SQL route.</li>
+ * <li>Change the database password to credential B via an admin 
connection.</li>
+ * <li>Update the pool's password and call {@link 
SqlComponent#onSecretRotation(Object)}.</li>
+ * <li>Verify that subsequent queries succeed and use a new physical 
connection (different
+ * {@code pg_backend_pid()}).</li>
+ * </ol>
+ * HikariCP is tested with a direct pool credential update; Agroal is tested 
with a custom
+ * {@link AgroalSecurityProvider} that supplies the current password on each 
new connection.
+ */
+class SqlCredentialRotationIT extends CamelTestSupport {
+
+    private static final String CRED_A = "password_a_initial"; // NOSONAR 
test-only credentials
+    private static final String CRED_B = "password_b_rotated"; // NOSONAR 
test-only credentials
+    private static final String TEST_USER = "camel_sql_rotation_test";
+
+    @RegisterExtension
+    static PostgresService postgres = 
PostgresServiceFactory.createSingletonService();
+
+    private SqlComponent sqlComponent;
+    private HikariDataSource hikariDs;
+
+    /**
+     * Creates the test database role before the Camel context is started.
+     * <p>
+     * {@code setupResources()} runs before {@link #bindToRegistry(Registry)}, 
so the role exists when HikariCP
+     * initialises its connection pool.
+     */
+    @Override
+    protected void setupResources() throws Exception {
+        super.setupResources();
+        try (Connection adminConn = DriverManager.getConnection(
+                postgres.jdbcUrl(), postgres.userName(), postgres.password());
+             Statement st = adminConn.createStatement()) {
+            st.execute("DROP ROLE IF EXISTS " + TEST_USER);
+            st.execute("CREATE ROLE " + TEST_USER + " LOGIN PASSWORD '" + 
CRED_A + "'");
+            st.execute("GRANT CONNECT ON DATABASE " + postgres.database() + " 
TO " + TEST_USER);
+        }
+    }
+
+    @AfterEach
+    void dropTestUser() throws Exception {
+        if (hikariDs != null) {
+            hikariDs.close();
+        }
+        try (Connection adminConn = DriverManager.getConnection(
+                postgres.jdbcUrl(), postgres.userName(), postgres.password());
+             Statement st = adminConn.createStatement()) {
+            st.execute("REVOKE ALL PRIVILEGES ON DATABASE " + 
postgres.database() + " FROM " + TEST_USER);
+            st.execute("DROP ROLE IF EXISTS " + TEST_USER);
+        }
+    }
+
+    @Override
+    protected void bindToRegistry(Registry registry) throws Exception {
+        HikariConfig config = new HikariConfig();
+        config.setJdbcUrl(postgres.jdbcUrl());
+        config.setUsername(TEST_USER);
+        config.setPassword(CRED_A);
+        config.setMaximumPoolSize(2);
+        config.setMinimumIdle(1);
+        config.setConnectionTestQuery("SELECT 1");
+        hikariDs = new HikariDataSource(config);
+
+        registry.bind("sqlRotationDs", hikariDs);
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:sql-pid")
+                        .to("sql:SELECT pg_backend_pid() AS 
pid?dataSource=#sqlRotationDs");
+            }
+        };
+    }
+
+    @Test
+    void hikariCp_credentialRotation_newConnectionUsesNewPassword() throws 
Exception {
+        sqlComponent = context.getComponent("sql", SqlComponent.class);
+
+        DefaultContextReloadStrategy reload = new 
DefaultContextReloadStrategy();
+        context.addService(reload);
+
+        // Step 1: verify the pool works with cred A
+        int pidBefore = queryBackendPid();
+        assertNotEquals(0, pidBefore, "pg_backend_pid() must return a non-zero 
value with credential A");
+
+        // Step 2: change the database password to cred B via the admin 
connection
+        try (Connection adminConn = DriverManager.getConnection(
+                postgres.jdbcUrl(), postgres.userName(), postgres.password());
+             Statement st = adminConn.createStatement()) {
+            st.execute("ALTER ROLE " + TEST_USER + " PASSWORD '" + CRED_B + 
"'");
+        }
+
+        // Verify old password no longer works for new connections
+        assertThrows(SQLException.class,
+                () -> DriverManager.getConnection(postgres.jdbcUrl(), 
TEST_USER, CRED_A),
+                "Old password must be rejected after ALTER ROLE");
+
+        // Step 3: update the pool's password to cred B, then trigger rotation 
via reload strategy
+        hikariDs.setPassword(CRED_B);
+        reload.onReload("test-rotation");
+
+        // Step 4: verify that a new query succeeds with cred B on a fresh 
connection
+        await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
+            int pidAfter = queryBackendPid();
+            assertNotEquals(0, pidAfter, "pg_backend_pid() must return a 
non-zero value after rotation");
+            assertNotEquals(pidBefore, pidAfter,
+                    "After rotation, pg_backend_pid() must differ — pool must 
have opened a fresh connection");
+        });
+    }
+
+    @Test
+    void agroal_credentialRotation_newConnectionUsesNewPassword() throws 
Exception {
+        // A marker credential object that our custom AgroalSecurityProvider 
recognises.
+        // The provider returns the current password from an AtomicReference, 
allowing
+        // the test to rotate the credential without rebuilding the DataSource.
+        record RotatingCredential() {
+        }
+
+        AtomicReference<String> currentPassword = new 
AtomicReference<>(CRED_A);
+
+        AgroalSecurityProvider rotating = credential -> {
+            if (credential instanceof RotatingCredential) {
+                Properties p = new Properties();
+                p.setProperty("password", currentPassword.get());
+                return p;
+            }
+            return null;
+        };
+
+        try (AgroalDataSource agroalDs = AgroalDataSource.from(
+                new AgroalDataSourceConfigurationSupplier()
+                        .metricsEnabled()
+                        .connectionPoolConfiguration(pool -> pool
+                                .maxSize(2)
+                                .minSize(1)
+                                .connectionFactoryConfiguration(factory -> 
factory
+                                        .jdbcUrl(postgres.jdbcUrl())
+                                        .principal(new 
NamePrincipal(TEST_USER))
+                                        .credential(new RotatingCredential())
+                                        .addSecurityProvider(rotating))))) {
+
+            // Register the Agroal DataSource in the registry so it can be 
discovered by the component
+            context.getRegistry().bind("agroalRotationDs", agroalDs);
+
+            // Build a temporary SqlComponent wired to the Agroal DataSource
+            SqlComponent agroalComponent = new SqlComponent();
+            agroalComponent.setDataSource(agroalDs);
+            agroalComponent.setCamelContext(context);
+
+            // Step 1: verify the pool works with cred A — grab a PID
+            int pidBefore;
+            try (Connection conn = agroalDs.getConnection();
+                 var rs = conn.createStatement().executeQuery("SELECT 
pg_backend_pid() AS pid")) {

Review Comment:
   🔧 **Nit:** Same `Statement` resource-tracking issue as in 
`JdbcCredentialRotationIT` — the `Statement` from `conn.createStatement()` is 
not tracked in the try-with-resources header.
   
   ```suggestion
               try (Connection conn = agroalDs.getConnection();
                    Statement st = conn.createStatement();
                    var rs = st.executeQuery("SELECT pg_backend_pid() AS pid")) 
{
   ```



##########
components/camel-jdbc/src/test/java/org/apache/camel/component/jdbc/JdbcCredentialRotationIT.java:
##########
@@ -0,0 +1,268 @@
+/*
+ * 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.jdbc;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import com.zaxxer.hikari.HikariConfig;
+import com.zaxxer.hikari.HikariDataSource;
+import io.agroal.api.AgroalDataSource;
+import 
io.agroal.api.configuration.supplier.AgroalDataSourceConfigurationSupplier;
+import io.agroal.api.security.AgroalSecurityProvider;
+import io.agroal.api.security.NamePrincipal;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.Registry;
+import org.apache.camel.support.DefaultContextReloadStrategy;
+import org.apache.camel.test.infra.postgres.services.PostgresService;
+import org.apache.camel.test.infra.postgres.services.PostgresServiceFactory;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * End-to-end integration test for credential rotation in {@link 
JdbcComponent}.
+ *
+ * <p>
+ * Verifies the full rotation flow against a real PostgreSQL database:
+ * <ol>
+ * <li>Start the pool with credential A and exercise a Camel JDBC route.</li>
+ * <li>Change the database password to credential B via an admin 
connection.</li>
+ * <li>Update the pool's password and call {@link 
JdbcComponent#onSecretRotation(Object)}.</li>
+ * <li>Verify that subsequent queries succeed and use a new physical 
connection (different
+ * {@code pg_backend_pid()}).</li>
+ * </ol>
+ * HikariCP is tested with a direct pool credential update; Agroal is tested 
with a custom
+ * {@link AgroalSecurityProvider} that supplies the current password on each 
new connection.
+ */
+class JdbcCredentialRotationIT extends CamelTestSupport {
+
+    private static final String CRED_A = "password_a_initial"; // NOSONAR 
test-only credentials
+    private static final String CRED_B = "password_b_rotated"; // NOSONAR 
test-only credentials
+    private static final String TEST_USER = "camel_rotation_test";
+
+    @RegisterExtension
+    static PostgresService postgres = 
PostgresServiceFactory.createSingletonService();
+
+    private JdbcComponent jdbcComponent;
+    private HikariDataSource hikariDs;
+
+    /**
+     * Creates the test database role before the Camel context is started.
+     * <p>
+     * {@code setupResources()} runs before {@link #bindToRegistry(Registry)}, 
so the role exists when HikariCP
+     * initialises its connection pool.
+     */
+    @Override
+    protected void setupResources() throws Exception {
+        super.setupResources();
+        try (Connection adminConn = DriverManager.getConnection(
+                postgres.jdbcUrl(), postgres.userName(), postgres.password());
+             Statement st = adminConn.createStatement()) {
+            st.execute("DROP ROLE IF EXISTS " + TEST_USER);
+            st.execute("CREATE ROLE " + TEST_USER + " LOGIN PASSWORD '" + 
CRED_A + "'");
+            st.execute("GRANT CONNECT ON DATABASE " + postgres.database() + " 
TO " + TEST_USER);
+        }
+    }
+
+    @AfterEach
+    void dropTestUser() throws Exception {
+        if (hikariDs != null) {
+            hikariDs.close();
+        }
+        try (Connection adminConn = DriverManager.getConnection(
+                postgres.jdbcUrl(), postgres.userName(), postgres.password());
+             Statement st = adminConn.createStatement()) {
+            st.execute("REVOKE ALL PRIVILEGES ON DATABASE " + 
postgres.database() + " FROM " + TEST_USER);
+            st.execute("DROP ROLE IF EXISTS " + TEST_USER);
+        }
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:jdbc-query")
+                        .to("jdbc:rotationTestDs");
+            }
+        };
+    }
+
+    @Override
+    protected void bindToRegistry(Registry registry) throws Exception {
+        HikariConfig config = new HikariConfig();
+        config.setJdbcUrl(postgres.jdbcUrl());
+        config.setUsername(TEST_USER);
+        config.setPassword(CRED_A);
+        config.setMaximumPoolSize(2);
+        config.setMinimumIdle(1);
+        config.setConnectionTestQuery("SELECT 1");
+        hikariDs = new HikariDataSource(config);
+
+        registry.bind("rotationTestDs", hikariDs);
+    }
+
+    @Test
+    void hikariCp_credentialRotation_newConnectionUsesNewPassword() throws 
Exception {
+        jdbcComponent = context.getComponent("jdbc", JdbcComponent.class);
+
+        DefaultContextReloadStrategy reload = new 
DefaultContextReloadStrategy();
+        context.addService(reload);
+
+        // Step 1: verify the pool works with cred A
+        int pidBefore = queryBackendPid();
+        assertNotEquals(0, pidBefore, "pg_backend_pid() must return a non-zero 
value with credential A");
+
+        // Step 2: change the database password to cred B via the admin 
connection
+        try (Connection adminConn = DriverManager.getConnection(
+                postgres.jdbcUrl(), postgres.userName(), postgres.password());
+             Statement st = adminConn.createStatement()) {
+            st.execute("ALTER ROLE " + TEST_USER + " PASSWORD '" + CRED_B + 
"'");
+        }
+
+        // Verify old password no longer works for new connections
+        assertThrows(SQLException.class,
+                () -> DriverManager.getConnection(postgres.jdbcUrl(), 
TEST_USER, CRED_A),
+                "Old password must be rejected after ALTER ROLE");
+
+        // Step 3: update the pool's password to cred B, then trigger rotation 
via reload strategy
+        hikariDs.setPassword(CRED_B);
+        reload.onReload("test-rotation");
+
+        // Step 4: verify that a new query succeeds with cred B on a fresh 
connection
+        await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
+            int pidAfter = queryBackendPid();
+            assertNotEquals(0, pidAfter, "pg_backend_pid() must return a 
non-zero value after rotation");
+            assertNotEquals(pidBefore, pidAfter,
+                    "After rotation, pg_backend_pid() must differ — pool must 
have opened a fresh connection");
+        });
+    }
+
+    @Test
+    void agroal_credentialRotation_newConnectionUsesNewPassword() throws 
Exception {
+        // A marker credential object that our custom AgroalSecurityProvider 
recognises.
+        // The provider returns the current password from an AtomicReference, 
allowing
+        // the test to rotate the credential without rebuilding the DataSource.
+        record RotatingCredential() {
+        }
+
+        AtomicReference<String> currentPassword = new 
AtomicReference<>(CRED_A);
+
+        AgroalSecurityProvider rotating = credential -> {
+            if (credential instanceof RotatingCredential) {
+                Properties p = new Properties();
+                p.setProperty("password", currentPassword.get());
+                return p;
+            }
+            return null;
+        };
+
+        try (AgroalDataSource agroalDs = AgroalDataSource.from(
+                new AgroalDataSourceConfigurationSupplier()
+                        .metricsEnabled()
+                        .connectionPoolConfiguration(pool -> pool
+                                .maxSize(2)
+                                .minSize(1)
+                                .connectionFactoryConfiguration(factory -> 
factory
+                                        .jdbcUrl(postgres.jdbcUrl())
+                                        .principal(new 
NamePrincipal(TEST_USER))
+                                        .credential(new RotatingCredential())
+                                        .addSecurityProvider(rotating))))) {
+
+            // Register the Agroal DataSource in the registry so it can be 
discovered by the component
+            context.getRegistry().bind("agroalRotationDs", agroalDs);
+
+            // Build a temporary JdbcComponent wired to the Agroal DataSource
+            JdbcComponent agroalComponent = new JdbcComponent();
+            agroalComponent.setDataSource(agroalDs);
+            agroalComponent.setCamelContext(context);
+
+            // Step 1: verify the pool works with cred A — grab a PID
+            int pidBefore;
+            try (Connection conn = agroalDs.getConnection();
+                 var rs = conn.createStatement().executeQuery("SELECT 
pg_backend_pid() AS pid")) {
+                assertTrue(rs.next(), "SELECT pg_backend_pid() must return a 
row");
+                pidBefore = rs.getInt("pid");
+                assertNotEquals(0, pidBefore, "pg_backend_pid() must return a 
non-zero value with credential A");
+            }
+
+            long flushCountBefore = agroalDs.getMetrics().flushCount();
+
+            // Step 2: change the database password to cred B via the admin 
connection
+            try (Connection adminConn = DriverManager.getConnection(
+                    postgres.jdbcUrl(), postgres.userName(), 
postgres.password());
+                 Statement st = adminConn.createStatement()) {
+                st.execute("ALTER ROLE " + TEST_USER + " PASSWORD '" + CRED_B 
+ "'");
+            }
+
+            // Verify old password no longer works for new connections
+            assertThrows(SQLException.class,
+                    () -> DriverManager.getConnection(postgres.jdbcUrl(), 
TEST_USER, CRED_A),
+                    "Old password must be rejected after ALTER ROLE");
+
+            // Step 3: update the rotating credential and trigger rotation
+            currentPassword.set(CRED_B);
+            agroalComponent.onSecretRotation("agroal-rotation");
+
+            // Step 4: wait for the flush and verify a new connection uses 
cred B
+            await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
+                long flushCountAfter = agroalDs.getMetrics().flushCount();
+                assertTrue(flushCountAfter > flushCountBefore,
+                        "Agroal flushCount must have increased after rotation 
— flush(GRACEFUL) must have been called");
+            });
+
+            // Step 5: verify the pool creates a new connection (different 
PID) with the new password
+            try (Connection conn = agroalDs.getConnection();
+                 var rs = conn.createStatement().executeQuery("SELECT 
pg_backend_pid() AS pid")) {

Review Comment:
   🔧 **Nit:** Same `Statement` resource-tracking issue — the intermediate 
`Statement` is not in the try-with-resources header.
   
   ```suggestion
               try (Connection conn = agroalDs.getConnection();
                    Statement st = conn.createStatement();
                    var rs = st.executeQuery("SELECT pg_backend_pid() AS pid")) 
{
   ```



##########
components/camel-sql/src/test/java/org/apache/camel/component/sql/SqlCredentialRotationIT.java:
##########
@@ -0,0 +1,267 @@
+/*
+ * 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.sql;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import com.zaxxer.hikari.HikariConfig;
+import com.zaxxer.hikari.HikariDataSource;
+import io.agroal.api.AgroalDataSource;
+import 
io.agroal.api.configuration.supplier.AgroalDataSourceConfigurationSupplier;
+import io.agroal.api.security.AgroalSecurityProvider;
+import io.agroal.api.security.NamePrincipal;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.Registry;
+import org.apache.camel.support.DefaultContextReloadStrategy;
+import org.apache.camel.test.infra.postgres.services.PostgresService;
+import org.apache.camel.test.infra.postgres.services.PostgresServiceFactory;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * End-to-end integration test for credential rotation in {@link SqlComponent}.
+ *
+ * <p>
+ * Verifies the full rotation flow against a real PostgreSQL database:
+ * <ol>
+ * <li>Start the pool with credential A and exercise a Camel SQL route.</li>
+ * <li>Change the database password to credential B via an admin 
connection.</li>
+ * <li>Update the pool's password and call {@link 
SqlComponent#onSecretRotation(Object)}.</li>
+ * <li>Verify that subsequent queries succeed and use a new physical 
connection (different
+ * {@code pg_backend_pid()}).</li>
+ * </ol>
+ * HikariCP is tested with a direct pool credential update; Agroal is tested 
with a custom
+ * {@link AgroalSecurityProvider} that supplies the current password on each 
new connection.
+ */
+class SqlCredentialRotationIT extends CamelTestSupport {
+
+    private static final String CRED_A = "password_a_initial"; // NOSONAR 
test-only credentials
+    private static final String CRED_B = "password_b_rotated"; // NOSONAR 
test-only credentials
+    private static final String TEST_USER = "camel_sql_rotation_test";
+
+    @RegisterExtension
+    static PostgresService postgres = 
PostgresServiceFactory.createSingletonService();
+
+    private SqlComponent sqlComponent;
+    private HikariDataSource hikariDs;
+
+    /**
+     * Creates the test database role before the Camel context is started.
+     * <p>
+     * {@code setupResources()} runs before {@link #bindToRegistry(Registry)}, 
so the role exists when HikariCP
+     * initialises its connection pool.
+     */
+    @Override
+    protected void setupResources() throws Exception {
+        super.setupResources();
+        try (Connection adminConn = DriverManager.getConnection(
+                postgres.jdbcUrl(), postgres.userName(), postgres.password());
+             Statement st = adminConn.createStatement()) {
+            st.execute("DROP ROLE IF EXISTS " + TEST_USER);
+            st.execute("CREATE ROLE " + TEST_USER + " LOGIN PASSWORD '" + 
CRED_A + "'");
+            st.execute("GRANT CONNECT ON DATABASE " + postgres.database() + " 
TO " + TEST_USER);
+        }
+    }
+
+    @AfterEach
+    void dropTestUser() throws Exception {
+        if (hikariDs != null) {
+            hikariDs.close();
+        }
+        try (Connection adminConn = DriverManager.getConnection(
+                postgres.jdbcUrl(), postgres.userName(), postgres.password());
+             Statement st = adminConn.createStatement()) {
+            st.execute("REVOKE ALL PRIVILEGES ON DATABASE " + 
postgres.database() + " FROM " + TEST_USER);
+            st.execute("DROP ROLE IF EXISTS " + TEST_USER);
+        }
+    }
+
+    @Override
+    protected void bindToRegistry(Registry registry) throws Exception {
+        HikariConfig config = new HikariConfig();
+        config.setJdbcUrl(postgres.jdbcUrl());
+        config.setUsername(TEST_USER);
+        config.setPassword(CRED_A);
+        config.setMaximumPoolSize(2);
+        config.setMinimumIdle(1);
+        config.setConnectionTestQuery("SELECT 1");
+        hikariDs = new HikariDataSource(config);
+
+        registry.bind("sqlRotationDs", hikariDs);
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:sql-pid")
+                        .to("sql:SELECT pg_backend_pid() AS 
pid?dataSource=#sqlRotationDs");
+            }
+        };
+    }
+
+    @Test
+    void hikariCp_credentialRotation_newConnectionUsesNewPassword() throws 
Exception {
+        sqlComponent = context.getComponent("sql", SqlComponent.class);
+
+        DefaultContextReloadStrategy reload = new 
DefaultContextReloadStrategy();
+        context.addService(reload);
+
+        // Step 1: verify the pool works with cred A
+        int pidBefore = queryBackendPid();
+        assertNotEquals(0, pidBefore, "pg_backend_pid() must return a non-zero 
value with credential A");
+
+        // Step 2: change the database password to cred B via the admin 
connection
+        try (Connection adminConn = DriverManager.getConnection(
+                postgres.jdbcUrl(), postgres.userName(), postgres.password());
+             Statement st = adminConn.createStatement()) {
+            st.execute("ALTER ROLE " + TEST_USER + " PASSWORD '" + CRED_B + 
"'");
+        }
+
+        // Verify old password no longer works for new connections
+        assertThrows(SQLException.class,
+                () -> DriverManager.getConnection(postgres.jdbcUrl(), 
TEST_USER, CRED_A),
+                "Old password must be rejected after ALTER ROLE");
+
+        // Step 3: update the pool's password to cred B, then trigger rotation 
via reload strategy
+        hikariDs.setPassword(CRED_B);
+        reload.onReload("test-rotation");
+
+        // Step 4: verify that a new query succeeds with cred B on a fresh 
connection
+        await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
+            int pidAfter = queryBackendPid();
+            assertNotEquals(0, pidAfter, "pg_backend_pid() must return a 
non-zero value after rotation");
+            assertNotEquals(pidBefore, pidAfter,
+                    "After rotation, pg_backend_pid() must differ — pool must 
have opened a fresh connection");
+        });
+    }
+
+    @Test
+    void agroal_credentialRotation_newConnectionUsesNewPassword() throws 
Exception {
+        // A marker credential object that our custom AgroalSecurityProvider 
recognises.
+        // The provider returns the current password from an AtomicReference, 
allowing
+        // the test to rotate the credential without rebuilding the DataSource.
+        record RotatingCredential() {
+        }
+
+        AtomicReference<String> currentPassword = new 
AtomicReference<>(CRED_A);
+
+        AgroalSecurityProvider rotating = credential -> {
+            if (credential instanceof RotatingCredential) {
+                Properties p = new Properties();
+                p.setProperty("password", currentPassword.get());
+                return p;
+            }
+            return null;
+        };
+
+        try (AgroalDataSource agroalDs = AgroalDataSource.from(
+                new AgroalDataSourceConfigurationSupplier()
+                        .metricsEnabled()
+                        .connectionPoolConfiguration(pool -> pool
+                                .maxSize(2)
+                                .minSize(1)
+                                .connectionFactoryConfiguration(factory -> 
factory
+                                        .jdbcUrl(postgres.jdbcUrl())
+                                        .principal(new 
NamePrincipal(TEST_USER))
+                                        .credential(new RotatingCredential())
+                                        .addSecurityProvider(rotating))))) {
+
+            // Register the Agroal DataSource in the registry so it can be 
discovered by the component
+            context.getRegistry().bind("agroalRotationDs", agroalDs);
+
+            // Build a temporary SqlComponent wired to the Agroal DataSource
+            SqlComponent agroalComponent = new SqlComponent();
+            agroalComponent.setDataSource(agroalDs);
+            agroalComponent.setCamelContext(context);
+
+            // Step 1: verify the pool works with cred A — grab a PID
+            int pidBefore;
+            try (Connection conn = agroalDs.getConnection();
+                 var rs = conn.createStatement().executeQuery("SELECT 
pg_backend_pid() AS pid")) {
+                assertTrue(rs.next(), "SELECT pg_backend_pid() must return a 
row");
+                pidBefore = rs.getInt("pid");
+                assertNotEquals(0, pidBefore, "pg_backend_pid() must return a 
non-zero value with credential A");
+            }
+
+            long flushCountBefore = agroalDs.getMetrics().flushCount();
+
+            // Step 2: change the database password to cred B via the admin 
connection
+            try (Connection adminConn = DriverManager.getConnection(
+                    postgres.jdbcUrl(), postgres.userName(), 
postgres.password());
+                 Statement st = adminConn.createStatement()) {
+                st.execute("ALTER ROLE " + TEST_USER + " PASSWORD '" + CRED_B 
+ "'");
+            }
+
+            // Verify old password no longer works for new connections
+            assertThrows(SQLException.class,
+                    () -> DriverManager.getConnection(postgres.jdbcUrl(), 
TEST_USER, CRED_A),
+                    "Old password must be rejected after ALTER ROLE");
+
+            // Step 3: update the rotating credential and trigger rotation
+            currentPassword.set(CRED_B);
+            agroalComponent.onSecretRotation("agroal-rotation");
+
+            // Step 4: wait for the flush and verify a new connection uses 
cred B
+            await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
+                long flushCountAfter = agroalDs.getMetrics().flushCount();
+                assertTrue(flushCountAfter > flushCountBefore,
+                        "Agroal flushCount must have increased after rotation 
— flush(GRACEFUL) must have been called");
+            });
+
+            // Step 5: verify the pool creates a new connection (different 
PID) with the new password
+            try (Connection conn = agroalDs.getConnection();
+                 var rs = conn.createStatement().executeQuery("SELECT 
pg_backend_pid() AS pid")) {

Review Comment:
   🔧 **Nit:** Same `Statement` resource-tracking issue — second occurrence.
   
   ```suggestion
               try (Connection conn = agroalDs.getConnection();
                    Statement st = conn.createStatement();
                    var rs = st.executeQuery("SELECT pg_backend_pid() AS pid")) 
{
   ```



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