pvillard31 commented on code in PR #11604:
URL: https://github.com/apache/nifi/pull/11604#discussion_r3901895327


##########
nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/test/java/org/apache/nifi/processors/gcp/cloudsql/GcpCloudSqlIamDatabasePasswordProviderTest.java:
##########
@@ -0,0 +1,1554 @@
+/*
+ * 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.nifi.processors.gcp.cloudsql;
+
+import com.google.auth.oauth2.AccessToken;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.auth.oauth2.IdentityPoolCredentials;
+import com.google.auth.oauth2.ImpersonatedCredentials;
+import org.apache.nifi.components.AllowableValue;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.PropertyValue;
+import org.apache.nifi.controller.AbstractControllerService;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.dbcp.api.DatabasePasswordProvider;
+import org.apache.nifi.dbcp.api.DatabasePasswordRequestContext;
+import org.apache.nifi.gcp.credentials.service.GCPCredentialsService;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.reporting.InitializationException;
+import org.apache.nifi.util.LogMessage;
+import org.apache.nifi.util.MockComponentLog;
+import org.apache.nifi.util.NoOpProcessor;
+import org.apache.nifi.util.TestRunner;
+import org.apache.nifi.util.TestRunners;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.Mockito;
+import org.slf4j.helpers.MessageFormatter;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Stream;
+
+import static 
org.apache.nifi.components.ConfigVerificationResult.Outcome.FAILED;
+import static 
org.apache.nifi.components.ConfigVerificationResult.Outcome.SUCCESSFUL;
+import static 
org.apache.nifi.processors.gcp.cloudsql.GcpCloudSqlIamDatabasePasswordProvider.DATABASE_TYPE;
+import static 
org.apache.nifi.processors.gcp.cloudsql.GcpCloudSqlIamDatabasePasswordProvider.FAILED_PASSWORD_MESSAGE;
+import static 
org.apache.nifi.processors.gcp.cloudsql.GcpCloudSqlIamDatabasePasswordProvider.GCP_CREDENTIALS_PROVIDER_SERVICE;
+import static 
org.apache.nifi.processors.gcp.cloudsql.GcpCloudSqlIamDatabasePasswordProvider.MALFORMED_MYSQL_JDBC_URL_MESSAGE;
+import static 
org.apache.nifi.processors.gcp.cloudsql.GcpCloudSqlIamDatabasePasswordProvider.MALFORMED_SSLMODE_MESSAGE;
+import static 
org.apache.nifi.processors.gcp.cloudsql.GcpCloudSqlIamDatabasePasswordProvider.SQLSERVICE_LOGIN_SCOPE;
+import static 
org.apache.nifi.processors.gcp.cloudsql.GcpCloudSqlIamDatabasePasswordProvider.VERIFY_CREDENTIALS_UNAVAILABLE;
+import static 
org.apache.nifi.processors.gcp.cloudsql.GcpCloudSqlIamDatabasePasswordProvider.VERIFY_IMPERSONATION_REQUIRED;
+import static 
org.apache.nifi.processors.gcp.cloudsql.GcpCloudSqlIamDatabasePasswordProvider.VERIFY_SCOPED_CREDENTIALS_UNAVAILABLE;
+import static 
org.apache.nifi.processors.gcp.cloudsql.GcpCloudSqlIamDatabasePasswordProvider.VERIFY_SCOPE_STEP;
+import static 
org.apache.nifi.processors.gcp.cloudsql.GcpCloudSqlIamDatabasePasswordProvider.VERIFY_TOKEN_ACQUISITION_FAILED;
+import static 
org.apache.nifi.processors.gcp.cloudsql.GcpCloudSqlIamDatabasePasswordProvider.VERIFY_TOKEN_MISSING;
+import static 
org.apache.nifi.processors.gcp.cloudsql.GcpCloudSqlIamDatabasePasswordProvider.VERIFY_TOKEN_STEP;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class GcpCloudSqlIamDatabasePasswordProviderTest {
+
+    private static final String CREDENTIALS_SERVICE_ID = "gcpCredentials";
+    private static final String PASSWORD_PROVIDER_ID = "cloudSqlIamProvider";
+    private static final String POSTGRES_DRIVER_CLASS = 
"org.postgresql.Driver";
+    private static final String MYSQL_DRIVER_CLASS = 
"com.mysql.cj.jdbc.Driver";
+    private static final String DATABASE_USER = 
"[email protected]";
+    private static final String MYSQL_DATABASE_USER = "service-account";
+    private static final String JDBC_URL = 
"jdbc:postgresql://example:5432/database?sslmode=require";
+    private static final String MYSQL_JDBC_URL = 
"jdbc:mysql://example:3306/database?sslMode=REQUIRED";
+    private static final String TOKEN_VALUE = "cloud-sql-token";
+    private static final String REFRESHED_TOKEN_VALUE = 
"refreshed-cloud-sql-token";
+    private static final String LEAK_SENTINEL = "sentinel-token-value";
+
+    private ExecutorService executorService;
+
+    @AfterEach
+    void tearDown() {
+        if (executorService != null) {
+            executorService.shutdownNow();
+        }
+    }
+
+    @Test
+    void 
testDatabaseTypeDescriptorSupportsPostgreSqlAndMySqlAndDefaultsToPostgreSql() {
+        final PropertyDescriptor descriptor = DATABASE_TYPE;
+
+        assertEquals(CloudSqlDatabaseType.POSTGRESQL.getValue(), 
descriptor.getDefaultValue());
+        assertTrue(descriptor.isRequired());
+        assertEquals("Database Type", descriptor.getName());
+        assertEquals(List.of(CloudSqlDatabaseType.POSTGRESQL.getValue(), 
CloudSqlDatabaseType.MYSQL.getValue()), descriptor.getAllowableValues().stream()
+                .map(AllowableValue::getValue)
+                .toList());
+    }
+
+    @Test
+    void testOnEnabledCachesScopedCredentialAndReusesIt() throws Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, 15));
+        final RootGoogleCredentials rootCredentials = new 
RootGoogleCredentials(scopedCredentials);
+        final TestRunner runner = configureRunner(rootCredentials);
+        final GcpCloudSqlIamDatabasePasswordProvider provider = 
getProviderImplementation(runner);
+
+        assertEquals(1, rootCredentials.getCreateScopedCount());
+        assertEquals(List.of(SQLSERVICE_LOGIN_SCOPE), 
rootCredentials.getLastRequestedScopes());
+        assertEquals(CloudSqlDatabaseType.POSTGRESQL, 
getDatabaseType(provider));
+        assertSame(scopedCredentials, getScopedCredentials(provider));
+
+        assertEquals(TOKEN_VALUE, new 
String(provider.getPassword(requestContext(JDBC_URL, DATABASE_USER, 
Map.of()))));
+        assertEquals(TOKEN_VALUE, new 
String(provider.getPassword(requestContext(JDBC_URL, DATABASE_USER, 
Map.of()))));
+
+        assertEquals(1, rootCredentials.getCreateScopedCount());
+        assertEquals(0, scopedCredentials.getRefreshAccessTokenCount());
+    }
+
+    @Test
+    void testOnDisabledClearsCachedCredentialAndDatabaseType() throws 
Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, 15));
+        final RootGoogleCredentials rootCredentials = new 
RootGoogleCredentials(scopedCredentials);
+        final TestRunner runner = configureRunner(rootCredentials);
+        final GcpCloudSqlIamDatabasePasswordProvider provider = 
getProviderImplementation(runner);
+
+        runner.disableControllerService(provider);
+
+        assertNull(getScopedCredentials(provider));
+        assertNull(getDatabaseType(provider));
+
+        final ProcessException exception = assertThrows(ProcessException.class,
+                () -> provider.getPassword(requestContext(JDBC_URL, 
DATABASE_USER, Map.of())));
+
+        assertEquals(FAILED_PASSWORD_MESSAGE, exception.getMessage());
+        assertNull(exception.getCause());
+    }
+
+    @Test
+    void testOnEnabledCachesScopedCredentialForMySqlAndUsesMySqlValidation() 
throws Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, 15));
+        final RootGoogleCredentials rootCredentials = new 
RootGoogleCredentials(scopedCredentials);
+        final TestRunner runner = configureRunner(rootCredentials, true, 
CloudSqlDatabaseType.MYSQL);
+        final GcpCloudSqlIamDatabasePasswordProvider provider = 
getProviderImplementation(runner);
+
+        assertEquals(1, rootCredentials.getCreateScopedCount());
+        assertEquals(List.of(SQLSERVICE_LOGIN_SCOPE), 
rootCredentials.getLastRequestedScopes());
+        assertEquals(CloudSqlDatabaseType.MYSQL, getDatabaseType(provider));
+        assertSame(scopedCredentials, getScopedCredentials(provider));
+
+        assertEquals(TOKEN_VALUE, new 
String(provider.getPassword(requestContext(MYSQL_JDBC_URL, MYSQL_DATABASE_USER, 
MYSQL_DRIVER_CLASS, Map.of()))));
+        assertEquals(0, scopedCredentials.getRefreshAccessTokenCount());
+    }
+
+    @Test
+    void testOnDisabledClearsCachedCredentialAndDatabaseTypeForMySql() throws 
Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, 15));
+        final RootGoogleCredentials rootCredentials = new 
RootGoogleCredentials(scopedCredentials);
+        final TestRunner runner = configureRunner(rootCredentials, true, 
CloudSqlDatabaseType.MYSQL);
+        final GcpCloudSqlIamDatabasePasswordProvider provider = 
getProviderImplementation(runner);
+
+        runner.disableControllerService(provider);
+
+        assertNull(getScopedCredentials(provider));
+        assertNull(getDatabaseType(provider));
+
+        final ProcessException exception = assertThrows(ProcessException.class,
+                () -> provider.getPassword(requestContext(MYSQL_JDBC_URL, 
MYSQL_DATABASE_USER, MYSQL_DRIVER_CLASS, Map.of())));
+
+        assertEquals(FAILED_PASSWORD_MESSAGE, exception.getMessage());
+        assertNull(exception.getCause());
+    }
+
+    @Test
+    void testVerifyImpersonatedCredentialsAcquireLiveToken() throws Exception {
+        final ImpersonatedCredentials scopedCredentials = 
impersonatedCredentials(accessToken(TOKEN_VALUE, 15));
+        final RootGoogleCredentials rootCredentials = new 
RootGoogleCredentials(scopedCredentials);
+        final TestRunner runner = configureRunner(rootCredentials);
+        final GcpCloudSqlIamDatabasePasswordProvider provider = 
getProviderImplementation(runner);
+
+        final List<ConfigVerificationResult> results = runner.verify(provider, 
Map.of());
+
+        assertEquals(2, results.size());
+        assertVerificationResult(results.get(0), VERIFY_SCOPE_STEP, SUCCESSFUL,
+                "Resolved Database Type PostgreSQL");
+        assertVerificationResult(results.get(0), VERIFY_SCOPE_STEP, SUCCESSFUL,
+                "created a Cloud SQL scoped ImpersonatedCredentials instance. 
Target service account impersonation is active.");
+        assertVerificationResult(results.get(1), VERIFY_TOKEN_STEP, SUCCESSFUL,
+                "Acquired a non-empty Cloud SQL IAM access token for 
PostgreSQL");
+        assertVerificationResult(results.get(1), VERIFY_TOKEN_STEP, SUCCESSFUL,
+                "verifies live subject token exchange, Google STS, and target 
service account impersonation");
+        assertEquals(2, rootCredentials.getCreateScopedCount());
+        Mockito.verify(scopedCredentials).refreshAccessToken();
+    }
+
+    @Test
+    void testVerifyIdentityPoolCredentialsRequiresImpersonation() throws 
Exception {
+        final IdentityPoolCredentials scopedCredentials = 
identityPoolCredentials(accessToken(TOKEN_VALUE, 15));
+        final RootGoogleCredentials rootCredentials = new 
RootGoogleCredentials(scopedCredentials);
+        final TestRunner runner = configureRunner(rootCredentials, false);
+        final GcpCloudSqlIamDatabasePasswordProvider provider = 
getProviderImplementation(runner);
+
+        final List<ConfigVerificationResult> results = runner.verify(provider, 
Map.of());
+
+        assertEquals(1, results.size());
+        assertVerificationResult(results.getFirst(), VERIFY_SCOPE_STEP, 
FAILED, "Resolved Database Type PostgreSQL");
+        assertVerificationResult(results.getFirst(), VERIFY_SCOPE_STEP, 
FAILED, VERIFY_IMPERSONATION_REQUIRED);
+        Mockito.verify(scopedCredentials, 
Mockito.never()).refreshAccessToken();
+    }
+
+    @Test
+    void testOnEnabledRejectsIdentityPoolCredentialsBeforePublishingState() 
throws Exception {
+        final IdentityPoolCredentials scopedCredentials = 
identityPoolCredentials(accessToken(TOKEN_VALUE, 15));
+        final GcpCloudSqlIamDatabasePasswordProvider provider = new 
GcpCloudSqlIamDatabasePasswordProvider();
+        final ConfigurationContext context = mock(ConfigurationContext.class);
+        final PropertyValue credentialsPropertyValue = 
mock(PropertyValue.class);
+        final PropertyValue databaseTypePropertyValue = 
mock(PropertyValue.class);
+        final GCPCredentialsService credentialsService = 
mock(GCPCredentialsService.class);
+
+        
when(context.getProperty(DATABASE_TYPE)).thenReturn(databaseTypePropertyValue);
+        
when(databaseTypePropertyValue.asAllowableValue(CloudSqlDatabaseType.class)).thenReturn(CloudSqlDatabaseType.POSTGRESQL);
+        
when(context.getProperty(GCP_CREDENTIALS_PROVIDER_SERVICE)).thenReturn(credentialsPropertyValue);
+        
when(credentialsPropertyValue.asControllerService(GCPCredentialsService.class)).thenReturn(credentialsService);
+        when(credentialsService.getGoogleCredentials()).thenReturn(new 
RootGoogleCredentials(scopedCredentials));
+
+        final InitializationException exception = 
assertThrows(InitializationException.class,
+                () -> provider.onEnabled(context));
+
+        assertEquals(VERIFY_IMPERSONATION_REQUIRED, exception.getMessage());
+        assertNull(getScopedCredentials(provider));
+        assertNull(getDatabaseType(provider));
+        Mockito.verify(scopedCredentials, 
Mockito.never()).refreshAccessToken();
+    }
+
+    @Test
+    void testGetPasswordRejectsIdentityPoolCredentialsBeforeRefresh() throws 
Exception {
+        final IdentityPoolCredentials scopedCredentials = 
identityPoolCredentials(accessToken(TOKEN_VALUE, -15));
+        final GcpCloudSqlIamDatabasePasswordProvider provider = new 
GcpCloudSqlIamDatabasePasswordProvider();
+        setDatabaseType(provider, CloudSqlDatabaseType.POSTGRESQL);
+        setScopedCredentials(provider, scopedCredentials);
+
+        final ProcessException exception = assertThrows(ProcessException.class,
+                () -> provider.getPassword(requestContext(JDBC_URL, 
DATABASE_USER, Map.of())));
+
+        assertEquals(FAILED_PASSWORD_MESSAGE, exception.getMessage());
+        assertNull(exception.getCause());
+        Mockito.verify(scopedCredentials, 
Mockito.never()).refreshAccessToken();
+    }
+
+    @Test
+    void testInvalidDatabaseTypePropertyIsRejectedByValidation() throws 
Exception {
+        final TestRunner runner = 
TestRunners.newTestRunner(NoOpProcessor.class);
+
+        final TestGCPCredentialsService credentialsService = new 
TestGCPCredentialsService(new RootGoogleCredentials(new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, 15))));
+        runner.addControllerService(CREDENTIALS_SERVICE_ID, 
credentialsService);
+        runner.enableControllerService(credentialsService);
+
+        final GcpCloudSqlIamDatabasePasswordProvider provider = new 
GcpCloudSqlIamDatabasePasswordProvider();
+        runner.addControllerService(PASSWORD_PROVIDER_ID, provider);
+        runner.setProperty(provider, GCP_CREDENTIALS_PROVIDER_SERVICE, 
CREDENTIALS_SERVICE_ID);
+        runner.setProperty(provider, DATABASE_TYPE, "SQLSERVER");
+
+        runner.assertNotValid(provider);
+    }
+
+    @Test
+    void testVerifyNullCredentialsFails() throws Exception {
+        final TestRunner runner = configureRunner(null, false);
+        final GcpCloudSqlIamDatabasePasswordProvider provider = 
getProviderImplementation(runner);
+
+        final List<ConfigVerificationResult> results = runner.verify(provider, 
Map.of());
+
+        assertEquals(1, results.size());
+        assertVerificationResult(results.getFirst(), VERIFY_SCOPE_STEP, 
FAILED, VERIFY_CREDENTIALS_UNAVAILABLE);
+    }
+
+    @Test
+    void testVerifyScopedCredentialCreationReturningNullFails() throws 
Exception {
+        final RootGoogleCredentials rootCredentials = new 
RootGoogleCredentials((GoogleCredentials) null);
+        final TestRunner runner = configureRunner(rootCredentials, false);
+        final GcpCloudSqlIamDatabasePasswordProvider provider = 
getProviderImplementation(runner);
+
+        final List<ConfigVerificationResult> results = runner.verify(provider, 
Map.of());
+
+        assertEquals(1, results.size());
+        assertVerificationResult(results.getFirst(), VERIFY_SCOPE_STEP, 
FAILED, VERIFY_SCOPED_CREDENTIALS_UNAVAILABLE);
+        assertEquals(List.of(SQLSERVICE_LOGIN_SCOPE), 
rootCredentials.getLastRequestedScopes());
+    }
+
+    @Test
+    void testVerifyScopedCredentialCreationFailureIsSanitized() throws 
Exception {
+        final RootGoogleCredentials rootCredentials = new 
RootGoogleCredentials(new IllegalStateException(LEAK_SENTINEL));
+        final TestRunner runner = configureRunner(rootCredentials, false);
+        final GcpCloudSqlIamDatabasePasswordProvider provider = 
getProviderImplementation(runner);
+
+        final List<ConfigVerificationResult> results = runner.verify(provider, 
Map.of());
+
+        assertEquals(1, results.size());
+        assertVerificationResult(results.getFirst(), VERIFY_SCOPE_STEP, 
FAILED, VERIFY_SCOPED_CREDENTIALS_UNAVAILABLE);
+        
assertFalse(results.getFirst().getExplanation().contains(LEAK_SENTINEL));
+        
assertNoLogMessagesContain(runner.getControllerServiceLogger(PASSWORD_PROVIDER_ID),
 LEAK_SENTINEL);
+    }
+
+    @Test
+    void testVerifyRefreshIOExceptionIsSanitized() throws Exception {
+        final ImpersonatedCredentials scopedCredentials = 
impersonatedCredentials(ioException(LEAK_SENTINEL, 
"com.google.auth.oauth2.ImpersonatedCredentials"));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials));
+        final GcpCloudSqlIamDatabasePasswordProvider provider = 
getProviderImplementation(runner);
+
+        final List<ConfigVerificationResult> results = runner.verify(provider, 
Map.of());
+
+        assertEquals(2, results.size());
+        assertVerificationResult(results.get(0), VERIFY_SCOPE_STEP, SUCCESSFUL,
+                "created a Cloud SQL scoped ImpersonatedCredentials instance. 
Target service account impersonation is active.");
+        assertVerificationResult(results.get(1), VERIFY_TOKEN_STEP, FAILED, 
VERIFY_TOKEN_ACQUISITION_FAILED);
+        assertFalse(results.get(1).getExplanation().contains(LEAK_SENTINEL));
+        
assertNoLogMessagesContain(runner.getControllerServiceLogger(PASSWORD_PROVIDER_ID),
 LEAK_SENTINEL);
+    }
+
+    @Test
+    void testVerifyRefreshRuntimeFailureIsSanitized() throws Exception {
+        final ImpersonatedCredentials scopedCredentials = 
impersonatedCredentials(new IllegalStateException(LEAK_SENTINEL));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials));
+        final GcpCloudSqlIamDatabasePasswordProvider provider = 
getProviderImplementation(runner);
+
+        final List<ConfigVerificationResult> results = runner.verify(provider, 
Map.of());
+
+        assertEquals(2, results.size());
+        assertVerificationResult(results.get(1), VERIFY_TOKEN_STEP, FAILED, 
VERIFY_TOKEN_ACQUISITION_FAILED);
+        
assertNoLogMessagesContain(runner.getControllerServiceLogger(PASSWORD_PROVIDER_ID),
 LEAK_SENTINEL);
+    }
+
+    @Test
+    void testVerifyNullAccessTokenFails() throws Exception {
+        final ImpersonatedCredentials scopedCredentials = 
impersonatedCredentials((AccessToken) null);
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials));
+        final GcpCloudSqlIamDatabasePasswordProvider provider = 
getProviderImplementation(runner);
+
+        final List<ConfigVerificationResult> results = runner.verify(provider, 
Map.of());
+
+        assertEquals(2, results.size());
+        assertVerificationResult(results.get(1), VERIFY_TOKEN_STEP, FAILED, 
VERIFY_TOKEN_MISSING);
+    }
+
+    @Test
+    void testVerifyBlankAccessTokenFails() throws Exception {
+        final ImpersonatedCredentials scopedCredentials = 
impersonatedCredentials(accessToken(" ", 15));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials));
+        final GcpCloudSqlIamDatabasePasswordProvider provider = 
getProviderImplementation(runner);
+
+        final List<ConfigVerificationResult> results = runner.verify(provider, 
Map.of());
+
+        assertEquals(2, results.size());
+        assertVerificationResult(results.get(1), VERIFY_TOKEN_STEP, FAILED, 
VERIFY_TOKEN_MISSING);
+    }
+
+    @Test
+    void testVerifyUsesFreshScopedCredentialWithoutMutatingEnabledState() 
throws Exception {
+        final TestScopedGoogleCredentials enabledScopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, 15));
+        final TestScopedGoogleCredentials verificationScopedCredentials = new 
TestScopedGoogleCredentials(accessToken(REFRESHED_TOKEN_VALUE, 15));
+        
verificationScopedCredentials.setRefreshedAccessToken(accessToken(REFRESHED_TOKEN_VALUE,
 15));
+        final RootGoogleCredentials rootCredentials = new 
RootGoogleCredentials(enabledScopedCredentials, verificationScopedCredentials);
+        final TestRunner runner = configureRunner(rootCredentials);
+        final GcpCloudSqlIamDatabasePasswordProvider provider = 
getProviderImplementation(runner);
+
+        final List<ConfigVerificationResult> results = runner.verify(provider, 
Map.of());
+        final char[] password = provider.getPassword(requestContext(JDBC_URL, 
DATABASE_USER, Map.of()));
+
+        assertEquals(2, rootCredentials.getCreateScopedCount());
+        assertEquals(0, enabledScopedCredentials.getRefreshAccessTokenCount());
+        assertEquals(1, 
verificationScopedCredentials.getRefreshAccessTokenCount());
+        assertFalse(results.get(1).getExplanation().contains("subject token 
exchange"));
+        assertTrue(results.get(1).getExplanation().contains("Cloud SQL IAM 
token acquisition for the current principal"));
+        assertVerificationResult(results.get(1), VERIFY_TOKEN_STEP, SUCCESSFUL,
+                "Use DBCP Verify for the end-to-end database check.");
+        assertArrayEquals(TOKEN_VALUE.toCharArray(), password);
+    }
+
+    @Test
+    void testVerifyUsesSqlServiceLoginScope() throws Exception {
+        final GcpCloudSqlIamDatabasePasswordProvider provider = new 
GcpCloudSqlIamDatabasePasswordProvider();
+        final ConfigurationContext context = mock(ConfigurationContext.class);
+        final PropertyValue credentialsPropertyValue = 
mock(PropertyValue.class);
+        final PropertyValue databaseTypePropertyValue = 
mock(PropertyValue.class);
+        final GCPCredentialsService credentialsService = 
mock(GCPCredentialsService.class);
+        final RootGoogleCredentials rootCredentials = new 
RootGoogleCredentials(new TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, 
15)));
+
+        
when(context.getProperty(DATABASE_TYPE)).thenReturn(databaseTypePropertyValue);
+        
when(databaseTypePropertyValue.asAllowableValue(CloudSqlDatabaseType.class)).thenReturn(CloudSqlDatabaseType.POSTGRESQL);
+        
when(context.getProperty(GCP_CREDENTIALS_PROVIDER_SERVICE)).thenReturn(credentialsPropertyValue);
+        
when(credentialsPropertyValue.asControllerService(GCPCredentialsService.class)).thenReturn(credentialsService);
+        
when(credentialsService.getGoogleCredentials()).thenReturn(rootCredentials);
+
+        final List<ConfigVerificationResult> results = 
provider.verify(context, mock(ComponentLog.class), Map.of());
+
+        assertEquals(2, results.size());
+        assertEquals(List.of(SQLSERVICE_LOGIN_SCOPE), 
rootCredentials.getLastRequestedScopes());
+        assertVerificationResult(results.getFirst(), VERIFY_SCOPE_STEP, 
SUCCESSFUL, "Resolved Database Type PostgreSQL");
+    }
+
+    @ParameterizedTest(name = "verify wording for {0}")
+    @MethodSource("verifySuccessContexts")
+    void testVerifySuccessWordingIsGenericForSelectedDatabaseType(final 
CloudSqlDatabaseType databaseType,
+                                                                  final 
GoogleCredentials scopedCredentials,
+                                                                  final String 
scopeMessage,
+                                                                  final String 
tokenMessage) throws Exception {
+        if (scopedCredentials instanceof TestScopedGoogleCredentials 
testScopedGoogleCredentials) {
+            
testScopedGoogleCredentials.setRefreshedAccessToken(accessToken(TOKEN_VALUE, 
15));
+        }
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials), true, databaseType);
+        final GcpCloudSqlIamDatabasePasswordProvider provider = 
getProviderImplementation(runner);
+
+        final List<ConfigVerificationResult> results = runner.verify(provider, 
Map.of());
+
+        assertEquals(2, results.size());
+        assertVerificationResult(results.get(0), VERIFY_SCOPE_STEP, 
SUCCESSFUL, "Resolved Database Type 
%s".formatted(databaseType.getDisplayName()));
+        assertVerificationResult(results.get(0), VERIFY_SCOPE_STEP, 
SUCCESSFUL, scopeMessage);
+        assertVerificationResult(results.get(1), VERIFY_TOKEN_STEP, SUCCESSFUL,
+                "Acquired a non-empty Cloud SQL IAM access token for 
%s".formatted(databaseType.getDisplayName()));
+        assertVerificationResult(results.get(1), VERIFY_TOKEN_STEP, 
SUCCESSFUL, tokenMessage);
+        assertVerificationResult(results.get(1), VERIFY_TOKEN_STEP, SUCCESSFUL,
+                "does not connect to the selected database. Use DBCP Verify 
for the end-to-end database check.");
+    }
+
+    @Test
+    void testChainedControllerServiceResolution() throws Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, 15));
+        final RootGoogleCredentials rootCredentials = new 
RootGoogleCredentials(scopedCredentials);
+        final TestRunner runner = configureRunner(rootCredentials);
+
+        final DatabasePasswordProvider provider = getProvider(runner);
+        final char[] password = provider.getPassword(requestContext(JDBC_URL, 
DATABASE_USER, Map.of()));
+
+        assertArrayEquals(TOKEN_VALUE.toCharArray(), password);
+    }
+
+    @Test
+    void testFreshTokenDoesNotRefresh() throws Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, 15));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials));
+
+        final DatabasePasswordProvider provider = getProvider(runner);
+        final char[] password = provider.getPassword(requestContext(JDBC_URL, 
DATABASE_USER, Map.of()));
+
+        assertArrayEquals(TOKEN_VALUE.toCharArray(), password);
+        assertEquals(0, scopedCredentials.getRefreshAccessTokenCount());
+    }
+
+    @Test
+    void testExpiredTokenRefreshes() throws Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, -15));
+        
scopedCredentials.setRefreshedAccessToken(accessToken(REFRESHED_TOKEN_VALUE, 
15));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials));
+
+        final DatabasePasswordProvider provider = getProvider(runner);
+        final char[] password = provider.getPassword(requestContext(JDBC_URL, 
DATABASE_USER, Map.of()));
+
+        assertArrayEquals(REFRESHED_TOKEN_VALUE.toCharArray(), password);
+        assertEquals(1, scopedCredentials.getRefreshAccessTokenCount());
+    }
+
+    @Test
+    void testConcurrentGetPasswordPerformsSingleRefresh() throws Exception {
+        final BlockingScopedGoogleCredentials scopedCredentials = new 
BlockingScopedGoogleCredentials();
+        
scopedCredentials.setRefreshedAccessToken(accessToken(REFRESHED_TOKEN_VALUE, 
15));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials));
+        final DatabasePasswordProvider provider = getProvider(runner);
+
+        executorService = Executors.newFixedThreadPool(2);
+        final CountDownLatch startLatch = new CountDownLatch(1);
+        final Future<char[]> first = executorService.submit(() -> 
getPasswordAfterStart(provider, startLatch));
+        final Future<char[]> second = executorService.submit(() -> 
getPasswordAfterStart(provider, startLatch));
+
+        startLatch.countDown();
+        assertTrue(scopedCredentials.awaitRefreshEntry());
+        scopedCredentials.releaseRefresh();
+
+        assertArrayEquals(REFRESHED_TOKEN_VALUE.toCharArray(), first.get(5, 
TimeUnit.SECONDS));
+        assertArrayEquals(REFRESHED_TOKEN_VALUE.toCharArray(), second.get(5, 
TimeUnit.SECONDS));
+        assertEquals(1, scopedCredentials.getRefreshAccessTokenCount());
+    }
+
+    @Test
+    void testNullRequestContextRejected() throws Exception {
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(new TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, 
15))));
+        final DatabasePasswordProvider provider = getProvider(runner);
+
+        final NullPointerException exception = 
assertThrows(NullPointerException.class, () -> provider.getPassword(null));
+
+        assertEquals("Database Password Request Context required", 
exception.getMessage());
+    }
+
+    @Test
+    void testBlankDatabaseUserRejectedBeforeRefresh() throws Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, -15));
+        
scopedCredentials.setRefreshedAccessToken(accessToken(REFRESHED_TOKEN_VALUE, 
15));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials));
+        final DatabasePasswordProvider provider = getProvider(runner);
+
+        final ProcessException exception = assertThrows(ProcessException.class,
+                () -> provider.getPassword(requestContext(JDBC_URL, " ", 
Map.of())));
+
+        assertEquals("Database Username must be configured for Cloud SQL IAM 
authentication", exception.getMessage());
+        assertEquals(0, scopedCredentials.getRefreshAccessTokenCount());
+    }
+
+    @Test
+    void testBlankMySqlDatabaseUserRejectedBeforeRefresh() throws Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, -15));
+        
scopedCredentials.setRefreshedAccessToken(accessToken(REFRESHED_TOKEN_VALUE, 
15));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials), true, CloudSqlDatabaseType.MYSQL);
+        final DatabasePasswordProvider provider = getProvider(runner);
+
+        final ProcessException exception = assertThrows(ProcessException.class,
+                () -> provider.getPassword(requestContext(MYSQL_JDBC_URL, " ", 
MYSQL_DRIVER_CLASS, Map.of())));
+
+        assertEquals("Database Username must be configured for Cloud SQL IAM 
authentication", exception.getMessage());
+        assertEquals(0, scopedCredentials.getRefreshAccessTokenCount());
+    }
+
+    @ParameterizedTest(name = "accepted sslmode {0} from URL")
+    @MethodSource("acceptedUrlSslModes")
+    void testAcceptedUrlSslModes(final String sslMode) throws Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, 15));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials));
+        final DatabasePasswordProvider provider = getProvider(runner);
+
+        final char[] password = provider.getPassword(requestContext(
+                
"jdbc:postgresql://example:5432/database?sslmode=%s".formatted(sslMode),
+                DATABASE_USER,
+                Map.of("sslmode", "disable")
+        ));
+
+        assertArrayEquals(TOKEN_VALUE.toCharArray(), password);
+    }
+
+    @Test
+    void testAcceptedCaseInsensitiveUrlSslModeNameAndDecodedValue() throws 
Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, 15));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials));
+        final DatabasePasswordProvider provider = getProvider(runner);
+
+        final char[] password = provider.getPassword(requestContext(
+                
"jdbc:postgresql://example:5432/database?SslMode=verify%2Dfull",
+                DATABASE_USER,
+                Map.of("sslmode", "disable")
+        ));
+
+        assertArrayEquals(TOKEN_VALUE.toCharArray(), password);
+    }
+
+    @ParameterizedTest(name = "accepted sslmode {0} from connection 
properties")
+    @MethodSource("acceptedPropertySslModes")
+    void testAcceptedConnectionPropertySslModes(final String sslMode) throws 
Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, 15));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials));
+        final DatabasePasswordProvider provider = getProvider(runner);
+
+        final char[] password = provider.getPassword(requestContext(
+                "jdbc:postgresql://example:5432/database",
+                DATABASE_USER,
+                Map.of("sslmode", sslMode)
+        ));
+
+        assertArrayEquals(TOKEN_VALUE.toCharArray(), password);
+    }
+
+    @Test
+    void testAcceptedCaseInsensitiveConnectionPropertyName() throws Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, 15));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials));
+        final DatabasePasswordProvider provider = getProvider(runner);
+
+        final char[] password = provider.getPassword(requestContext(
+                "jdbc:postgresql://example:5432/database",
+                DATABASE_USER,
+                Map.of("SSLMODE", "require")
+        ));
+
+        assertArrayEquals(TOKEN_VALUE.toCharArray(), password);
+    }
+
+    @Test
+    void testMissingSslModeRejectedBeforeRefresh() throws Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, -15));
+        
scopedCredentials.setRefreshedAccessToken(accessToken(REFRESHED_TOKEN_VALUE, 
15));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials));
+        final DatabasePasswordProvider provider = getProvider(runner);
+
+        final ProcessException exception = assertThrows(ProcessException.class,
+                () -> 
provider.getPassword(requestContext("jdbc:postgresql://example:5432/database", 
DATABASE_USER, Map.of())));
+
+        assertEquals("PostgreSQL sslmode must be configured for Cloud SQL IAM 
authentication", exception.getMessage());
+        assertEquals(0, scopedCredentials.getRefreshAccessTokenCount());
+    }
+
+    @ParameterizedTest(name = "rejected sslmode {0}")
+    @MethodSource("rejectedSslModeContexts")
+    void testRejectedSslModes(final String jdbcUrl, final Map<String, String> 
connectionProperties, final String expectedSslMode) throws Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, -15));
+        
scopedCredentials.setRefreshedAccessToken(accessToken(REFRESHED_TOKEN_VALUE, 
15));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials));
+        final DatabasePasswordProvider provider = getProvider(runner);
+
+        final ProcessException exception = assertThrows(ProcessException.class,
+                () -> provider.getPassword(requestContext(jdbcUrl, 
DATABASE_USER, connectionProperties)));
+
+        assertEquals("PostgreSQL sslmode [%s] is not supported for Cloud SQL 
IAM authentication".formatted(expectedSslMode), exception.getMessage());
+        assertEquals(0, scopedCredentials.getRefreshAccessTokenCount());
+    }
+
+    @Test
+    void testUrlSslModeTakesPrecedenceOverConnectionProperties() throws 
Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, 15));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials));
+        final DatabasePasswordProvider provider = getProvider(runner);
+
+        final char[] password = provider.getPassword(requestContext(
+                "jdbc:postgresql://example:5432/database?sslmode=require",
+                DATABASE_USER,
+                Map.of("sslmode", "disable")
+        ));
+
+        assertArrayEquals(TOKEN_VALUE.toCharArray(), password);
+    }
+
+    @Test
+    void testDuplicateUrlSslModeLastInsecureValueRejectedBeforeRefresh() 
throws Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, -15));
+        
scopedCredentials.setRefreshedAccessToken(accessToken(REFRESHED_TOKEN_VALUE, 
15));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials));
+        final DatabasePasswordProvider provider = getProvider(runner);
+
+        final ProcessException exception = assertThrows(ProcessException.class,
+                () -> provider.getPassword(requestContext(
+                        
"jdbc:postgresql://example:5432/database?sslmode=require&sslmode=disable",
+                        DATABASE_USER,
+                        Map.of("sslmode", "verify-full")
+                )));
+
+        assertEquals("PostgreSQL sslmode [disable] is not supported for Cloud 
SQL IAM authentication", exception.getMessage());
+        assertEquals(0, scopedCredentials.getRefreshAccessTokenCount());
+    }
+
+    @Test
+    void testDuplicateUrlSslModeLastSecureValueAccepted() throws Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, 15));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials));
+        final DatabasePasswordProvider provider = getProvider(runner);
+
+        final char[] password = provider.getPassword(requestContext(
+                
"jdbc:postgresql://example:5432/database?sslmode=disable&sslmode=require",
+                DATABASE_USER,
+                Map.of("sslmode", "disable")
+        ));
+
+        assertArrayEquals(TOKEN_VALUE.toCharArray(), password);
+    }
+
+    @Test
+    void testMalformedUrlEncodedSslModeValueRejectedBeforeRefresh() throws 
Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, -15));
+        
scopedCredentials.setRefreshedAccessToken(accessToken(REFRESHED_TOKEN_VALUE, 
15));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials));
+        final DatabasePasswordProvider provider = getProvider(runner);
+
+        final ProcessException exception = assertThrows(ProcessException.class,
+                () -> provider.getPassword(requestContext(
+                        "jdbc:postgresql://example:5432/database?sslmode=%GG",
+                        DATABASE_USER,
+                        Map.of("sslmode", "require")
+                )));
+
+        assertEquals(MALFORMED_SSLMODE_MESSAGE, exception.getMessage());
+        assertNull(exception.getCause());
+        assertEquals(0, scopedCredentials.getRefreshAccessTokenCount());
+    }
+
+    @Test
+    void testMalformedUrlEncodedSslModeNameRejectedBeforeRefresh() throws 
Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, -15));
+        
scopedCredentials.setRefreshedAccessToken(accessToken(REFRESHED_TOKEN_VALUE, 
15));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials));
+        final DatabasePasswordProvider provider = getProvider(runner);
+
+        final ProcessException exception = assertThrows(ProcessException.class,
+                () -> provider.getPassword(requestContext(
+                        
"jdbc:postgresql://example:5432/database?sslmo%G=require",
+                        DATABASE_USER,
+                        Map.of("sslmode", "require")
+                )));
+
+        assertEquals(MALFORMED_SSLMODE_MESSAGE, exception.getMessage());
+        assertNull(exception.getCause());
+        assertEquals(0, scopedCredentials.getRefreshAccessTokenCount());
+    }
+
+    @ParameterizedTest(name = "accepted MySQL sslMode {0} from URL")
+    @MethodSource("acceptedMySqlUrlSslModes")
+    void testAcceptedMySqlUrlSslModes(final String sslMode) throws Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, 15));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials), true, CloudSqlDatabaseType.MYSQL);
+        final DatabasePasswordProvider provider = getProvider(runner);
+
+        final char[] password = provider.getPassword(requestContext(
+                
"jdbc:mysql://example:3306/database?sslMode=%s".formatted(sslMode),
+                MYSQL_DATABASE_USER,
+                MYSQL_DRIVER_CLASS,
+                Map.of()
+        ));
+
+        assertArrayEquals(TOKEN_VALUE.toCharArray(), password);
+    }
+
+    @ParameterizedTest(name = "accepted MySQL sslMode {0} from connection 
properties")
+    @MethodSource("acceptedMySqlPropertySslModes")
+    void testAcceptedMySqlConnectionPropertySslModes(final String sslMode) 
throws Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, 15));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials), true, CloudSqlDatabaseType.MYSQL);
+        final DatabasePasswordProvider provider = getProvider(runner);
+
+        final char[] password = provider.getPassword(requestContext(
+                "jdbc:mysql://example:3306/database",
+                MYSQL_DATABASE_USER,
+                MYSQL_DRIVER_CLASS,
+                Map.of("sslMode", sslMode)
+        ));
+
+        assertArrayEquals(TOKEN_VALUE.toCharArray(), password);
+    }
+
+    @Test
+    void 
testRejectedMySqlCaseInsensitiveUrlSslModeNameAndDecodedValueBeforeRefresh() 
throws Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, -15));
+        
scopedCredentials.setRefreshedAccessToken(accessToken(REFRESHED_TOKEN_VALUE, 
15));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials), true, CloudSqlDatabaseType.MYSQL);
+        final DatabasePasswordProvider provider = getProvider(runner);
+
+        final ProcessException exception = assertThrows(ProcessException.class,
+                () -> provider.getPassword(requestContext(
+                        
"jdbc:mysql://example:3306/database?SslMode=verify%5Fidentity",
+                        MYSQL_DATABASE_USER,
+                        MYSQL_DRIVER_CLASS,
+                        Map.of("sslMode", "VERIFY_CA")
+                )));
+
+        assertEquals("MySQL sslMode must be configured as REQUIRED, VERIFY_CA, 
or VERIFY_IDENTITY for Cloud SQL IAM authentication", exception.getMessage());
+        assertEquals(0, scopedCredentials.getRefreshAccessTokenCount());
+    }
+
+    @Test
+    void testRejectedMySqlCaseInsensitiveConnectionPropertyNameBeforeRefresh() 
throws Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, -15));
+        
scopedCredentials.setRefreshedAccessToken(accessToken(REFRESHED_TOKEN_VALUE, 
15));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials), true, CloudSqlDatabaseType.MYSQL);
+        final DatabasePasswordProvider provider = getProvider(runner);
+
+        final ProcessException exception = assertThrows(ProcessException.class,
+                () -> provider.getPassword(requestContext(
+                        "jdbc:mysql://example:3306/database",
+                        MYSQL_DATABASE_USER,
+                        MYSQL_DRIVER_CLASS,
+                        Map.of("SSLMODE", "required")
+                )));
+
+        assertEquals("MySQL sslMode must be configured as REQUIRED, VERIFY_CA, 
or VERIFY_IDENTITY for Cloud SQL IAM authentication", exception.getMessage());
+        assertEquals(0, scopedCredentials.getRefreshAccessTokenCount());
+    }
+
+    @Test
+    void 
testMySqlConnectionPropertiesSslModeTakesPrecedenceOverJdbcUrlBeforeRefresh() 
throws Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, -15));
+        
scopedCredentials.setRefreshedAccessToken(accessToken(REFRESHED_TOKEN_VALUE, 
15));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials), true, CloudSqlDatabaseType.MYSQL);
+        final DatabasePasswordProvider provider = getProvider(runner);
+
+        final ProcessException exception = assertThrows(ProcessException.class,
+                () -> provider.getPassword(requestContext(
+                        "jdbc:mysql://example:3306/database?sslMode=REQUIRED",
+                        MYSQL_DATABASE_USER,
+                        MYSQL_DRIVER_CLASS,
+                        Map.of("sslMode", "DISABLED")
+                )));
+
+        assertEquals("MySQL sslMode must be configured as REQUIRED, VERIFY_CA, 
or VERIFY_IDENTITY for Cloud SQL IAM authentication", exception.getMessage());
+        assertEquals(0, scopedCredentials.getRefreshAccessTokenCount());
+    }
+
+    @Test
+    void testMySqlDuplicateUrlSslModeLastInsecureValueRejectedBeforeRefresh() 
throws Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, -15));
+        
scopedCredentials.setRefreshedAccessToken(accessToken(REFRESHED_TOKEN_VALUE, 
15));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials), true, CloudSqlDatabaseType.MYSQL);
+        final DatabasePasswordProvider provider = getProvider(runner);
+
+        final ProcessException exception = assertThrows(ProcessException.class,
+                () -> provider.getPassword(requestContext(
+                        
"jdbc:mysql://example:3306/database?sslMode=REQUIRED&sslMode=DISABLED",
+                        MYSQL_DATABASE_USER,
+                        MYSQL_DRIVER_CLASS,
+                        Map.of()
+                )));
+
+        assertEquals("MySQL sslMode must be configured as REQUIRED, VERIFY_CA, 
or VERIFY_IDENTITY for Cloud SQL IAM authentication", exception.getMessage());
+        assertEquals(0, scopedCredentials.getRefreshAccessTokenCount());
+    }
+
+    @Test
+    void testMySqlDuplicateUrlSslModeLastSecureValueAccepted() throws 
Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, 15));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials), true, CloudSqlDatabaseType.MYSQL);
+        final DatabasePasswordProvider provider = getProvider(runner);
+
+        final char[] password = provider.getPassword(requestContext(
+                
"jdbc:mysql://example:3306/database?sslMode=PREFERRED&sslMode=VERIFY_CA",
+                MYSQL_DATABASE_USER,
+                MYSQL_DRIVER_CLASS,
+                Map.of()
+        ));
+
+        assertArrayEquals(TOKEN_VALUE.toCharArray(), password);
+    }
+
+    @Test
+    void 
testMySqlExactConnectionPropertySslModeOverridesMissingJdbcUrlSslMode() throws 
Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, 15));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials), true, CloudSqlDatabaseType.MYSQL);
+        final DatabasePasswordProvider provider = getProvider(runner);
+
+        final char[] password = provider.getPassword(requestContext(
+                "jdbc:mysql://example:3306/database",
+                MYSQL_DATABASE_USER,
+                MYSQL_DRIVER_CLASS,
+                Map.of("sslMode", "VERIFY_CA")
+        ));
+
+        assertArrayEquals(TOKEN_VALUE.toCharArray(), password);
+    }
+
+    @Test
+    void testMissingMySqlSslModeRejectedBeforeRefresh() throws Exception {
+        final TestScopedGoogleCredentials scopedCredentials = new 
TestScopedGoogleCredentials(accessToken(TOKEN_VALUE, -15));
+        
scopedCredentials.setRefreshedAccessToken(accessToken(REFRESHED_TOKEN_VALUE, 
15));
+        final TestRunner runner = configureRunner(new 
RootGoogleCredentials(scopedCredentials), true, CloudSqlDatabaseType.MYSQL);
+        final DatabasePasswordProvider provider = getProvider(runner);
+
+        final ProcessException exception = assertThrows(ProcessException.class,
+                () -> 
provider.getPassword(requestContext("jdbc:mysql://example:3306/database", 
MYSQL_DATABASE_USER, MYSQL_DRIVER_CLASS, Map.of())));
+
+        assertEquals("MySQL sslMode must be configured as REQUIRED, VERIFY_CA, 
or VERIFY_IDENTITY for Cloud SQL IAM authentication", exception.getMessage());

Review Comment:
   Agreed. I removed the exact exception-message assertions in favor of 
checking the relevant outcome or a short semantic keyword. The tests still 
verify that credential and token values are not exposed through failure paths.



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