/*
 * Copyright (c) 2017, 2023, Oracle and/or its affiliates.
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, version 2.0, as published by the
 * Free Software Foundation.
 *
 * This program is also distributed with certain software (including but not
 * limited to OpenSSL) that is licensed under separate terms, as designated in a
 * particular file or component or in included license documentation. The
 * authors of MySQL hereby grant you an additional permission to link the
 * program and your derivative works with the separately licensed software that
 * they have included with MySQL.
 *
 * Without limiting anything contained in the foregoing, this file, which is
 * part of MySQL Connector/J, is also subject to the Universal FOSS Exception,
 * version 1.0, a copy of which can be found at
 * http://oss.oracle.com/licenses/universal-foss-exception.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License, version 2.0,
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
 */

package testsuite.x.devapi;

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.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.function.Function;

import javax.net.ssl.SSLContext;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import com.mysql.cj.CoreSession;
import com.mysql.cj.ServerVersion;
import com.mysql.cj.conf.PropertyDefinitions;
import com.mysql.cj.conf.PropertyDefinitions.AuthMech;
import com.mysql.cj.conf.PropertyDefinitions.SslMode;
import com.mysql.cj.conf.PropertyDefinitions.XdevapiSslMode;
import com.mysql.cj.conf.PropertyKey;
import com.mysql.cj.conf.PropertySet;
import com.mysql.cj.exceptions.CJCommunicationsException;
import com.mysql.cj.exceptions.SSLParamsException;
import com.mysql.cj.protocol.x.XAuthenticationProvider;
import com.mysql.cj.protocol.x.XProtocol;
import com.mysql.cj.protocol.x.XProtocolError;
import com.mysql.cj.xdevapi.Client;
import com.mysql.cj.xdevapi.ClientFactory;
import com.mysql.cj.xdevapi.Row;
import com.mysql.cj.xdevapi.Session;
import com.mysql.cj.xdevapi.SessionImpl;
import com.mysql.cj.xdevapi.SqlResult;

public class SecureSessionTest extends DevApiBaseTestCase {

    final String trustStoreUrl = "file:src/test/config/ssl-test-certs/ca-truststore";
    final String trustStorePath = "src/test/config/ssl-test-certs/ca-truststore";
    final String trustStorePassword = "password";

    final String clientKeyStoreUrl = "file:src/test/config/ssl-test-certs/client-keystore";
    final String clientKeyStorePath = "src/test/config/ssl-test-certs/client-keystore";
    final String clientKeyStorePassword = "password";

    final Properties sslFreeTestProperties = (Properties) this.testProperties.clone();
    String sslFreeBaseUrl = this.baseUrl;

    @BeforeEach
    public void setupSecureSessionTest() {
        assumeTrue(this.isSetForXTests, PropertyDefinitions.SYSP_testsuite_url_mysqlx + " must be set to run this test.");
        if (setupTestSession()) {
            System.clearProperty("javax.net.ssl.trustStore");
            System.clearProperty("javax.net.ssl.trustStoreType");
            System.clearProperty("javax.net.ssl.trustStorePassword");
            System.clearProperty("javax.net.ssl.keyStore");
            System.clearProperty("javax.net.ssl.keyStoreType");
            System.clearProperty("javax.net.ssl.keyStorePassword");

            this.sslFreeTestProperties.remove(PropertyKey.xdevapiSslMode.getKeyName());
            this.sslFreeTestProperties.remove(PropertyKey.xdevapiSslTrustStoreUrl.getKeyName());
            this.sslFreeTestProperties.remove(PropertyKey.xdevapiSslTrustStoreType.getKeyName());
            this.sslFreeTestProperties.remove(PropertyKey.xdevapiSslTrustStorePassword.getKeyName());
            this.sslFreeTestProperties.remove(PropertyKey.xdevapiFallbackToSystemTrustStore.getKeyName());
            this.sslFreeTestProperties.remove(PropertyKey.xdevapiSslKeyStoreUrl.getKeyName());
            this.sslFreeTestProperties.remove(PropertyKey.xdevapiSslKeyStoreType.getKeyName());
            this.sslFreeTestProperties.remove(PropertyKey.xdevapiSslKeyStorePassword.getKeyName());
            this.sslFreeTestProperties.remove(PropertyKey.xdevapiFallbackToSystemKeyStore.getKeyName());
            this.sslFreeTestProperties.remove(PropertyKey.xdevapiTlsCiphersuites.getKeyName());
            this.sslFreeTestProperties.remove(PropertyKey.xdevapiTlsVersions.getKeyName());
            // Also remove global security properties.
            this.sslFreeTestProperties.remove(PropertyKey.sslMode.getKeyName());
            this.sslFreeTestProperties.remove(PropertyKey.trustCertificateKeyStoreUrl.getKeyName());
            this.sslFreeTestProperties.remove(PropertyKey.trustCertificateKeyStoreType.getKeyName());
            this.sslFreeTestProperties.remove(PropertyKey.trustCertificateKeyStorePassword.getKeyName());
            this.sslFreeTestProperties.remove(PropertyKey.fallbackToSystemTrustStore.getKeyName());
            this.sslFreeTestProperties.remove(PropertyKey.clientCertificateKeyStoreUrl.getKeyName());
            this.sslFreeTestProperties.remove(PropertyKey.clientCertificateKeyStoreType.getKeyName());
            this.sslFreeTestProperties.remove(PropertyKey.clientCertificateKeyStorePassword.getKeyName());
            this.sslFreeTestProperties.remove(PropertyKey.fallbackToSystemKeyStore.getKeyName());
            this.sslFreeTestProperties.remove(PropertyKey.tlsCiphersuites.getKeyName());
            this.sslFreeTestProperties.remove(PropertyKey.tlsVersions.getKeyName());

            this.sslFreeBaseUrl = this.baseUrl;
            this.sslFreeBaseUrl = this.sslFreeBaseUrl.replaceAll(PropertyKey.xdevapiSslMode.getKeyName() + "=",
                    PropertyKey.xdevapiSslMode.getKeyName() + "VOID=");
            this.sslFreeBaseUrl = this.sslFreeBaseUrl.replaceAll(PropertyKey.xdevapiSslTrustStoreUrl.getKeyName() + "=",
                    PropertyKey.xdevapiSslTrustStoreUrl.getKeyName() + "VOID=");
            this.sslFreeBaseUrl = this.sslFreeBaseUrl.replaceAll(PropertyKey.xdevapiSslTrustStorePassword.getKeyName() + "=",
                    PropertyKey.xdevapiSslTrustStorePassword.getKeyName() + "VOID=");
            this.sslFreeBaseUrl = this.sslFreeBaseUrl.replaceAll(PropertyKey.xdevapiSslTrustStoreType.getKeyName() + "=",
                    PropertyKey.xdevapiSslTrustStoreType.getKeyName() + "VOID=");
            this.sslFreeBaseUrl = this.sslFreeBaseUrl.replaceAll(PropertyKey.xdevapiFallbackToSystemTrustStore.getKeyName() + "=",
                    PropertyKey.xdevapiFallbackToSystemTrustStore.getKeyName() + "VOID=");
            this.sslFreeBaseUrl = this.sslFreeBaseUrl.replaceAll(PropertyKey.xdevapiSslKeyStoreUrl.getKeyName() + "=",
                    PropertyKey.xdevapiSslKeyStoreUrl.getKeyName() + "VOID=");
            this.sslFreeBaseUrl = this.sslFreeBaseUrl.replaceAll(PropertyKey.xdevapiSslKeyStorePassword.getKeyName() + "=",
                    PropertyKey.xdevapiSslKeyStorePassword.getKeyName() + "VOID=");
            this.sslFreeBaseUrl = this.sslFreeBaseUrl.replaceAll(PropertyKey.xdevapiSslKeyStoreType.getKeyName() + "=",
                    PropertyKey.xdevapiSslKeyStoreType.getKeyName() + "VOID=");
            this.sslFreeBaseUrl = this.sslFreeBaseUrl.replaceAll(PropertyKey.xdevapiFallbackToSystemKeyStore.getKeyName() + "=",
                    PropertyKey.xdevapiFallbackToSystemKeyStore.getKeyName() + "VOID=");
            this.sslFreeBaseUrl = this.sslFreeBaseUrl.replaceAll(PropertyKey.xdevapiTlsCiphersuites.getKeyName() + "=",
                    PropertyKey.xdevapiTlsCiphersuites.getKeyName() + "VOID=");
            this.sslFreeBaseUrl = this.sslFreeBaseUrl.replaceAll(PropertyKey.xdevapiTlsVersions.getKeyName() + "=",
                    PropertyKey.xdevapiTlsVersions.getKeyName() + "VOID=");
            // Also remove global security properties.
            this.sslFreeBaseUrl = this.sslFreeBaseUrl.replaceAll(PropertyKey.sslMode.getKeyName() + "=", PropertyKey.sslMode.getKeyName() + "VOID=");
            this.sslFreeBaseUrl = this.sslFreeBaseUrl.replaceAll(PropertyKey.trustCertificateKeyStoreUrl.getKeyName() + "=",
                    PropertyKey.trustCertificateKeyStoreUrl.getKeyName() + "VOID=");
            this.sslFreeBaseUrl = this.sslFreeBaseUrl.replaceAll(PropertyKey.trustCertificateKeyStoreType.getKeyName() + "=",
                    PropertyKey.trustCertificateKeyStoreType.getKeyName() + "VOID=");
            this.sslFreeBaseUrl = this.sslFreeBaseUrl.replaceAll(PropertyKey.trustCertificateKeyStorePassword.getKeyName() + "=",
                    PropertyKey.trustCertificateKeyStorePassword.getKeyName() + "VOID=");
            this.sslFreeBaseUrl = this.sslFreeBaseUrl.replaceAll(PropertyKey.fallbackToSystemTrustStore.getKeyName() + "=",
                    PropertyKey.fallbackToSystemTrustStore.getKeyName() + "VOID=");
            this.sslFreeBaseUrl = this.sslFreeBaseUrl.replaceAll(PropertyKey.clientCertificateKeyStoreUrl.getKeyName() + "=",
                    PropertyKey.clientCertificateKeyStoreUrl.getKeyName() + "VOID=");
            this.sslFreeBaseUrl = this.sslFreeBaseUrl.replaceAll(PropertyKey.clientCertificateKeyStoreType.getKeyName() + "=",
                    PropertyKey.clientCertificateKeyStoreType.getKeyName() + "VOID=");
            this.sslFreeBaseUrl = this.sslFreeBaseUrl.replaceAll(PropertyKey.clientCertificateKeyStorePassword.getKeyName() + "=",
                    PropertyKey.clientCertificateKeyStorePassword.getKeyName() + "VOID=");
            this.sslFreeBaseUrl = this.sslFreeBaseUrl.replaceAll(PropertyKey.fallbackToSystemKeyStore.getKeyName() + "=",
                    PropertyKey.fallbackToSystemKeyStore.getKeyName() + "VOID=");
            this.sslFreeBaseUrl = this.sslFreeBaseUrl.replaceAll(PropertyKey.xdevapiTlsCiphersuites.getKeyName() + "=",
                    PropertyKey.xdevapiTlsCiphersuites.getKeyName() + "VOID=");
            this.sslFreeBaseUrl = this.sslFreeBaseUrl.replaceAll(PropertyKey.xdevapiTlsVersions.getKeyName() + "=",
                    PropertyKey.xdevapiTlsVersions.getKeyName() + "VOID=");

            if (!this.sslFreeBaseUrl.contains("?")) {
                this.sslFreeBaseUrl += "?";
            }
        }
    }

    @AfterEach
    public void teardownSecureSessionTest() {
        System.clearProperty("javax.net.ssl.trustStore");
        System.clearProperty("javax.net.ssl.trustStoreType");
        System.clearProperty("javax.net.ssl.trustStorePassword");
        System.clearProperty("javax.net.ssl.keyStore");
        System.clearProperty("javax.net.ssl.keyStoreType");
        System.clearProperty("javax.net.ssl.keyStorePassword");
        destroyTestSession();
    }

    /**
     * Tests non-secure {@link Session}s created via URL and properties map.
     */
    @Test
    public void testNonSecureSession() {
        try {
            Session testSession = this.fact.getSession(this.baseUrl);
            testSession.sql("CREATE USER IF NOT EXISTS 'testPlainAuth'@'%' IDENTIFIED WITH mysql_native_password BY 'pwd'").execute();
            testSession.sql("GRANT SELECT ON *.* TO 'testPlainAuth'@'%'").execute();
            testSession.close();

            String userAndSslFreeBaseUrl = this.sslFreeBaseUrl;
            userAndSslFreeBaseUrl = userAndSslFreeBaseUrl.replaceAll(getTestUser() + ":" + getTestPassword() + "@", "");
            userAndSslFreeBaseUrl = userAndSslFreeBaseUrl.replaceAll(PropertyKey.USER.getKeyName() + "=", PropertyKey.USER.getKeyName() + "VOID=");
            userAndSslFreeBaseUrl = userAndSslFreeBaseUrl.replaceAll(PropertyKey.PASSWORD.getKeyName() + "=", PropertyKey.PASSWORD.getKeyName() + "VOID=");

            testSession = this.fact.getSession(userAndSslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, XdevapiSslMode.DISABLED)
                    + makeParam(PropertyKey.USER, "testPlainAuth") + makeParam(PropertyKey.PASSWORD, "pwd"));
            assertNonSecureSession(testSession);
            testSession.close();

            Properties props = new Properties(this.sslFreeTestProperties);
            props.setProperty(PropertyKey.xdevapiSslMode.getKeyName(), XdevapiSslMode.DISABLED.toString());
            props.setProperty(PropertyKey.USER.getKeyName(), "testPlainAuth");
            props.setProperty(PropertyKey.PASSWORD.getKeyName(), "pwd");
            testSession = this.fact.getSession(props);
            assertNonSecureSession(testSession);
            testSession.close();

        } catch (Throwable t) {
            throw t;
        } finally {
            Session testSession = this.fact.getSession(this.baseUrl);
            testSession.sql("DROP USER if exists testPlainAuth").execute();
            testSession.close();
        }
    }

    /**
     * Tests secure {@link Session}s created via URL and properties map. This is the default if no ssl-mode is provided.
     */
    @Test
    public void testSecureSessionDefaultAndRequired() {
        Session testSession = this.fact.getSession(this.sslFreeBaseUrl);
        assertSecureSession(testSession);
        testSession.close();

        testSession = this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, XdevapiSslMode.REQUIRED));
        assertSecureSession(testSession);
        testSession.close();

        testSession = this.fact.getSession(this.sslFreeTestProperties);
        assertSecureSession(testSession);
        testSession.close();

        Properties props = new Properties(this.sslFreeTestProperties);
        props.setProperty(PropertyKey.xdevapiSslMode.getKeyName(), XdevapiSslMode.REQUIRED.toString());
        testSession = this.fact.getSession(props);
        assertSecureSession(testSession);
        testSession.close();
    }

    /**
     * Tests secure {@link Session}s created via URL and properties map, with the SSL system properties also defined.
     */
    @Test
    public void testSecureSessionDefaultAndRequiredWithSystemPropsPresent() {
        assumeTrue(supportsTestCertificates(this.session),
                "This test requires the server configured with SSL certificates from ConnectorJ/src/test/config/ssl-test-certs");

        System.setProperty("javax.net.ssl.trustStore", this.trustStorePath);
        System.setProperty("javax.net.ssl.trustStorePassword", this.trustStorePassword);

        Session testSession = this.fact.getSession(this.sslFreeBaseUrl);
        assertSecureSession(testSession);
        testSession.close();

        testSession = this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, XdevapiSslMode.VERIFY_CA));
        assertSecureSession(testSession);
        testSession.close();

        testSession = this.fact.getSession(this.sslFreeTestProperties);
        assertSecureSession(testSession);
        testSession.close();

        Properties props = new Properties(this.sslFreeTestProperties);
        props.setProperty(PropertyKey.xdevapiSslMode.getKeyName(), XdevapiSslMode.REQUIRED.toString());
        testSession = this.fact.getSession(props);
        assertSecureSession(testSession);
        testSession.close();
    }

    /**
     * Tests secure {@link Session}s created via URL and properties map, verifying server certificate.
     */
    @Test
    public void testSecureSessionVerifyServerCertificate() {
        assumeTrue(supportsTestCertificates(this.session),
                "This test requires the server configured with SSL certificates from ConnectorJ/src/test/config/ssl-test-certs");

        Session testSession = this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, XdevapiSslMode.VERIFY_CA)
                + makeParam(PropertyKey.xdevapiSslTrustStoreUrl, this.trustStoreUrl)
                + makeParam(PropertyKey.xdevapiSslTrustStorePassword, this.trustStorePassword));
        assertSecureSession(testSession);
        testSession.close();

        Properties props = new Properties(this.sslFreeTestProperties);
        props.setProperty(PropertyKey.xdevapiSslMode.getKeyName(), XdevapiSslMode.VERIFY_CA.toString());
        props.setProperty(PropertyKey.xdevapiSslTrustStoreUrl.getKeyName(), this.trustStoreUrl);
        props.setProperty(PropertyKey.xdevapiSslTrustStorePassword.getKeyName(), this.trustStorePassword);
        testSession = this.fact.getSession(props);
        assertSecureSession(testSession);
        testSession.close();
    }

    /**
     * Tests secure {@link Session}s created via URL and properties map combined with SSL system properties, verifying server certificate.
     */
    @Test
    public void testSecureSessionVerifyServerCertificateUsingSystemProps() {
        assumeTrue(supportsTestCertificates(this.session),
                "This test requires the server configured with SSL certificates from ConnectorJ/src/test/config/ssl-test-certs");

        System.setProperty("javax.net.ssl.trustStore", this.trustStorePath);
        System.setProperty("javax.net.ssl.trustStorePassword", this.trustStorePassword);

        Session testSession = this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, XdevapiSslMode.VERIFY_CA));
        assertSecureSession(testSession);
        testSession.close();

        Properties props = new Properties(this.sslFreeTestProperties);
        props.setProperty(PropertyKey.xdevapiSslMode.getKeyName(), XdevapiSslMode.VERIFY_CA.toString());
        testSession = this.fact.getSession(props);
        assertSecureSession(testSession);
        testSession.close();
    }

    /**
     * Tests secure {@link Session}s created via URL and properties map, verifying server certificate.
     * This test would pass if the server certificate had "CN=<host_name>", with <host_name> equals to the host name in the test URL.
     */
    @Test
    @Disabled("requires a certificate with CN=<host_name> equals to the host name in the test URL")
    public void testSecureSessionVerifyServerCertificateIdentity() {
        Session testSession = this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, XdevapiSslMode.VERIFY_IDENTITY)
                + makeParam(PropertyKey.xdevapiSslTrustStoreUrl, this.trustStoreUrl)
                + makeParam(PropertyKey.xdevapiSslTrustStorePassword, this.trustStorePassword));
        assertSecureSession(testSession);
        testSession.close();

        Properties props = new Properties(this.sslFreeTestProperties);
        props.setProperty(PropertyKey.xdevapiSslMode.getKeyName(), XdevapiSslMode.VERIFY_IDENTITY.toString());
        props.setProperty(PropertyKey.xdevapiSslTrustStoreUrl.getKeyName(), this.trustStoreUrl);
        props.setProperty(PropertyKey.xdevapiSslTrustStorePassword.getKeyName(), this.trustStorePassword);
        testSession = this.fact.getSession(props);
        assertSecureSession(testSession);
        testSession.close();
    }

    /**
     * Tests exception thrown on missing truststore for a secure {@link Session}.
     */
    @Test
    public void testSecureSessionMissingTrustStore() {
        assertThrows(CJCommunicationsException.class, "No truststore provided to verify the Server certificate\\.",
                () -> this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiFallbackToSystemTrustStore, "false")
                        + makeParam(PropertyKey.xdevapiSslMode, XdevapiSslMode.VERIFY_CA)));

        assertThrows(CJCommunicationsException.class, "No truststore provided to verify the Server certificate\\.",
                () -> this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiFallbackToSystemTrustStore, "false")
                        + makeParam(PropertyKey.xdevapiSslMode, XdevapiSslMode.VERIFY_IDENTITY)));

        Properties props = new Properties(this.sslFreeTestProperties);
        props.setProperty(PropertyKey.xdevapiFallbackToSystemTrustStore.getKeyName(), "false");
        props.setProperty(PropertyKey.xdevapiSslMode.getKeyName(), XdevapiSslMode.VERIFY_CA.toString());
        assertThrows(CJCommunicationsException.class, "No truststore provided to verify the Server certificate\\.", () -> this.fact.getSession(props));

        props.setProperty(PropertyKey.xdevapiSslMode.getKeyName(), XdevapiSslMode.VERIFY_IDENTITY.toString());
        assertThrows(CJCommunicationsException.class, "No truststore provided to verify the Server certificate\\.", () -> this.fact.getSession(props));
    }

    /**
     * Tests exception thrown on verifying server certificate identity failure.
     * The server certificate used in this test has "CN=MySQL Connector/J Server".
     */
    @Test
    public void testSecureSessionVerifyServerCertificateIdentityFailure() {
        // Meaningful error message is deep inside the stack trace.
        assertThrows(CJCommunicationsException.class,
                () -> this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, XdevapiSslMode.VERIFY_IDENTITY)
                        + makeParam(PropertyKey.xdevapiSslTrustStoreUrl, this.trustStoreUrl)
                        + makeParam(PropertyKey.xdevapiSslTrustStorePassword, this.trustStorePassword)));

        Properties props = new Properties(this.sslFreeTestProperties);
        props.setProperty(PropertyKey.xdevapiSslMode.getKeyName(), XdevapiSslMode.VERIFY_IDENTITY.toString());
        props.setProperty(PropertyKey.xdevapiSslTrustStoreUrl.getKeyName(), this.trustStoreUrl);
        props.setProperty(PropertyKey.xdevapiSslTrustStorePassword.getKeyName(), this.trustStorePassword);
        // Meaningful error message is deep inside the stack trace.
        assertThrows(CJCommunicationsException.class, () -> this.fact.getSession(props));
    }

    /**
     * Tests that PLAIN, MYSQL41, SHA256_MEMORY, and EXTERNAL authentication mechanisms.
     *
     * @throws Throwable
     */
    @Test
    public void testAuthMechanisms() throws Throwable {
        try {
            this.session.sql("CREATE USER IF NOT EXISTS 'testAuthMechNative'@'%' IDENTIFIED WITH mysql_native_password BY 'mysqlnative'").execute();
            this.session.sql("GRANT SELECT ON *.* TO 'testAuthMechNative'@'%'").execute();
            this.session.sql("CREATE USER IF NOT EXISTS 'testAuthMechSha256'@'%' IDENTIFIED WITH sha256_password BY 'sha256'").execute();
            this.session.sql("GRANT SELECT ON *.* TO 'testAuthMechSha256'@'%'").execute();
            if (mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.3"))) {
                this.session.sql("CREATE USER IF NOT EXISTS 'testAuthMechCachingSha2'@'%' IDENTIFIED WITH caching_sha2_password BY 'cachingsha2'").execute();
                this.session.sql("GRANT SELECT ON *.* TO 'testAuthMechCachingSha2'@'%'").execute();
            }

            final Field sf = SessionImpl.class.getDeclaredField("session");
            sf.setAccessible(true);
            final Field pf = CoreSession.class.getDeclaredField("protocol");
            pf.setAccessible(true);
            final Field mf = XAuthenticationProvider.class.getDeclaredField("authMech");
            mf.setAccessible(true);

            Function<Session, AuthMech> getAuthMech = s -> {
                try {
                    return (AuthMech) mf.get(((XProtocol) pf.get(sf.get(s))).getAuthenticationProvider());
                } catch (IllegalArgumentException | IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
            };

            /*
             * Access denied (ER_ACCESS_DENIED_ERROR) error message from servers up to version 8.0.11.
             * In MySQL 8.0.12 this message changed due to fix for Bug#27675699 - FAILED AUTHENTICATION AT X PLUGIN ALWAYS RETURNS ER_ACCESS_DENIED_ERROR.
             * This variable may be redefined as needed along the test.
             */
            String accessDeniedErrMsg = "ERROR 1045 \\(HY000\\) Invalid user or password";

            /*
             * Authenticate using (default) TLS first. As per MySQL 8.0.4 X Plugin this is required so that SHA2[56] logins get cached in SHA2_MEMORY.
             */
            Session testSession = null;
            Properties props = new Properties(this.sslFreeTestProperties);

            // With default auth mechanism for secure connections (PLAIN).

            // *** User: mysqlnative; Auth: default.
            props.setProperty(PropertyKey.USER.getKeyName(), "testAuthMechNative");
            props.setProperty(PropertyKey.PASSWORD.getKeyName(), "mysqlnative");

            testSession = this.fact.getSession(props);
            assertSecureSession(testSession);
            assertEquals(AuthMech.PLAIN, getAuthMech.apply(testSession)); // Connection is secure, passwords are safe & account gets cached.
            assertUser("testAuthMechNative", testSession);
            testSession.close();

            if (mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.3"))) {
                // *** User: testAuthMechSha256; Auth: default.
                props.setProperty(PropertyKey.USER.getKeyName(), "testAuthMechSha256");
                props.setProperty(PropertyKey.PASSWORD.getKeyName(), "sha256");

                testSession = this.fact.getSession(props);
                assertSecureSession(testSession);
                assertEquals(AuthMech.PLAIN, getAuthMech.apply(testSession)); // Connection is secure, passwords are safe & account gets cached.
                assertUser("testAuthMechSha256", testSession);
                testSession.close();
            }

            if (mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.3"))) {
                // *** User: testAuthMechCachingSha2; Auth: default.
                props.setProperty(PropertyKey.USER.getKeyName(), "testAuthMechCachingSha2");
                props.setProperty(PropertyKey.PASSWORD.getKeyName(), "cachingsha2");

                testSession = this.fact.getSession(props);
                assertSecureSession(testSession);
                assertEquals(AuthMech.PLAIN, getAuthMech.apply(testSession)); // Connection is secure, passwords are safe & account gets cached.
                assertUser("testAuthMechCachingSha2", testSession);
                testSession.close();
            }

            // Forcing an auth mechanism.

            // *** User: testAuthMechNative; Auth: PLAIN.
            props.setProperty(PropertyKey.USER.getKeyName(), "testAuthMechNative");
            props.setProperty(PropertyKey.PASSWORD.getKeyName(), "mysqlnative");
            props.setProperty(PropertyKey.xdevapiAuth.getKeyName(), "PLAIN");

            testSession = this.fact.getSession(props);
            assertSecureSession(testSession);
            assertEquals(AuthMech.PLAIN, getAuthMech.apply(testSession)); // Connection is secure, passwords are safe.
            assertUser("testAuthMechNative", testSession);
            testSession.close();

            // *** User: testAuthMechNative; Auth: MYSQL41.
            props.setProperty(PropertyKey.xdevapiAuth.getKeyName(), "MYSQL41");

            testSession = this.fact.getSession(props);
            assertSecureSession(testSession);
            assertEquals(AuthMech.MYSQL41, getAuthMech.apply(testSession)); // Matching auth mechanism.
            assertUser("testAuthMechNative", testSession);
            testSession.close();

            if (mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.4"))) { // SHA256_MEMORY support added in MySQL 8.0.4.
                // *** User: testAuthMechNative; Auth: SHA256_MEMORY.
                props.setProperty(PropertyKey.xdevapiAuth.getKeyName(), "SHA256_MEMORY");

                testSession = this.fact.getSession(props);
                assertSecureSession(testSession);
                assertEquals(AuthMech.SHA256_MEMORY, getAuthMech.apply(testSession)); // Account is cached by now.
                assertUser("testAuthMechNative", testSession);
                testSession.close();
            }

            if (mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.3"))) {
                // *** User: testAuthMechSha256; Auth: PLAIN.
                props.setProperty(PropertyKey.USER.getKeyName(), "testAuthMechSha256");
                props.setProperty(PropertyKey.PASSWORD.getKeyName(), "sha256");
                props.setProperty(PropertyKey.xdevapiAuth.getKeyName(), "PLAIN");

                testSession = this.fact.getSession(props);
                assertSecureSession(testSession);
                assertEquals(AuthMech.PLAIN, getAuthMech.apply(testSession)); // Connection is secure, passwords are safe.
                assertUser("testAuthMechSha256", testSession);
                testSession.close();

                // *** User: testAuthMechSha256; Auth: MYSQL41.
                props.setProperty(PropertyKey.xdevapiAuth.getKeyName(), "MYSQL41");
                if (mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.12"))) {
                    accessDeniedErrMsg = "ERROR 1045 \\(HY000\\) Access denied for user 'testAuthMechSha256'@.*";
                }

                assertThrows(XProtocolError.class, accessDeniedErrMsg, () -> this.fact.getSession(props)); // Auth mech mismatch.

                if (mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.4"))) { // SHA256_MEMORY support added in MySQL 8.0.4.
                    // *** User: testAuthMechSha256; Auth: SHA256_MEMORY.
                    props.setProperty(PropertyKey.xdevapiAuth.getKeyName(), "SHA256_MEMORY");

                    testSession = this.fact.getSession(props);
                    assertSecureSession(testSession);
                    assertEquals(AuthMech.SHA256_MEMORY, getAuthMech.apply(testSession)); // Account is cached by now.
                    assertUser("testAuthMechSha256", testSession);
                    testSession.close();
                }
            }

            if (mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.3"))) {
                // *** User: testAuthMechCachingSha2; Auth: PLAIN.
                props.setProperty(PropertyKey.USER.getKeyName(), "testAuthMechCachingSha2");
                props.setProperty(PropertyKey.PASSWORD.getKeyName(), "cachingsha2");
                props.setProperty(PropertyKey.xdevapiAuth.getKeyName(), "PLAIN");

                testSession = this.fact.getSession(props);
                assertSecureSession(testSession);
                assertEquals(AuthMech.PLAIN, getAuthMech.apply(testSession)); // Connection is secure, passwords are safe.
                assertUser("testAuthMechCachingSha2", testSession);
                testSession.close();

                // *** User: testAuthMechCachingSha2; Auth: MYSQL41.
                props.setProperty(PropertyKey.xdevapiAuth.getKeyName(), "MYSQL41");
                if (mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.12"))) {
                    accessDeniedErrMsg = "ERROR 1045 \\(HY000\\) Access denied for user 'testAuthMechCachingSha2'@.*";
                }

                assertThrows(XProtocolError.class, accessDeniedErrMsg, () -> this.fact.getSession(props)); // Auth mech mismatch.

                if (mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.4"))) { // SHA256_MEMORY support added in MySQL 8.0.4.
                    // User: testAuthMechCachingSha2; Auth: SHA256_MEMORY.
                    props.setProperty(PropertyKey.xdevapiAuth.getKeyName(), "SHA256_MEMORY");

                    testSession = this.fact.getSession(props);
                    assertSecureSession(testSession);
                    assertEquals(AuthMech.SHA256_MEMORY, getAuthMech.apply(testSession)); // Account is cached by now.
                    assertUser("testAuthMechCachingSha2", testSession);
                    testSession.close();
                }
            }

            // *** User: external; Auth: EXTERNAL.
            props.setProperty(PropertyKey.USER.getKeyName(), "external");
            props.setProperty(PropertyKey.PASSWORD.getKeyName(), "external");
            props.setProperty(PropertyKey.xdevapiAuth.getKeyName(), "EXTERNAL");

            assertThrows(XProtocolError.class, "ERROR 1251 \\(HY000\\) Invalid authentication method EXTERNAL", () -> this.fact.getSession(props));

            props.remove(PropertyKey.xdevapiAuth.getKeyName());

            /*
             * Authenticate using non-secure connections.
             */
            props.setProperty(PropertyKey.xdevapiSslMode.getKeyName(), PropertyDefinitions.XdevapiSslMode.DISABLED.toString());

            // With default auth mechanism for non-secure connections (MYSQL41|SHA2_MEMORY).

            // *** User: testAuthMechNative; Auth: default.
            props.setProperty(PropertyKey.USER.getKeyName(), "testAuthMechNative");
            props.setProperty(PropertyKey.PASSWORD.getKeyName(), "mysqlnative");

            testSession = this.fact.getSession(props);
            assertNonSecureSession(testSession);
            assertEquals(AuthMech.MYSQL41, getAuthMech.apply(testSession)); // Matching auth mechanism.
            assertUser("testAuthMechNative", testSession);
            testSession.close();

            if (mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.4"))) { // SHA256_PASSWORD requires secure connections in MySQL 8.0.3 and below.
                // *** User: testAuthMechSha256; Auth: default.
                props.setProperty(PropertyKey.USER.getKeyName(), "testAuthMechSha256");
                props.setProperty(PropertyKey.PASSWORD.getKeyName(), "sha256");

                testSession = this.fact.getSession(props);
                assertNonSecureSession(testSession);
                assertEquals(AuthMech.SHA256_MEMORY, getAuthMech.apply(testSession)); // Account is cached by now.
                assertUser("testAuthMechSha256", testSession);
                testSession.close();
            }

            if (mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.4"))) { // CACHING_SHA2_PASSWORD requires secure connections in MySQL 8.0.3 and below.
                // *** User: testAuthMechCachingSha2; Auth: default.
                props.setProperty(PropertyKey.USER.getKeyName(), "testAuthMechCachingSha2");
                props.setProperty(PropertyKey.PASSWORD.getKeyName(), "cachingsha2");

                testSession = this.fact.getSession(props);
                assertNonSecureSession(testSession);
                assertEquals(AuthMech.SHA256_MEMORY, getAuthMech.apply(testSession)); // Account is cached by now.
                assertUser("testAuthMechCachingSha2", testSession);
                testSession.close();
            }

            // Forcing an auth mechanism.

            // *** User: testAuthMechNative; Auth: PLAIN.
            props.setProperty(PropertyKey.USER.getKeyName(), "testAuthMechNative");
            props.setProperty(PropertyKey.PASSWORD.getKeyName(), "mysqlnative");
            props.setProperty(PropertyKey.xdevapiAuth.getKeyName(), "PLAIN");

            assertThrows(XProtocolError.class, "PLAIN authentication is not allowed via unencrypted connection\\.", () -> this.fact.getSession(props));

            // *** User: testAuthMechNative; Auth: MYSQL41.
            props.setProperty(PropertyKey.xdevapiAuth.getKeyName(), "MYSQL41");

            testSession = this.fact.getSession(props);
            assertNonSecureSession(testSession);
            assertEquals(AuthMech.MYSQL41, getAuthMech.apply(testSession)); // Matching auth mechanism.
            assertUser("testAuthMechNative", testSession);
            testSession.close();

            if (mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.4"))) { // SHA256_MEMORY support added in MySQL 8.0.4.
                // *** User: testAuthMechNative; Auth: SHA256_MEMORY.
                props.setProperty(PropertyKey.xdevapiAuth.getKeyName(), "SHA256_MEMORY");

                testSession = this.fact.getSession(props);
                assertNonSecureSession(testSession);
                assertEquals(AuthMech.SHA256_MEMORY, getAuthMech.apply(testSession)); // Account is cached by now.
                assertUser("testAuthMechNative", testSession);
                testSession.close();
            }

            if (mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.3"))) {
                // *** User: testAuthMechSha256; Auth: PLAIN.
                props.setProperty(PropertyKey.USER.getKeyName(), "testAuthMechSha256");
                props.setProperty(PropertyKey.PASSWORD.getKeyName(), "sha256");
                props.setProperty(PropertyKey.xdevapiAuth.getKeyName(), "PLAIN");

                assertThrows(XProtocolError.class, "PLAIN authentication is not allowed via unencrypted connection\\.", () -> this.fact.getSession(props));

                // *** User: testAuthMechSha256; Auth: MYSQL41.
                props.setProperty(PropertyKey.xdevapiAuth.getKeyName(), "MYSQL41");
                if (mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.12"))) {
                    accessDeniedErrMsg = "ERROR 1045 \\(HY000\\) Access denied for user 'testAuthMechSha256'@.*";
                }

                assertThrows(XProtocolError.class, accessDeniedErrMsg, () -> this.fact.getSession(props)); // Auth mech mismatch.

                if (mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.4"))) { // SHA256_MEMORY support added in MySQL 8.0.4.
                    // *** User: testAuthMechSha256; Auth: SHA256_MEMORY.
                    props.setProperty(PropertyKey.xdevapiAuth.getKeyName(), "SHA256_MEMORY");

                    testSession = this.fact.getSession(props);
                    assertNonSecureSession(testSession);
                    assertEquals(AuthMech.SHA256_MEMORY, getAuthMech.apply(testSession)); // Account is cached by now.
                    assertUser("testAuthMechSha256", testSession);
                    testSession.close();
                }
            }

            if (mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.3"))) {
                // *** User: testAuthMechCachingSha2; Auth: PLAIN.
                props.setProperty(PropertyKey.USER.getKeyName(), "testAuthMechCachingSha2");
                props.setProperty(PropertyKey.PASSWORD.getKeyName(), "cachingsha2");
                props.setProperty(PropertyKey.xdevapiAuth.getKeyName(), "PLAIN");

                assertThrows(XProtocolError.class, "PLAIN authentication is not allowed via unencrypted connection\\.", () -> this.fact.getSession(props));

                // *** User: testAuthMechCachingSha2; Auth: MYSQL41.
                props.setProperty(PropertyKey.xdevapiAuth.getKeyName(), "MYSQL41");
                if (mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.12"))) {
                    accessDeniedErrMsg = "ERROR 1045 \\(HY000\\) Access denied for user 'testAuthMechCachingSha2'@.*";
                }

                assertThrows(XProtocolError.class, accessDeniedErrMsg, () -> this.fact.getSession(props)); // Auth mech mismatch.

                if (mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.4"))) { // SHA256_MEMORY support added in MySQL 8.0.4.
                    // *** User: testAuthMechCachingSha2; Auth: SHA256_MEMORY.
                    props.setProperty(PropertyKey.xdevapiAuth.getKeyName(), "SHA256_MEMORY");

                    testSession = this.fact.getSession(props);
                    assertNonSecureSession(testSession);
                    assertEquals(AuthMech.SHA256_MEMORY, getAuthMech.apply(testSession)); // Account is cached by now.
                    assertUser("testAuthMechCachingSha2", testSession);
                    testSession.close();
                }
            }

            // *** User: external; Auth: EXTERNAL.
            props.setProperty(PropertyKey.USER.getKeyName(), "external");
            props.setProperty(PropertyKey.PASSWORD.getKeyName(), "external");
            props.setProperty(PropertyKey.xdevapiAuth.getKeyName(), "EXTERNAL");

            assertThrows(XProtocolError.class, "ERROR 1251 \\(HY000\\) Invalid authentication method EXTERNAL", () -> this.fact.getSession(props));

            props.remove(PropertyKey.xdevapiAuth.getKeyName());
        } finally {
            this.session.sql("DROP USER IF EXISTS testAuthMechNative").execute();
            this.session.sql("DROP USER IF EXISTS testAuthMechSha256").execute();
            this.session.sql("DROP USER IF EXISTS testAuthMechCachingSha2").execute();
        }
    }

    /**
     * Tests TLSv1.2
     *
     * @throws Exception
     */
    @Test
    public void testTLSv1_2() throws Exception {
        assumeTrue(supportsTestCertificates(this.session),
                "This test requires the server configured with SSL certificates from ConnectorJ/src/test/config/ssl-test-certs");

        // newer GPL servers, like 8.0.4+, are using OpenSSL and can use RSA encryption, while old ones compiled with yaSSL cannot
        Session sess = this.fact.getSession(this.sslFreeBaseUrl);
        boolean gplWithRSA = allowsRsa(sess);
        String highestCommonTlsVersion = getHighestCommonTlsVersion(sess);
        sess.close();

        Properties props = new Properties(this.sslFreeTestProperties);
        props.setProperty(PropertyKey.xdevapiSslMode.getKeyName(), PropertyDefinitions.XdevapiSslMode.VERIFY_CA.toString());
        props.setProperty(PropertyKey.xdevapiSslTrustStoreUrl.getKeyName(), this.trustStoreUrl);
        props.setProperty(PropertyKey.xdevapiSslTrustStorePassword.getKeyName(), this.trustStorePassword);

        // defaults to TLSv1.1
        Session testSession = this.fact.getSession(props);
        assertSecureSession(testSession);
        assertTlsVersion(testSession, highestCommonTlsVersion);
        testSession.close();

        // restricted to TLSv1
        props.setProperty(PropertyKey.tlsVersions.getKeyName(), "TLSv1");
        assertThrows(CJCommunicationsException.class, ".+TLS protocols TLSv1 and TLSv1.1 are not supported. Accepted values are TLSv1.2 and TLSv1.3.",
                () -> this.fact.getSession(props));

        // TLSv1.2 should fail with servers compiled with yaSSL
        props.setProperty(PropertyKey.tlsVersions.getKeyName(), "TLSv1.2,TLSv1.1,TLSv1");
        if (gplWithRSA) {
            testSession = this.fact.getSession(props);
            assertSecureSession(testSession);
            assertTlsVersion(testSession, "TLSv1.2");
            testSession.close();
        } else {
            assertThrows(CJCommunicationsException.class, "javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake",
                    () -> this.fact.getSession(props));
        }
    }

    private boolean allowsRsa(Session sess) {
        boolean allowsRSA = false;
        SqlResult rset = sess.sql("SHOW STATUS LIKE 'Rsa_public_key'").execute();
        if (rset.hasNext()) {
            String value = rset.fetchOne().getString(1);
            allowsRSA = value != null && value.length() > 0;
        }
        return allowsRSA;
    }

    private void assertUser(String user, Session sess) {
        SqlResult rows = sess.sql("SELECT USER(),CURRENT_USER()").execute();
        Row row = rows.fetchOne();
        assertEquals(user, row.getString(0).split("@")[0]);
        assertEquals(user, row.getString(1).split("@")[0]);
    }

    private void assertTlsVersion(Session sess, String expectedTlsVersion) {
        SqlResult rs = sess.sql("SHOW SESSION STATUS LIKE 'mysqlx_ssl_version'").execute();
        String actual = rs.fetchOne().getString(1);
        assertEquals(expectedTlsVersion, actual);
    }

    private String getHighestCommonTlsVersion(Session sess) throws Exception {
        // Find out which TLS protocol versions are supported by this JVM.
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, null, null);
        List<String> jvmSupportedProtocols = Arrays.asList(sslContext.createSSLEngine().getSupportedProtocols());

        SqlResult rset = sess.sql("SHOW GLOBAL VARIABLES LIKE 'tls_version'").execute();
        String value = rset.fetchOne().getString(1);
        //  this.rs = sslConn.createStatement().executeQuery("SHOW GLOBAL VARIABLES LIKE 'tls_version'");
        //  assertTrue(this.rs.next());

        List<String> serverSupportedProtocols = Arrays.asList(value.trim().split("\\s*,\\s*"));
        String highestCommonTlsVersion = "";
        for (String p : new String[] { "TLSv1.3", "TLSv1.2", "TLSv1.1", "TLSv1" }) {
            if (jvmSupportedProtocols.contains(p) && serverSupportedProtocols.contains(p)) {
                highestCommonTlsVersion = p;
                break;
            }
        }
        System.out.println("Server supports TLS protocols: " + serverSupportedProtocols);
        System.out.println("Highest common TLS protocol: " + highestCommonTlsVersion);

        return highestCommonTlsVersion;
    }

    /**
     * Tests fix for Bug#25494338, ENABLEDSSLCIPHERSUITES PARAMETER NOT WORKING AS EXPECTED WITH X-PLUGIN.
     *
     * @throws Exception
     */
    @Test
    public void testBug25494338() throws Exception {
        assumeTrue(supportsTestCertificates(this.session),
                "This test requires the server configured with SSL certificates from ConnectorJ/src/test/config/ssl-test-certs");

        String testCipher1 = "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"; // TLSv1.2 IANA Cipher name.
        String expectedCipher1 = "ECDHE-RSA-AES128-GCM-SHA256"; // TLSv1.2 OpenSSL Cipher name.
        String testCipher2 = "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"; // TLSv1.2 IANA Cipher name.
        String expectedCipher2 = "ECDHE-RSA-AES256-GCM-SHA384"; // TLSv1.2 OpenSSL Cipher name.
        Session sess = this.fact.getSession(this.baseUrl);
        String testTlsVersion = getHighestCommonTlsVersion(sess); // At least TLSv1.2 is expected to be supported.
        sess.close();
        if ("TLSv1.3".equalsIgnoreCase(testTlsVersion)) {
            testCipher1 = "TLS_AES_256_GCM_SHA384"; // TLSv1.3 IANA Cipher name.
            expectedCipher1 = "TLS_AES_256_GCM_SHA384"; // TLSv1.3 IANA Cipher name.
            testCipher2 = "TLS_AES_128_GCM_SHA256"; // TLSv1.3 IANA Cipher name.
            expectedCipher2 = "TLS_AES_128_GCM_SHA256"; // TLSv1.3 IANA Cipher name.
        }

        Session testSession = null;
        try {
            Properties props = new Properties(this.sslFreeTestProperties);
            testSession = this.fact.getSession(props);

            testSession.sql("CREATE USER 'bug25494338user'@'%' IDENTIFIED WITH mysql_native_password BY 'pwd' REQUIRE CIPHER '" + expectedCipher2 + "'")
                    .execute();
            testSession.sql("GRANT SELECT ON *.* TO 'bug25494338user'@'%'").execute();

            props.setProperty(PropertyKey.xdevapiSslMode.getKeyName(), PropertyDefinitions.XdevapiSslMode.VERIFY_CA.toString());
            props.setProperty(PropertyKey.xdevapiSslTrustStoreUrl.getKeyName(), this.trustStoreUrl);
            props.setProperty(PropertyKey.xdevapiSslTrustStorePassword.getKeyName(), this.trustStorePassword);
            props.setProperty(PropertyKey.clientCertificateKeyStoreUrl.getKeyName(), this.clientKeyStoreUrl);
            props.setProperty(PropertyKey.clientCertificateKeyStorePassword.getKeyName(), this.clientKeyStorePassword);

            // 1. Allow only TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256/TLS_AES_256_GCM_SHA384 cipher
            props.setProperty(PropertyKey.tlsCiphersuites.getKeyName(), testCipher1);
            sess = this.fact.getSession(props);
            assertSessionStatusEquals(sess, "mysqlx_ssl_cipher", expectedCipher1);
            sess.close();

            // 2. Allow only TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384/TLS_AES_128_GCM_SHA256 cipher
            props.setProperty(PropertyKey.tlsCiphersuites.getKeyName(), testCipher2);
            sess = this.fact.getSession(props);
            assertSessionStatusEquals(sess, "mysqlx_ssl_cipher", expectedCipher2);
            assertSessionStatusEquals(sess, "ssl_cipher", "");
            sess.close();

            // 3. Check connection with required client certificate
            props.setProperty(PropertyKey.USER.getKeyName(), "bug25494338user");
            props.setProperty(PropertyKey.PASSWORD.getKeyName(), "pwd");

            sess = this.fact.getSession(props);
            assertSessionStatusEquals(sess, "mysqlx_ssl_cipher", expectedCipher2);
            assertSessionStatusEquals(sess, "ssl_cipher", "");
            sess.close();
        } catch (Throwable t) {
            throw t;
        } finally {
            if (testSession != null) {
                testSession.sql("DROP USER bug25494338user").execute();
            }
        }
    }

    /**
     * Tests fix for Bug#23597281, GETNODESESSION() CALL WITH SSL PARAMETERS RETURNS CJCOMMUNICATIONSEXCEPTION
     */
    @Test
    public void testBug23597281() {
        assumeTrue(supportsTestCertificates(this.session),
                "This test requires the server configured with SSL certificates from ConnectorJ/src/test/config/ssl-test-certs");

        Properties props = new Properties(this.sslFreeTestProperties);
        props.setProperty(PropertyKey.xdevapiSslMode.getKeyName(), PropertyDefinitions.XdevapiSslMode.VERIFY_CA.toString());
        props.setProperty(PropertyKey.xdevapiSslTrustStoreUrl.getKeyName(), this.trustStoreUrl);
        props.setProperty(PropertyKey.xdevapiSslTrustStorePassword.getKeyName(), this.trustStorePassword);

        Session nSession;
        for (int i = 0; i < 100; i++) {
            nSession = this.fact.getSession(props);
            nSession.close();
            nSession = null;
        }
    }

    /**
     * Tests fix for Bug#26227653, WL#10528 DIFF BEHAVIOUR WHEN SYSTEM PROP JAVAX.NET.SSL.TRUSTSTORETYPE IS SET
     *
     * The actual bug is: if wrong system-wide SSL settings are provided, the session should not fail if 'xdevapi.ssl-mode=REQUIRED'.
     */
    @Test
    public void testBug26227653() {
        System.setProperty("javax.net.ssl.trustStore", "dummy_truststore");
        System.setProperty("javax.net.ssl.trustStorePassword", "some_password");
        System.setProperty("javax.net.ssl.trustStoreType", "wrong_type");

        Session testSession = this.fact.getSession(this.sslFreeBaseUrl);
        assertSecureSession(testSession);
        testSession.close();

        testSession = this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, PropertyDefinitions.XdevapiSslMode.REQUIRED));
        assertSecureSession(testSession);
        testSession.close();

        testSession = this.fact.getSession(this.sslFreeTestProperties);
        assertSecureSession(testSession);
        testSession.close();

        Properties props = new Properties(this.sslFreeTestProperties);
        props.setProperty(PropertyKey.xdevapiSslMode.getKeyName(), PropertyDefinitions.XdevapiSslMode.REQUIRED.toString());
        testSession = this.fact.getSession(props);
        assertSecureSession(testSession);
        testSession.close();
    }

    /**
     * Tests fix for Bug#27629553, NPE FROM GETSESSION() FOR SSL CONNECTION WHEN NO PASSWORD PASSED.
     */
    @Test
    public void testBug27629553() {
        assumeTrue(supportsTestCertificates(this.session),
                "This test requires the server configured with SSL certificates from ConnectorJ/src/test/config/ssl-test-certs");

        Session testSession = this.fact.getSession(this.baseUrl);
        testSession.sql("CREATE USER IF NOT EXISTS 'testBug27629553'@'%' IDENTIFIED WITH mysql_native_password").execute();
        testSession.sql("GRANT SELECT ON *.* TO 'testBug27629553'@'%'").execute();
        testSession.close();

        Properties props = (Properties) this.sslFreeTestProperties.clone();
        props.setProperty("user", "testBug27629553");
        props.remove("password");
        props.setProperty(PropertyKey.xdevapiSslMode.getKeyName(), PropertyDefinitions.XdevapiSslMode.VERIFY_CA.toString());
        props.setProperty(PropertyKey.xdevapiSslTrustStoreUrl.getKeyName(), this.trustStoreUrl);
        props.setProperty(PropertyKey.xdevapiSslTrustStorePassword.getKeyName(), this.trustStorePassword);
        props.setProperty(PropertyKey.clientCertificateKeyStoreUrl.getKeyName(), this.clientKeyStoreUrl);
        props.setProperty(PropertyKey.clientCertificateKeyStorePassword.getKeyName(), this.clientKeyStorePassword);

        testSession = this.fact.getSession(props);
        testSession.close();
    }

    @Test
    public void testXdevapiTlsVersionsAndCiphersuites() throws Exception {
        assumeTrue(supportsTestCertificates(this.session),
                "This test requires the server configured with SSL certificates from ConnectorJ/src/test/config/ssl-test-certs");
        assumeTrue(supportsTestCertificates(this.session), "This test requires the server with RSA support.");

        String testCipher = "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"; // TLSv1.2 IANA Cipher name.
        String expectedCipher = "ECDHE-RSA-AES128-GCM-SHA256"; // TLSv1.2 OpenSSL Cipher name.
        Session sess = this.fact.getSession(this.baseUrl);
        String testTlsVersion = getHighestCommonTlsVersion(sess); // At least TLSv1.2 is expected to be supported.
        String testCipher2 = "DHE-RSA-AES128-GCM-SHA256";
        sess.close();
        if ("TLSv1.3".equalsIgnoreCase(testTlsVersion)) {
            testCipher = "TLS_AES_256_GCM_SHA384"; // TLSv1.3 IANA Cipher name.
            expectedCipher = "TLS_AES_256_GCM_SHA384"; // TLSv1.3 IANA Cipher name.
            testCipher2 = "TLS_AES_128_GCM_SHA256";
        }

        Properties props = new Properties(this.sslFreeTestProperties);
        props.setProperty(PropertyKey.xdevapiSslMode.getKeyName(), PropertyDefinitions.XdevapiSslMode.VERIFY_CA.toString());
        props.setProperty(PropertyKey.xdevapiSslTrustStoreUrl.getKeyName(), this.trustStoreUrl);
        props.setProperty(PropertyKey.xdevapiSslTrustStorePassword.getKeyName(), this.trustStorePassword);

        Session testSession;
        final ClientFactory cf = new ClientFactory();

        // TS.FR.1_1. Create an X DevAPI session using a connection string containing the connection property xdevapi.tls-versions with a single TLS protocol.
        //            Assess that the connection property is initialized with the correct values and that the correct protocol was used (consult status variable
        //            ssl_version for details).
        //
        // UPD: Behaviour was changed by WL#14805.
        assertThrows(SSLParamsException.class, "TLS protocols TLSv1 and TLSv1.1 are not supported. Accepted values are TLSv1.2 and TLSv1.3.",
                () -> this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiTlsVersions, "TLSv1")));

        // TS.FR.1_2. Create an X DevAPI session using a connection string containing the connection property xdevapi.tls-versions with a valid list of TLS
        //            protocols.
        //            Assess that the connection property is initialized with the correct values and that the correct protocol was used (consult status variable
        //            ssl_version for details).
        testSession = this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiTlsVersions, "TLSv1.2,TLSv1.1,TLSv1"));
        assertSecureSession(testSession);
        assertTlsVersion(testSession, "TLSv1.2");
        testSession.close();

        // TS.FR.1_3. Create an X DevAPI session using a connection properties map containing the connection property xdevapi.tls-versions with a single TLS
        //            protocol.
        //            Assess that the connection property is initialized with the correct values and that the correct protocol was used (consult status variable
        //            ssl_version for details).
        //
        // UPD: Behaviour was changed by WL#14805.
        props.setProperty(PropertyKey.xdevapiTlsVersions.getKeyName(), "TLSv1");
        assertThrows(SSLParamsException.class, "TLS protocols TLSv1 and TLSv1.1 are not supported. Accepted values are TLSv1.2 and TLSv1.3.",
                () -> this.fact.getSession(props));

        // TS.FR.1_4. Create an X DevAPI session using a connection properties map containing the connection property xdevapi.tls-versions with a valid list of
        //            TLS protocols.
        //            Assess that the connection property is initialized with the correct values and that the correct protocol was used (consult status variable
        //            ssl_version for details).
        props.setProperty(PropertyKey.xdevapiTlsVersions.getKeyName(), "TLSv1.2,TLSv1.1,TLSv1");
        testSession = this.fact.getSession(props);
        assertSecureSession(testSession);
        assertTlsVersion(testSession, "TLSv1.2");
        testSession.close();

        // TS.FR.1_5. Repeat the tests TS.FR.1_1 and TS.FR.1_2 using a ClientFactory instead of a SessionFactory.
        //
        // UPD: Behaviour was changed by WL#14805.
        assertThrows(SSLParamsException.class, "TLS protocols TLSv1 and TLSv1.1 are not supported. Accepted values are TLSv1.2 and TLSv1.3.", () -> {
            Client cli1 = cf.getClient(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiTlsVersions, "TLSv1"), "{\"pooling\": {\"enabled\": true}}");
            cli1.getSession();
            return null;
        });

        Client cli = cf.getClient(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiTlsVersions, "TLSv1.2,TLSv1.1,TLSv1"),
                "{\"pooling\": {\"enabled\": true}}");
        testSession = cli.getSession();
        assertSecureSession(testSession);
        assertTlsVersion(testSession, "TLSv1.2");
        cli.close();

        // TS.FR.2_1. Create an X DevAPI session using a connection string containing the connection property xdevapi.tls-versions without any value.
        //            Assess that the code terminates with a WrongArgumentException containing the defined message.
        //
        // UPD: Behaviour was changed by WL#14805.
        assertThrows(SSLParamsException.class, "Specified list of TLS versions is empty. Accepted values are TLSv1.2 and TLSv1.3.",
                () -> this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiTlsVersions, "")));

        // TS.FR.2_2. Create an X DevAPI session using a connection properties map containing the connection property xdevapi.tls-versions without any value.
        //            Assess that the code terminates with a WrongArgumentException containing the defined message.
        //
        // UPD: Behaviour was changed by WL#14805.
        props.setProperty(PropertyKey.xdevapiTlsVersions.getKeyName(), "");
        assertThrows(SSLParamsException.class, "Specified list of TLS versions is empty. Accepted values are TLSv1.2 and TLSv1.3.",
                () -> this.fact.getSession(props));

        // TS.FR.2_3. Repeat the test TS.FR.2_1 using a ClientFactory instead of a SessionFactory.
        //
        // UPD: Behaviour was changed by WL#14805.
        assertThrows(SSLParamsException.class, "Specified list of TLS versions is empty. Accepted values are TLSv1.2 and TLSv1.3.", () -> {
            Client cli1 = cf.getClient(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiTlsVersions, ""), "{\"pooling\": {\"enabled\": true}}");
            cli1.getSession();
            return null;
        });

        // TS.FR.3_1. Create an X DevAPI session using a connection string containing the connection property xdevapi.tls-versions with
        //            an invalid value, for example SSLv3.
        //            Assess that the code terminates with a WrongArgumentException containing the defined message.
        //
        // UPD: Behaviour was changed by WL#14805.
        assertThrows(SSLParamsException.class, "Specified list of TLS versions only contains non valid TLS protocols. Accepted values are TLSv1.2 and TLSv1.3.",
                () -> this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiTlsVersions, "SSLv3")));

        // TS.FR.3_2. Create an X DevAPI session using a connection properties map containing the connection property xdevapi.tls-versions with
        //            an invalid value, for example SSLv3.
        //            Assess that the code terminates with a WrongArgumentException containing the defined message.
        //
        // UPD: Behaviour was changed by WL#14805.
        props.setProperty(PropertyKey.xdevapiTlsVersions.getKeyName(), "SSLv3");
        assertThrows(SSLParamsException.class, "Specified list of TLS versions only contains non valid TLS protocols. Accepted values are TLSv1.2 and TLSv1.3.",
                () -> this.fact.getSession(props));

        // TS.FR.3_3. Repeat the test TS.FR.3_1 using a ClientFactory instead of a SessionFactory.
        //
        // UPD: Behaviour was changed by WL#14805.
        assertThrows(SSLParamsException.class, "Specified list of TLS versions only contains non valid TLS protocols. Accepted values are TLSv1.2 and TLSv1.3.",
                () -> {
                    Client cli1 = cf.getClient(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiTlsVersions, "SSLv3"), "{\"pooling\": {\"enabled\": true}}");
                    cli1.getSession();
                    return null;
                });

        // TS.FR.4_1. Create an X DevAPI session using a connection string containing the connection property xdevapi.tls-ciphersuites with a single valid
        //            cipher-suite.
        //            Assess that the connection property is initialized with the correct values and that the correct protocol was used (consult status variable
        //            ssl_cipher for details).
        testSession = this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiTlsCiphersuites, testCipher));
        assertSessionStatusEquals(testSession, "mysqlx_ssl_cipher", expectedCipher);
        assertTlsVersion(testSession, testTlsVersion);
        testSession.close();

        // TS.FR.4_2. Create an X DevAPI session using a connection string containing the connection property xdevapi.tls-ciphersuites with a valid list of
        //            cipher-suites.
        //            Assess that the connection property is initialized with the correct values and that the correct protocol was used (consult status variable
        //            ssl_cipher for details).
        testSession = this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiTlsCiphersuites, testCipher + "," + testCipher2));
        assertSessionStatusEquals(testSession, "mysqlx_ssl_cipher", expectedCipher);
        assertTlsVersion(testSession, testTlsVersion);
        testSession.close();

        // TS.FR.4_3   Create an X DevAPI session using a connection string containing the connection property xdevapi.tls-ciphersuites with a list of valid and
        //             invalid cipher-suites, starting with an invalid one.
        //             Assess that the connection property is initialized with the correct values and that the correct protocol was used (consult status
        //             variable ssl_cipher for details).
        testSession = this.fact
                .getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiTlsCiphersuites, "TLS_RSA_EXPORT1024_WITH_RC4_56_MD5," + testCipher));
        assertSessionStatusEquals(testSession, "mysqlx_ssl_cipher", expectedCipher);
        assertTlsVersion(testSession, testTlsVersion);
        testSession.close();

        // TS.FR.4_4. Create an X DevAPI session using a connection string containing the connection property xdevapi.tls-ciphersuites with a single invalid
        //            cipher-suite.
        //            Assess that the connection property is initialized with the correct values and that the connection fails with an SSL error.
        Throwable ex = assertThrows(CJCommunicationsException.class, "Unable to connect to any of the target hosts\\.", () -> {
            this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiTlsCiphersuites, "TLS_RSA_EXPORT1024_WITH_RC4_56_MD5"));
            return null;
        });
        assertNotNull(ex.getCause());
        assertEquals("javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate)",
                ex.getCause().getMessage());

        // TS.FR.4_5. Create an X DevAPI session using a connection properties map containing the connection property xdevapi.tls-versions with a single valid
        //            cipher-suite.
        //            Assess that the connection property is initialized with the correct values and that the correct protocol was used (consult status variable
        //            ssl_cipher for details).
        props.remove(PropertyKey.xdevapiTlsVersions.getKeyName());
        props.setProperty(PropertyKey.xdevapiTlsCiphersuites.getKeyName(), testCipher);
        testSession = this.fact.getSession(props);
        assertSessionStatusEquals(testSession, "mysqlx_ssl_cipher", expectedCipher);
        testSession.close();

        // TS.FR.4_6. Create an X DevAPI session using a connection properties map containing the connection property xdevapi.tls-versions with a valid list of
        //            cipher-suites.
        //            Assess that the connection property is initialized with the correct values and that the correct protocol was used (consult status variable
        //            ssl_cipher for details).
        props.setProperty(PropertyKey.xdevapiTlsCiphersuites.getKeyName(), testCipher + "," + testCipher2);
        testSession = this.fact.getSession(props);
        assertSessionStatusEquals(testSession, "mysqlx_ssl_cipher", expectedCipher);
        testSession.close();

        // TS.FR.4_7. Create an X DevAPI session using a connection properties map containing the connection property xdevapi.tls-versions with a list of valid
        //            and invalid cipher-suites, starting with an invalid one.
        //            Assess that the connection property is initialized with the correct values and that the correct protocol was used (consult status variable
        //            ssl_cipher for details).
        props.setProperty(PropertyKey.xdevapiTlsCiphersuites.getKeyName(), "TLS_RSA_EXPORT1024_WITH_RC4_56_MD5," + testCipher);
        testSession = this.fact.getSession(props);
        assertSessionStatusEquals(testSession, "mysqlx_ssl_cipher", expectedCipher);
        testSession.close();

        // TS.FR.4_8. Create an X DevAPI session using a connection properties map containing the connection property xdevapi.tls-ciphersuites with a single
        //            invalid cipher-suite.
        //            Assess that the connection property is initialized with the correct values and that the connection fails with an SSL error.
        props.setProperty(PropertyKey.xdevapiTlsCiphersuites.getKeyName(), "TLS_RSA_EXPORT1024_WITH_RC4_56_MD5");
        assertThrows(CJCommunicationsException.class,
                "javax.net.ssl.SSLHandshakeException: No appropriate protocol \\(protocol is disabled or cipher suites are inappropriate\\)", () -> {
                    this.fact.getSession(props);
                    return null;
                });

        // TS.FR.4_9. Repeat the tests TS.FR.4_1 to TS.FR.4_4 using a ClientFactory instead of a SessionFactory.
        cli = cf.getClient(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiTlsCiphersuites, testCipher), "{\"pooling\": {\"enabled\": true}}");
        testSession = cli.getSession();
        assertSessionStatusEquals(testSession, "mysqlx_ssl_cipher", expectedCipher);
        assertTlsVersion(testSession, testTlsVersion);
        testSession.close();

        cli = cf.getClient(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiTlsCiphersuites, testCipher + "," + testCipher2),
                "{\"pooling\": {\"enabled\": true}}");
        testSession = cli.getSession();
        assertSessionStatusEquals(testSession, "mysqlx_ssl_cipher", expectedCipher);
        assertTlsVersion(testSession, testTlsVersion);
        testSession.close();

        cli = cf.getClient(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiTlsCiphersuites, "TLS_RSA_EXPORT1024_WITH_RC4_56_MD5," + testCipher),
                "{\"pooling\": {\"enabled\": true}}");
        testSession = cli.getSession();
        assertSessionStatusEquals(testSession, "mysqlx_ssl_cipher", expectedCipher);
        assertTlsVersion(testSession, testTlsVersion);
        testSession.close();

        ex = assertThrows(CJCommunicationsException.class, "Unable to connect to any of the target hosts\\.", () -> {
            Client cli1 = cf.getClient(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiTlsCiphersuites, "TLS_RSA_EXPORT1024_WITH_RC4_56_MD5"),
                    "{\"pooling\": {\"enabled\": true}}");
            cli1.getSession();
            return null;
        });
        assertNotNull(ex.getCause());
        assertEquals("javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate)",
                ex.getCause().getMessage());

        // TS.FR.5_1. Create an X DevAPI session using a connection string without the connection properties xdevapi.tls-versions and xdevapi.tls-ciphersuites.
        //            Assess that the session is created successfully and the connection properties are initialized with the expected values.
        testSession = this.fact.getSession(this.sslFreeBaseUrl);
        assertSecureSession(testSession);
        assertTlsVersion(testSession, testTlsVersion);
        testSession.close();

        // TS.FR.5_2. Create an X DevAPI session using a connection string with the connection property xdevapi.tls-versions but without
        //            xdevapi.tls-ciphersuites.
        //            Assess that the session is created successfully and the connection property xdevapi.tls-versions is initialized with the expected values.
        testSession = this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiTlsVersions, testTlsVersion));
        assertSecureSession(testSession);
        assertTlsVersion(testSession, testTlsVersion);
        testSession.close();

        // TS.FR.5_3. Create an X DevAPI session using a connection string with the connection property xdevapi.tls-ciphersuites but without
        //            xdevapi.tls-versions.
        //            Assess that the session is created successfully and the connection property xdevapi.tls-ciphersuites is initialized with the expected
        //            values.
        testSession = this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiTlsCiphersuites, testCipher));
        assertSessionStatusEquals(testSession, "mysqlx_ssl_cipher", expectedCipher);
        assertTlsVersion(testSession, testTlsVersion);
        testSession.close();

        // TS.FR.5_4. Create an X DevAPI session using a connection properties map without the connection properties xdevapi.tls-versions and
        //            xdevapi.tls-ciphersuites.
        //            Assess that the session is created successfully and the connection properties are initialized with the expected values.
        props.remove(PropertyKey.xdevapiTlsVersions.getKeyName());
        props.remove(PropertyKey.xdevapiTlsCiphersuites.getKeyName());
        testSession = this.fact.getSession(props);
        assertSecureSession(testSession);
        assertTlsVersion(testSession, testTlsVersion);
        testSession.close();

        // TS.FR.5_5. Create an X DevAPI session using a connection properties map with the connection property xdevapi.tls-versions but without
        //            xdevapi.tls-ciphersuites.
        //            Assess that the session is created successfully and the connection property xdevapi.tls-versions is initialized with the expected values.
        props.setProperty(PropertyKey.xdevapiTlsVersions.getKeyName(), testTlsVersion);
        testSession = this.fact.getSession(props);
        assertSecureSession(testSession);
        assertTlsVersion(testSession, testTlsVersion);
        testSession.close();

        // TS.FR.5_6. Create an X DevAPI session using a connection properties map with the connection property xdevapi.tls-ciphersuites but without
        //            xdevapi.tls-versions.
        //            Assess that the session is created successfully and the connection property xdevapi.tls-ciphersuites is initialized with the expected
        //            values.
        props.remove(PropertyKey.xdevapiTlsVersions.getKeyName());
        props.setProperty(PropertyKey.xdevapiTlsCiphersuites.getKeyName(), testCipher);
        testSession = this.fact.getSession(props);
        assertTlsVersion(testSession, testTlsVersion);
        assertSessionStatusEquals(testSession, "mysqlx_ssl_cipher", expectedCipher);
        testSession.close();

        // TS.FR.5_7. Repeat the tests TS.FR.5_1 to TS.FR.5_3 using a ClientFactory instead of a SessionFactory.
        cli = cf.getClient(this.sslFreeBaseUrl, "{\"pooling\": {\"enabled\": true}}");
        testSession = cli.getSession();
        assertSecureSession(testSession);
        assertTlsVersion(testSession, testTlsVersion);
        cli.close();

        cli = cf.getClient(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiTlsVersions, testTlsVersion), "{\"pooling\": {\"enabled\": true}}");
        testSession = cli.getSession();
        assertSecureSession(testSession);
        assertTlsVersion(testSession, testTlsVersion);
        cli.close();

        cli = cf.getClient(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiTlsCiphersuites, testCipher), "{\"pooling\": {\"enabled\": true}}");
        testSession = cli.getSession();
        assertSecureSession(testSession);
        assertTlsVersion(testSession, testTlsVersion);
        cli.close();

        // TS.FR.6_1. Create an X DevAPI session using a connection string with the connection property xdevapi.ssl-mode=DISABLED and both the connection
        //            properties xdevapi.tls-versions and xdevapi.tls-ciphersuites.
        //            Assess that the code terminates with a WrongArgumentException containing the defined message.
        //
        // UPD: Behaviour was changed by WL#14835.
        String xdevapiSSLMode = makeParam(PropertyKey.xdevapiSslMode, PropertyDefinitions.XdevapiSslMode.DISABLED.toString());
        testSession = this.fact.getSession(this.sslFreeBaseUrl + xdevapiSSLMode + makeParam(PropertyKey.xdevapiTlsVersions, testTlsVersion)
                + makeParam(PropertyKey.xdevapiTlsCiphersuites, testCipher));
        assertNonSecureSession(testSession);
        testSession.close();

        // TS.FR.6_2. Create an X DevAPI session using a connection string with the connection property xdevapi.ssl-mode=DISABLED and the connection property
        //            xdevapi.tls-versions but not xdevapi.tls-ciphersuites.
        //            Assess that the code terminates with a WrongArgumentException containing the defined message.
        //
        // UPD: Behaviour was changed by WL#14835.
        testSession = this.fact.getSession(this.sslFreeBaseUrl + xdevapiSSLMode + makeParam(PropertyKey.xdevapiTlsVersions, testTlsVersion));
        assertNonSecureSession(testSession);
        testSession.close();

        // TS.FR.6_3. Create an X DevAPI session using a connection string with the connection property xdevapi.ssl-mode=DISABLED and the connection property
        //            xdevapi.tls-ciphersuites but not xdevapi.tls-versions.
        //            Assess that the code terminates with a WrongArgumentException containing the defined message.
        //
        // UPD: Behaviour was changed by WL#14835.
        testSession = this.fact.getSession(this.sslFreeBaseUrl + xdevapiSSLMode + makeParam(PropertyKey.xdevapiTlsCiphersuites, testCipher));
        assertNonSecureSession(testSession);
        testSession.close();

        // TS.FR.6_4. Create an X DevAPI session using a connection properties map with the connection property xdevapi.ssl-mode=DISABLED and both the
        //            connection properties xdevapi.tls-versions and xdevapi.tls-ciphersuites.
        //            Assess that the code terminates with a WrongArgumentException containing the defined message.
        //
        // UPD: Behaviour was changed by WL#14835.
        props.setProperty(PropertyKey.xdevapiSslMode.getKeyName(), PropertyDefinitions.XdevapiSslMode.DISABLED.toString());
        props.setProperty(PropertyKey.xdevapiTlsVersions.getKeyName(), testTlsVersion);
        props.setProperty(PropertyKey.xdevapiTlsCiphersuites.getKeyName(), testCipher);
        testSession = this.fact.getSession(props);
        assertNonSecureSession(testSession);
        testSession.close();

        // TS.FR.6_5. Create an X DevAPI session using a connection properties map with the connection property xdevapi.ssl-mode=DISABLED and the connection
        //            property xdevapi.tls-versions but not xdevapi.tls-ciphersuites.
        //            Assess that the code terminates with a WrongArgumentException containing the defined message.
        //
        // UPD: Behaviour was changed by WL#14835.
        props.setProperty(PropertyKey.xdevapiTlsVersions.getKeyName(), testTlsVersion);
        props.remove(PropertyKey.xdevapiTlsCiphersuites.getKeyName());
        testSession = this.fact.getSession(props);
        assertNonSecureSession(testSession);
        testSession.close();

        // TS.FR.6_6. Create an X DevAPI session using a connection properties map with the connection property xdevapi.ssl-mode=DISABLED and the connection
        //            property xdevapi.tls-ciphersuites but not xdevapi.tls-versions.
        //            Assess that the code terminates with a WrongArgumentException containing the defined message.
        //
        // UPD: Behaviour was changed by WL#14835.
        props.remove(PropertyKey.xdevapiTlsVersions.getKeyName());
        props.setProperty(PropertyKey.xdevapiTlsCiphersuites.getKeyName(), testCipher);
        testSession = this.fact.getSession(props);
        assertNonSecureSession(testSession);
        testSession.close();

        // TS.FR.6_7. Repeat the tests TS.FR.6_1 to TS.FR.6_3 using a ClientFactory instead of a SessionFactory.
        //
        // UPD: Behaviour was changed by WL#14835.
        cli = cf.getClient(this.sslFreeBaseUrl + xdevapiSSLMode + makeParam(PropertyKey.xdevapiTlsVersions, testTlsVersion),
                "{\"pooling\": {\"enabled\": true}}");
        testSession = cli.getSession();
        assertNonSecureSession(testSession);
        cli.close();
        cli = cf.getClient(this.sslFreeBaseUrl + xdevapiSSLMode + makeParam(PropertyKey.xdevapiTlsVersions, testTlsVersion)
                + makeParam(PropertyKey.xdevapiTlsCiphersuites, testCipher), "{\"pooling\": {\"enabled\": true}}");
        testSession = cli.getSession();
        assertNonSecureSession(testSession);
        cli.close();
        cli = cf.getClient(this.sslFreeBaseUrl + xdevapiSSLMode + makeParam(PropertyKey.xdevapiTlsCiphersuites, testCipher),
                "{\"pooling\": {\"enabled\": true}}");
        testSession = cli.getSession();
        assertNonSecureSession(testSession);
        cli.close();
    }

    /**
     * Tests that given SSL/TLS related session properties values are processed as expected.
     *
     * @throws Exception
     */
    @Test
    public void testXdevapiSslConnectionOptions() throws Exception {
        assumeTrue(supportsTestCertificates(this.session),
                "This test requires the server configured with SSL certificates from ConnectorJ/src/test/config/ssl-test-certs");

        Session testSess;
        PropertySet propSet;

        /*
         * Check defaults.
         */
        testSess = this.fact.getSession(this.sslFreeBaseUrl);
        propSet = ((SessionImpl) testSess).getSession().getPropertySet();
        // X DevAPI options.
        assertEquals(XdevapiSslMode.REQUIRED, propSet.getProperty(PropertyKey.xdevapiSslMode).getValue());
        assertNull(propSet.getProperty(PropertyKey.xdevapiSslTrustStoreUrl).getValue());
        assertEquals("JKS", propSet.getProperty(PropertyKey.xdevapiSslTrustStoreType).getValue());
        assertNull(propSet.getProperty(PropertyKey.xdevapiSslTrustStorePassword).getValue());
        assertTrue(propSet.getBooleanProperty(PropertyKey.xdevapiFallbackToSystemTrustStore).getValue());
        assertNull(propSet.getProperty(PropertyKey.xdevapiSslKeyStoreUrl).getValue());
        assertEquals("JKS", propSet.getProperty(PropertyKey.xdevapiSslKeyStoreType).getValue());
        assertNull(propSet.getProperty(PropertyKey.xdevapiSslKeyStorePassword).getValue());
        assertTrue(propSet.getBooleanProperty(PropertyKey.xdevapiFallbackToSystemKeyStore).getValue());
        // Global (JDBC) options.
        assertEquals(SslMode.REQUIRED, propSet.getProperty(PropertyKey.sslMode).getValue());
        assertNull(propSet.getProperty(PropertyKey.trustCertificateKeyStoreUrl).getValue());
        assertEquals("JKS", propSet.getProperty(PropertyKey.trustCertificateKeyStoreType).getValue());
        assertNull(propSet.getProperty(PropertyKey.trustCertificateKeyStorePassword).getValue());
        assertTrue(propSet.getBooleanProperty(PropertyKey.fallbackToSystemTrustStore).getValue());
        assertNull(propSet.getProperty(PropertyKey.clientCertificateKeyStoreUrl).getValue());
        assertEquals("JKS", propSet.getProperty(PropertyKey.clientCertificateKeyStoreType).getValue());
        assertNull(propSet.getProperty(PropertyKey.clientCertificateKeyStorePassword).getValue());
        assertTrue(propSet.getBooleanProperty(PropertyKey.fallbackToSystemKeyStore).getValue());
        testSess.close();

        /*
         * Check SSL properties set globally (JDBC)
         */
        Properties props = new Properties(this.sslFreeTestProperties);
        // Set global SSL connection properties.
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.VERIFY_CA.toString());
        props.setProperty(PropertyKey.trustCertificateKeyStoreUrl.getKeyName(), "file:src/test/config/ssl-test-certs/ca-truststore");
        props.setProperty(PropertyKey.trustCertificateKeyStoreType.getKeyName(), "JKS");
        props.setProperty(PropertyKey.trustCertificateKeyStorePassword.getKeyName(), "password");
        props.setProperty(PropertyKey.fallbackToSystemTrustStore.getKeyName(), "false");
        props.setProperty(PropertyKey.clientCertificateKeyStoreUrl.getKeyName(), "file:src/test/config/ssl-test-certs/client-keystore");
        props.setProperty(PropertyKey.clientCertificateKeyStoreType.getKeyName(), "JKS");
        props.setProperty(PropertyKey.clientCertificateKeyStorePassword.getKeyName(), "password");
        props.setProperty(PropertyKey.fallbackToSystemKeyStore.getKeyName(), "false");

        testSess = this.fact.getSession(props);
        propSet = ((SessionImpl) testSess).getSession().getPropertySet();
        // X DevAPI options keep defaults.
        assertEquals(XdevapiSslMode.REQUIRED, propSet.getProperty(PropertyKey.xdevapiSslMode).getValue());
        assertNull(propSet.getProperty(PropertyKey.xdevapiSslTrustStoreUrl).getValue());
        assertEquals("JKS", propSet.getProperty(PropertyKey.xdevapiSslTrustStoreType).getValue());
        assertNull(propSet.getProperty(PropertyKey.xdevapiSslTrustStorePassword).getValue());
        assertTrue(propSet.getBooleanProperty(PropertyKey.xdevapiFallbackToSystemTrustStore).getValue());
        assertNull(propSet.getProperty(PropertyKey.xdevapiSslKeyStoreUrl).getValue());
        assertEquals("JKS", propSet.getProperty(PropertyKey.xdevapiSslKeyStoreType).getValue());
        assertNull(propSet.getProperty(PropertyKey.xdevapiSslKeyStorePassword).getValue());
        assertTrue(propSet.getBooleanProperty(PropertyKey.xdevapiFallbackToSystemKeyStore).getValue());
        // Global (JDBC) were set.
        assertEquals(SslMode.VERIFY_CA, propSet.getProperty(PropertyKey.sslMode).getValue());
        assertEquals("file:src/test/config/ssl-test-certs/ca-truststore", propSet.getProperty(PropertyKey.trustCertificateKeyStoreUrl).getValue());
        assertEquals("JKS", propSet.getProperty(PropertyKey.trustCertificateKeyStoreType).getValue());
        assertEquals("password", propSet.getProperty(PropertyKey.trustCertificateKeyStorePassword).getValue());
        assertFalse(propSet.getBooleanProperty(PropertyKey.fallbackToSystemTrustStore).getValue());
        assertEquals("file:src/test/config/ssl-test-certs/client-keystore", propSet.getProperty(PropertyKey.clientCertificateKeyStoreUrl).getValue());
        assertEquals("JKS", propSet.getProperty(PropertyKey.clientCertificateKeyStoreType).getValue());
        assertEquals("password", propSet.getProperty(PropertyKey.clientCertificateKeyStorePassword).getValue());
        assertFalse(propSet.getBooleanProperty(PropertyKey.fallbackToSystemKeyStore).getValue());
        testSess.close();

        props.setProperty(PropertyKey.fallbackToSystemTrustStore.getKeyName(), "true");
        props.setProperty(PropertyKey.fallbackToSystemKeyStore.getKeyName(), "true");

        testSess = this.fact.getSession(props);
        propSet = ((SessionImpl) testSess).getSession().getPropertySet();
        // X DevAPI options keep defaults.
        assertEquals(XdevapiSslMode.REQUIRED, propSet.getProperty(PropertyKey.xdevapiSslMode).getValue());
        assertNull(propSet.getProperty(PropertyKey.xdevapiSslTrustStoreUrl).getValue());
        assertEquals("JKS", propSet.getProperty(PropertyKey.xdevapiSslTrustStoreType).getValue());
        assertNull(propSet.getProperty(PropertyKey.xdevapiSslTrustStorePassword).getValue());
        assertTrue(propSet.getBooleanProperty(PropertyKey.xdevapiFallbackToSystemTrustStore).getValue());
        assertNull(propSet.getProperty(PropertyKey.xdevapiSslKeyStoreUrl).getValue());
        assertEquals("JKS", propSet.getProperty(PropertyKey.xdevapiSslKeyStoreType).getValue());
        assertNull(propSet.getProperty(PropertyKey.xdevapiSslKeyStorePassword).getValue());
        assertTrue(propSet.getBooleanProperty(PropertyKey.xdevapiFallbackToSystemKeyStore).getValue());
        // Global (JDBC) options were set.
        assertEquals(SslMode.VERIFY_CA, propSet.getProperty(PropertyKey.sslMode).getValue());
        assertEquals("file:src/test/config/ssl-test-certs/ca-truststore", propSet.getProperty(PropertyKey.trustCertificateKeyStoreUrl).getValue());
        assertEquals("JKS", propSet.getProperty(PropertyKey.trustCertificateKeyStoreType).getValue());
        assertEquals("password", propSet.getProperty(PropertyKey.trustCertificateKeyStorePassword).getValue());
        assertTrue(propSet.getBooleanProperty(PropertyKey.fallbackToSystemTrustStore).getValue());
        assertEquals("file:src/test/config/ssl-test-certs/client-keystore", propSet.getProperty(PropertyKey.clientCertificateKeyStoreUrl).getValue());
        assertEquals("JKS", propSet.getProperty(PropertyKey.clientCertificateKeyStoreType).getValue());
        assertEquals("password", propSet.getProperty(PropertyKey.clientCertificateKeyStorePassword).getValue());
        assertTrue(propSet.getBooleanProperty(PropertyKey.fallbackToSystemKeyStore).getValue());
        testSess.close();

        /*
         * Check SSL properties set locally on the X DevAPI.
         */
        props = new Properties(this.sslFreeTestProperties);
        // Set global SSL connection properties.
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.toString());
        props.setProperty(PropertyKey.trustCertificateKeyStoreUrl.getKeyName(), "trust-cert-keystore-url");
        props.setProperty(PropertyKey.trustCertificateKeyStoreType.getKeyName(), "trust-cert-keystore-type");
        props.setProperty(PropertyKey.trustCertificateKeyStorePassword.getKeyName(), "trust-cert-keystore-pwd");
        props.setProperty(PropertyKey.fallbackToSystemTrustStore.getKeyName(), "false");
        props.setProperty(PropertyKey.clientCertificateKeyStoreUrl.getKeyName(), "client-cert-keystore-url");
        props.setProperty(PropertyKey.clientCertificateKeyStoreType.getKeyName(), "client-cert-keystore-type");
        props.setProperty(PropertyKey.clientCertificateKeyStorePassword.getKeyName(), "client-cert-keystore-pwd");
        props.setProperty(PropertyKey.fallbackToSystemKeyStore.getKeyName(), "false");
        // Set X DevAPI local connection properties.
        props.setProperty(PropertyKey.xdevapiSslMode.getKeyName(), XdevapiSslMode.VERIFY_CA.toString());
        props.setProperty(PropertyKey.xdevapiSslTrustStoreUrl.getKeyName(), "file:src/test/config/ssl-test-certs/ca-truststore");
        props.setProperty(PropertyKey.xdevapiSslTrustStoreType.getKeyName(), "JKS");
        props.setProperty(PropertyKey.xdevapiSslTrustStorePassword.getKeyName(), "password");
        props.setProperty(PropertyKey.xdevapiFallbackToSystemTrustStore.getKeyName(), "false");
        props.setProperty(PropertyKey.xdevapiSslKeyStoreUrl.getKeyName(), "file:src/test/config/ssl-test-certs/client-keystore");
        props.setProperty(PropertyKey.xdevapiSslKeyStoreType.getKeyName(), "JKS");
        props.setProperty(PropertyKey.xdevapiSslKeyStorePassword.getKeyName(), "password");
        props.setProperty(PropertyKey.xdevapiFallbackToSystemKeyStore.getKeyName(), "false");

        testSess = this.fact.getSession(props);
        propSet = ((SessionImpl) testSess).getSession().getPropertySet();
        // X DevAPI options were set.
        assertEquals(XdevapiSslMode.VERIFY_CA, propSet.getProperty(PropertyKey.xdevapiSslMode).getValue());
        assertEquals("file:src/test/config/ssl-test-certs/ca-truststore", propSet.getProperty(PropertyKey.xdevapiSslTrustStoreUrl).getValue());
        assertEquals("JKS", propSet.getProperty(PropertyKey.xdevapiSslTrustStoreType).getValue());
        assertEquals("password", propSet.getProperty(PropertyKey.xdevapiSslTrustStorePassword).getValue());
        assertFalse(propSet.getBooleanProperty(PropertyKey.xdevapiFallbackToSystemTrustStore).getValue());
        assertEquals("file:src/test/config/ssl-test-certs/client-keystore", propSet.getProperty(PropertyKey.xdevapiSslKeyStoreUrl).getValue());
        assertEquals("JKS", propSet.getProperty(PropertyKey.xdevapiSslKeyStoreType).getValue());
        assertEquals("password", propSet.getProperty(PropertyKey.xdevapiSslKeyStorePassword).getValue());
        assertFalse(propSet.getBooleanProperty(PropertyKey.xdevapiFallbackToSystemKeyStore).getValue());
        // Global (JDBC) options were overridden.
        assertEquals(SslMode.VERIFY_CA, propSet.getProperty(PropertyKey.sslMode).getValue());
        assertEquals("file:src/test/config/ssl-test-certs/ca-truststore", propSet.getProperty(PropertyKey.trustCertificateKeyStoreUrl).getValue());
        assertEquals("JKS", propSet.getProperty(PropertyKey.trustCertificateKeyStoreType).getValue());
        assertEquals("password", propSet.getProperty(PropertyKey.trustCertificateKeyStorePassword).getValue());
        assertFalse(propSet.getBooleanProperty(PropertyKey.fallbackToSystemTrustStore).getValue());
        assertEquals("file:src/test/config/ssl-test-certs/client-keystore", propSet.getProperty(PropertyKey.clientCertificateKeyStoreUrl).getValue());
        assertEquals("JKS", propSet.getProperty(PropertyKey.clientCertificateKeyStoreType).getValue());
        assertEquals("password", propSet.getProperty(PropertyKey.clientCertificateKeyStorePassword).getValue());
        assertFalse(propSet.getBooleanProperty(PropertyKey.fallbackToSystemKeyStore).getValue());
        testSess.close();

        props.setProperty(PropertyKey.xdevapiFallbackToSystemTrustStore.getKeyName(), "true");
        props.setProperty(PropertyKey.xdevapiFallbackToSystemKeyStore.getKeyName(), "true");

        testSess = this.fact.getSession(props);
        propSet = ((SessionImpl) testSess).getSession().getPropertySet();
        // X DevAPI options were set.
        assertEquals(XdevapiSslMode.VERIFY_CA, propSet.getProperty(PropertyKey.xdevapiSslMode).getValue());
        assertEquals("file:src/test/config/ssl-test-certs/ca-truststore", propSet.getProperty(PropertyKey.xdevapiSslTrustStoreUrl).getValue());
        assertEquals("JKS", propSet.getProperty(PropertyKey.xdevapiSslTrustStoreType).getValue());
        assertEquals("password", propSet.getProperty(PropertyKey.xdevapiSslTrustStorePassword).getValue());
        assertTrue(propSet.getBooleanProperty(PropertyKey.xdevapiFallbackToSystemTrustStore).getValue());
        assertEquals("file:src/test/config/ssl-test-certs/client-keystore", propSet.getProperty(PropertyKey.xdevapiSslKeyStoreUrl).getValue());
        assertEquals("JKS", propSet.getProperty(PropertyKey.xdevapiSslKeyStoreType).getValue());
        assertEquals("password", propSet.getProperty(PropertyKey.xdevapiSslKeyStorePassword).getValue());
        assertTrue(propSet.getBooleanProperty(PropertyKey.xdevapiFallbackToSystemKeyStore).getValue());
        // Global (JDBC) options were overridden.
        assertEquals(SslMode.VERIFY_CA, propSet.getProperty(PropertyKey.sslMode).getValue());
        assertEquals("file:src/test/config/ssl-test-certs/ca-truststore", propSet.getProperty(PropertyKey.trustCertificateKeyStoreUrl).getValue());
        assertEquals("JKS", propSet.getProperty(PropertyKey.trustCertificateKeyStoreType).getValue());
        assertEquals("password", propSet.getProperty(PropertyKey.trustCertificateKeyStorePassword).getValue());
        assertTrue(propSet.getBooleanProperty(PropertyKey.fallbackToSystemTrustStore).getValue());
        assertEquals("file:src/test/config/ssl-test-certs/client-keystore", propSet.getProperty(PropertyKey.clientCertificateKeyStoreUrl).getValue());
        assertEquals("JKS", propSet.getProperty(PropertyKey.clientCertificateKeyStoreType).getValue());
        assertEquals("password", propSet.getProperty(PropertyKey.clientCertificateKeyStorePassword).getValue());
        assertTrue(propSet.getBooleanProperty(PropertyKey.fallbackToSystemKeyStore).getValue());
        testSess.close();
    }

    /**
     * Tests connection property 'xdevapi.fallback-to-system-truststore' behavior.
     *
     * @throws Exception
     */
    @Test
    public void testFallbackToSystemTrustStore() throws Exception {
        assumeTrue(supportsTestCertificates(this.session),
                "This test requires the server configured with SSL certificates from ConnectorJ/src/test/config/ssl-test-certs");

        Session testSess;

        /*
         * Valid system-wide TrustStore.
         */
        System.setProperty("javax.net.ssl.trustStore", "file:src/test/config/ssl-test-certs/ca-truststore");
        System.setProperty("javax.net.ssl.trustStoreType", "JKS");
        System.setProperty("javax.net.ssl.trustStorePassword", "password");

        // No session-local TrustStore.
        testSess = this.fact.getSession(this.sslFreeBaseUrl);
        assertSecureSession(testSess);
        testSess.close();
        testSess = this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, XdevapiSslMode.VERIFY_CA));
        assertSecureSession(testSess);
        testSess.close();
        testSess = this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, XdevapiSslMode.VERIFY_CA)
                + makeParam(PropertyKey.xdevapiFallbackToSystemTrustStore, "true"));
        assertSecureSession(testSess);
        testSess.close();
        assertThrows(CJCommunicationsException.class, () -> this.fact.getSession(this.sslFreeBaseUrl
                + makeParam(PropertyKey.xdevapiSslMode, XdevapiSslMode.VERIFY_CA) + makeParam(PropertyKey.xdevapiFallbackToSystemTrustStore, "false")));

        // Invalid session-local TrustStore:
        testSess = this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, XdevapiSslMode.REQUIRED)
                + makeParam(PropertyKey.xdevapiSslTrustStoreUrl, "file:src/test/config/ssl-test-certs/ca-truststore-ext")
                + makeParam(PropertyKey.xdevapiSslTrustStoreType, "JKS") + makeParam(PropertyKey.xdevapiSslTrustStorePassword, "password"));
        assertSecureSession(testSess);
        testSess.close();
        assertThrows(CJCommunicationsException.class,
                () -> this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, XdevapiSslMode.VERIFY_CA)
                        + makeParam(PropertyKey.xdevapiSslTrustStoreUrl, "file:src/test/config/ssl-test-certs/ca-truststore-ext")
                        + makeParam(PropertyKey.xdevapiSslTrustStoreType, "JKS") + makeParam(PropertyKey.xdevapiSslTrustStorePassword, "password")));
        assertThrows(CJCommunicationsException.class,
                () -> this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, XdevapiSslMode.VERIFY_CA)
                        + makeParam(PropertyKey.xdevapiFallbackToSystemTrustStore, "true")
                        + makeParam(PropertyKey.xdevapiSslTrustStoreUrl, "file:src/test/config/ssl-test-certs/ca-truststore-ext")
                        + makeParam(PropertyKey.xdevapiSslTrustStoreType, "JKS") + makeParam(PropertyKey.xdevapiSslTrustStorePassword, "password")));
        assertThrows(CJCommunicationsException.class,
                () -> this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, XdevapiSslMode.VERIFY_CA)
                        + makeParam(PropertyKey.xdevapiFallbackToSystemTrustStore, "false")
                        + makeParam(PropertyKey.xdevapiSslTrustStoreUrl, "file:src/test/config/ssl-test-certs/ca-truststore-ext")
                        + makeParam(PropertyKey.xdevapiSslTrustStoreType, "JKS") + makeParam(PropertyKey.xdevapiSslTrustStorePassword, "password")));

        /*
         * Invalid system-wide TrustStore.
         */
        System.setProperty("javax.net.ssl.trustStore", "file:src/test/config/ssl-test-certs/ca-truststore-ext");
        System.setProperty("javax.net.ssl.trustStoreType", "JKS");
        System.setProperty("javax.net.ssl.trustStorePassword", "password");

        // No session-local TrustStore.
        testSess = this.fact.getSession(this.sslFreeBaseUrl);
        assertSecureSession(testSess);
        testSess.close();
        assertThrows(CJCommunicationsException.class,
                () -> this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, XdevapiSslMode.VERIFY_CA)));
        assertThrows(CJCommunicationsException.class, () -> this.fact.getSession(this.sslFreeBaseUrl
                + makeParam(PropertyKey.xdevapiSslMode, XdevapiSslMode.VERIFY_CA) + makeParam(PropertyKey.xdevapiFallbackToSystemTrustStore, "true")));
        assertThrows(CJCommunicationsException.class, () -> this.fact.getSession(this.sslFreeBaseUrl
                + makeParam(PropertyKey.xdevapiSslMode, XdevapiSslMode.VERIFY_CA) + makeParam(PropertyKey.xdevapiFallbackToSystemTrustStore, "false")));

        // Valid session-local TrustStore:
        testSess = this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, XdevapiSslMode.REQUIRED)
                + makeParam(PropertyKey.xdevapiSslTrustStoreUrl, "file:src/test/config/ssl-test-certs/ca-truststore")
                + makeParam(PropertyKey.xdevapiSslTrustStoreType, "JKS") + makeParam(PropertyKey.xdevapiSslTrustStorePassword, "password"));
        assertSecureSession(testSess);
        testSess.close();
        testSess = this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, XdevapiSslMode.VERIFY_CA)
                + makeParam(PropertyKey.xdevapiSslTrustStoreUrl, "file:src/test/config/ssl-test-certs/ca-truststore")
                + makeParam(PropertyKey.xdevapiSslTrustStoreType, "JKS") + makeParam(PropertyKey.xdevapiSslTrustStorePassword, "password"));
        assertSecureSession(testSess);
        testSess.close();
        testSess = this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, XdevapiSslMode.VERIFY_CA)
                + makeParam(PropertyKey.xdevapiFallbackToSystemTrustStore, "true")
                + makeParam(PropertyKey.xdevapiSslTrustStoreUrl, "file:src/test/config/ssl-test-certs/ca-truststore")
                + makeParam(PropertyKey.xdevapiSslTrustStoreType, "JKS") + makeParam(PropertyKey.xdevapiSslTrustStorePassword, "password"));
        assertSecureSession(testSess);
        testSess.close();
        testSess = this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, XdevapiSslMode.VERIFY_CA)
                + makeParam(PropertyKey.xdevapiFallbackToSystemTrustStore, "false")
                + makeParam(PropertyKey.xdevapiSslTrustStoreUrl, "file:src/test/config/ssl-test-certs/ca-truststore")
                + makeParam(PropertyKey.xdevapiSslTrustStoreType, "JKS") + makeParam(PropertyKey.xdevapiSslTrustStorePassword, "password"));
        assertSecureSession(testSess);
        testSess.close();
    }

    /**
     * Tests connection property 'xdevapi.fallback-to-system-keystore' behavior.
     *
     * @throws Exception
     */
    @Test
    public void testFallbackToSystemKeyStore() throws Exception {
        assumeTrue(supportsTestCertificates(this.session),
                "This test requires the server configured with SSL certificates from ConnectorJ/src/test/config/ssl-test-certs");

        final String user = "testFbToSysKS";
        try {
            Session testSession = this.fact.getSession(this.baseUrl);
            testSession.sql("CREATE USER IF NOT EXISTS '" + user + "'@'%' IDENTIFIED BY 'password' REQUIRE X509").execute();
            testSession.sql("GRANT ALL ON *.* TO '" + user + "'@'%'").execute();
            testSession.close();

            final Properties props = new Properties(this.sslFreeTestProperties);
            props.setProperty(PropertyKey.USER.getKeyName(), user);
            props.setProperty(PropertyKey.PASSWORD.getKeyName(), "password");
            props.setProperty(PropertyKey.xdevapiSslMode.getKeyName(), XdevapiSslMode.REQUIRED.toString());

            Session testSess;

            /*
             * Valid system-wide KeyStore.
             */
            System.setProperty("javax.net.ssl.keyStore", "file:src/test/config/ssl-test-certs/client-keystore");
            System.setProperty("javax.net.ssl.keyStoreType", "JKS");
            System.setProperty("javax.net.ssl.keyStorePassword", "password");

            // No connection-local KeyStore.
            testSess = this.fact.getSession(props);
            assertSecureSession(testSess, user);
            testSess.close();
            props.setProperty(PropertyKey.xdevapiFallbackToSystemKeyStore.getKeyName(), "true");
            testSess = this.fact.getSession(props);
            assertSecureSession(testSess, user);
            testSess.close();
            props.setProperty(PropertyKey.xdevapiFallbackToSystemKeyStore.getKeyName(), "false");
            assertThrows(XProtocolError.class, mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.12")) ? ".*Access denied for user '" + user + "'@.*"
                    : ".*Current account requires TLS to be activate.", () -> this.fact.getSession(props));

            props.remove(PropertyKey.xdevapiFallbackToSystemKeyStore.getKeyName());

            // Invalid connection-local KeyStore:
            props.setProperty(PropertyKey.xdevapiSslKeyStoreUrl.getKeyName(), "file:src/test/config/ssl-test-certs/client-keystore-ext");
            props.setProperty(PropertyKey.xdevapiSslKeyStoreType.getKeyName(), "JKS");
            props.setProperty(PropertyKey.xdevapiSslKeyStorePassword.getKeyName(), "password");
            assertThrows(CJCommunicationsException.class, () -> this.fact.getSession(props));
            props.setProperty(PropertyKey.xdevapiFallbackToSystemKeyStore.getKeyName(), "true");
            assertThrows(CJCommunicationsException.class, () -> this.fact.getSession(props));
            props.setProperty(PropertyKey.xdevapiFallbackToSystemKeyStore.getKeyName(), "false");
            assertThrows(CJCommunicationsException.class, () -> this.fact.getSession(props));

            props.remove(PropertyKey.xdevapiSslKeyStoreUrl.getKeyName());
            props.remove(PropertyKey.xdevapiSslKeyStoreType.getKeyName());
            props.remove(PropertyKey.xdevapiSslKeyStorePassword.getKeyName());
            props.remove(PropertyKey.xdevapiFallbackToSystemKeyStore.getKeyName());

            /*
             * Invalid system-wide KeyStore.
             */
            System.setProperty("javax.net.ssl.keyStore", "file:src/test/config/ssl-test-certs/client-keystore-ext");
            System.setProperty("javax.net.ssl.keyStoreType", "JKS");
            System.setProperty("javax.net.ssl.keyStorePassword", "password");

            // No connection-local KeyStore.
            assertThrows(CJCommunicationsException.class, () -> this.fact.getSession(props));
            props.setProperty(PropertyKey.xdevapiFallbackToSystemKeyStore.getKeyName(), "true");
            assertThrows(CJCommunicationsException.class, () -> this.fact.getSession(props));
            props.setProperty(PropertyKey.xdevapiFallbackToSystemKeyStore.getKeyName(), "false");
            assertThrows(XProtocolError.class, mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.12")) ? ".*Access denied for user '" + user + "'@.*"
                    : ".*Current account requires TLS to be activate.", () -> this.fact.getSession(props));

            props.remove(PropertyKey.xdevapiFallbackToSystemKeyStore.getKeyName());

            // Valid connection-local KeyStore:
            props.setProperty(PropertyKey.xdevapiSslKeyStoreUrl.getKeyName(), "file:src/test/config/ssl-test-certs/client-keystore");
            props.setProperty(PropertyKey.xdevapiSslKeyStoreType.getKeyName(), "JKS");
            props.setProperty(PropertyKey.xdevapiSslKeyStorePassword.getKeyName(), "password");
            testSess = this.fact.getSession(props);
            assertSecureSession(testSess, user);
            testSess.close();
            props.setProperty(PropertyKey.xdevapiFallbackToSystemKeyStore.getKeyName(), "true");
            testSess = this.fact.getSession(props);
            assertSecureSession(testSess, user);
            testSess.close();
            props.setProperty(PropertyKey.xdevapiFallbackToSystemKeyStore.getKeyName(), "false");
            testSess = this.fact.getSession(props);
            assertSecureSession(testSess, user);
            testSess.close();
        } finally {
            System.clearProperty("javax.net.ssl.keyStore");
            System.clearProperty("javax.net.ssl.keyStoreType");
            System.clearProperty("javax.net.ssl.keyStorePassword");

            Session testSession = this.fact.getSession(this.baseUrl);
            testSession.sql("DROP USER IF EXISTS " + user).execute();
            testSession.close();
        }
    }

    /**
     * Tests WL#14805, Remove support for TLS 1.0 and 1.1.
     *
     * @throws Exception
     */
    @Test
    public void testTLSVersionRemoval() throws Exception {
        assumeTrue(supportsTestCertificates(this.session),
                "This test requires the server configured with SSL certificates from ConnectorJ/src/test/config/ssl-test-certs");

        String testCipher = "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"; // TLSv1.2 IANA Cipher name.
        String expectedCipher = "ECDHE-RSA-AES128-GCM-SHA256"; // TLSv1.2 OpenSSL Cipher name.
        Session sess = this.fact.getSession(this.baseUrl);
        String testTlsVersion = getHighestCommonTlsVersion(sess); // At least TLSv1.2 is expected to be supported.
        sess.close();
        if ("TLSv1.3".equalsIgnoreCase(testTlsVersion)) {
            testCipher = "TLS_AES_256_GCM_SHA384"; // TLSv1.3 IANA Cipher name.
            expectedCipher = "TLS_AES_256_GCM_SHA384"; // TLSv1.3 IANA Cipher name.
        }

        Properties props = new Properties(this.sslFreeTestProperties);
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.REQUIRED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");

        // TS.FR.1_1. Create a Connection with the connection property tlsVersions=TLSv1.2. Assess that the connection is created successfully and it is using TLSv1.2.
        props.setProperty(PropertyKey.tlsVersions.getKeyName(), testTlsVersion);
        sess = this.fact.getSession(props);
        assertSecureSession(sess);
        assertTlsVersion(sess, testTlsVersion);
        sess.close();

        props.remove(PropertyKey.tlsVersions.getKeyName());
        props.setProperty(PropertyKey.xdevapiTlsVersions.getKeyName(), testTlsVersion);
        sess = this.fact.getSession(props);
        assertSecureSession(sess);
        assertTlsVersion(sess, testTlsVersion);
        sess.close();

        // TS.FR.1_2. Create a Connection with the connection property enabledTLSProtocols=TLSv1.2. Assess that the connection is created successfully and it is using TLSv1.2.
        props.remove(PropertyKey.xdevapiTlsVersions.getKeyName());
        props.setProperty("enabledTLSProtocols", testTlsVersion);
        sess = this.fact.getSession(props);
        assertSecureSession(sess);
        assertTlsVersion(sess, testTlsVersion);
        sess.close();
        props.remove("enabledTLSProtocols");

        // TS.FR.2_1. Create a Connection with the connection property tlsCiphersuites=[valid-cipher-suite]. Assess that the connection is created successfully and it is using the cipher suite specified.
        props.setProperty(PropertyKey.tlsCiphersuites.getKeyName(), testCipher);
        sess = this.fact.getSession(props);
        assertSecureSession(sess);
        assertSessionStatusEquals(sess, "mysqlx_ssl_cipher", expectedCipher);
        sess.close();

        props.remove(PropertyKey.tlsCiphersuites.getKeyName());
        props.setProperty(PropertyKey.xdevapiTlsCiphersuites.getKeyName(), testCipher);
        sess = this.fact.getSession(props);
        assertSecureSession(sess);
        assertSessionStatusEquals(sess, "mysqlx_ssl_cipher", expectedCipher);
        sess.close();

        // TS.FR.2_2. Create a Connection with the connection property enabledSSLCipherSuites=[valid-cipher-suite] . Assess that the connection is created successfully and it is using the cipher suite specified.
        props.remove(PropertyKey.xdevapiTlsCiphersuites.getKeyName());
        props.setProperty("enabledSSLCipherSuites", testCipher);
        sess = this.fact.getSession(props);
        assertSecureSession(sess);
        assertSessionStatusEquals(sess, "mysqlx_ssl_cipher", expectedCipher);
        sess.close();
        props.remove("enabledSSLCipherSuites");

        // TS.FR.3_1. Create a Connection with the connection property tlsVersions=TLSv1. Assess that the connection fails.
        props.setProperty(PropertyKey.tlsVersions.getKeyName(), "TLSv1");
        assertThrows(CJCommunicationsException.class, ".+TLS protocols TLSv1 and TLSv1.1 are not supported. Accepted values are TLSv1.2 and TLSv1.3.+",
                () -> this.fact.getSession(props));

        props.remove(PropertyKey.tlsVersions.getKeyName());
        props.setProperty(PropertyKey.xdevapiTlsVersions.getKeyName(), "TLSv1");
        assertThrows(SSLParamsException.class, "TLS protocols TLSv1 and TLSv1.1 are not supported. Accepted values are TLSv1.2 and TLSv1.3.+",
                () -> this.fact.getSession(props));

        // TS.FR.3_2. Create a Connection with the connection property tlsVersions=TLSv1.1. Assess that the connection fails.
        props.remove(PropertyKey.xdevapiTlsVersions.getKeyName());
        props.setProperty(PropertyKey.tlsVersions.getKeyName(), "TLSv1.1");
        assertThrows(CJCommunicationsException.class, ".+TLS protocols TLSv1 and TLSv1.1 are not supported. Accepted values are TLSv1.2 and TLSv1.3.+",
                () -> this.fact.getSession(props));

        props.remove(PropertyKey.tlsVersions.getKeyName());
        props.setProperty(PropertyKey.xdevapiTlsVersions.getKeyName(), "TLSv1.1");
        assertThrows(SSLParamsException.class, "TLS protocols TLSv1 and TLSv1.1 are not supported. Accepted values are TLSv1.2 and TLSv1.3.+",
                () -> this.fact.getSession(props));
        props.remove(PropertyKey.xdevapiTlsVersions.getKeyName());

        // TS.FR.3_3. Create a Connection with the connection property enabledTLSProtocols=TLSv1. Assess that the connection fails.
        props.setProperty("enabledTLSProtocols", "TLSv1");
        assertThrows(CJCommunicationsException.class, ".+TLS protocols TLSv1 and TLSv1.1 are not supported. Accepted values are TLSv1.2 and TLSv1.3.+",
                () -> this.fact.getSession(props));
        props.remove("enabledTLSProtocols");

        // TS.FR.3_4. Create a Connection with the connection property enabledTLSProtocols=TLSv1.1. Assess that the connection fails.
        props.setProperty("enabledTLSProtocols", "TLSv1.1");
        assertThrows(CJCommunicationsException.class, ".+TLS protocols TLSv1 and TLSv1.1 are not supported. Accepted values are TLSv1.2 and TLSv1.3.+",
                () -> this.fact.getSession(props));
        props.remove("enabledTLSProtocols");

        // TS.FR.4. Create a Connection with the connection property tlsVersions=TLSv1 and sslMode=DISABLED. Assess that the connection is created successfully and it is not using encryption.
        props.setProperty(PropertyKey.tlsVersions.getKeyName(), "TLSv1");
        props.setProperty(PropertyKey.xdevapiSslMode.getKeyName(), SslMode.DISABLED.name());
        sess = this.fact.getSession(props);
        assertNonSecureSession(sess);
        sess.close();
        props.remove(PropertyKey.tlsVersions.getKeyName());

        props.setProperty(PropertyKey.xdevapiTlsVersions.getKeyName(), "TLSv1");
        sess = this.fact.getSession(props);
        assertNonSecureSession(sess);
        sess.close();

        props.remove(PropertyKey.xdevapiTlsVersions.getKeyName());
        props.remove(PropertyKey.xdevapiSslMode.getKeyName());

        // TS.FR.5_1. Create a Connection with the connection property tlsVersions=FOO,BAR.
        //            Assess that the connection fails with the error message "Specified list of TLS versions only contains non valid TLS protocols. Accepted values are TLSv1.2 and TLSv1.3."
        props.setProperty(PropertyKey.tlsVersions.getKeyName(), "FOO,BAR");
        assertThrows(CJCommunicationsException.class,
                ".+Specified list of TLS versions only contains non valid TLS protocols. Accepted values are TLSv1.2 and TLSv1.3.+",
                () -> this.fact.getSession(props));

        props.setProperty(PropertyKey.tlsVersions.getKeyName(), "FOO,,,BAR");
        assertThrows(CJCommunicationsException.class,
                ".+Specified list of TLS versions only contains non valid TLS protocols. Accepted values are TLSv1.2 and TLSv1.3.+",
                () -> this.fact.getSession(props));

        props.remove(PropertyKey.tlsVersions.getKeyName());

        props.setProperty(PropertyKey.xdevapiTlsVersions.getKeyName(), "FOO,BAR");
        assertThrows(SSLParamsException.class,
                "Specified list of TLS versions only contains non valid TLS protocols. Accepted values are TLSv1.2 and TLSv1.3.+",
                () -> this.fact.getSession(props));

        props.setProperty(PropertyKey.xdevapiTlsVersions.getKeyName(), "FOO,,,BAR");
        assertThrows(SSLParamsException.class,
                "Specified list of TLS versions only contains non valid TLS protocols. Accepted values are TLSv1.2 and TLSv1.3.+",
                () -> this.fact.getSession(props));

        props.remove(PropertyKey.xdevapiTlsVersions.getKeyName());

        // TS.FR.5_2. Create a Connection with the connection property tlsVersions=FOO,TLSv1.1.
        //            Assess that the connection fails with the error message "TLS protocols TLSv1 and TLSv1.1 are not supported. Accepted values are TLSv1.2 and TLSv1.3."
        props.setProperty(PropertyKey.tlsVersions.getKeyName(), "FOO,TLSv1.1");
        assertThrows(CJCommunicationsException.class, ".+TLS protocols TLSv1 and TLSv1.1 are not supported. Accepted values are TLSv1.2 and TLSv1.3.+",
                () -> this.fact.getSession(props));

        props.setProperty(PropertyKey.xdevapiTlsVersions.getKeyName(), "FOO,TLSv1.1");
        assertThrows(SSLParamsException.class, "TLS protocols TLSv1 and TLSv1.1 are not supported. Accepted values are TLSv1.2 and TLSv1.3.+",
                () -> this.fact.getSession(props));
        props.remove(PropertyKey.xdevapiTlsVersions.getKeyName());

        // TS.FR.5_3. Create a Connection with the connection property tlsVersions=TLSv1,TLSv1.1.
        //            Assess that the connection fails with the error message "TLS protocols TLSv1 and TLSv1.1 are not supported. Accepted values are TLSv1.2 and TLSv1.3."
        props.setProperty(PropertyKey.xdevapiTlsVersions.getKeyName(), "TLSv1,TLSv1.1");
        assertThrows(SSLParamsException.class, "TLS protocols TLSv1 and TLSv1.1 are not supported. Accepted values are TLSv1.2 and TLSv1.3.+",
                () -> this.fact.getSession(props));
        props.remove(PropertyKey.xdevapiTlsVersions.getKeyName());

        // TS.FR.6. Create a Connection with the connection property tlsVersions= (empty value).
        //          Assess that the connection fails with the error message "Specified list of TLS versions is empty. Accepted values are TLSv1.2 and TLSv13."
        props.setProperty(PropertyKey.xdevapiTlsVersions.getKeyName(), "");
        assertThrows(SSLParamsException.class, "Specified list of TLS versions is empty. Accepted values are TLSv1.2 and TLSv1.3.+",
                () -> this.fact.getSession(props));

        props.setProperty(PropertyKey.xdevapiTlsVersions.getKeyName(), "   ");
        assertThrows(SSLParamsException.class, "Specified list of TLS versions is empty. Accepted values are TLSv1.2 and TLSv1.3.+",
                () -> this.fact.getSession(props));

        props.setProperty(PropertyKey.xdevapiTlsVersions.getKeyName(), ",,,");
        assertThrows(SSLParamsException.class, "Specified list of TLS versions is empty. Accepted values are TLSv1.2 and TLSv1.3.+",
                () -> this.fact.getSession(props));

        props.setProperty(PropertyKey.xdevapiTlsVersions.getKeyName(), ",  ,,");
        assertThrows(SSLParamsException.class, "Specified list of TLS versions is empty. Accepted values are TLSv1.2 and TLSv1.3.+",
                () -> this.fact.getSession(props));

        props.remove(PropertyKey.xdevapiTlsVersions.getKeyName());

        // TS.FR.7. Create a Connection with the connection property tlsVersions=FOO,TLSv1,TLSv1.1,TLSv1.2.
        //          Assess that the connection is created successfully and it is using TLSv1.2.
        props.setProperty(PropertyKey.tlsVersions.getKeyName(), "FOO,TLSv1,TLSv1.1,TLSv1.2");
        sess = this.fact.getSession(props);
        assertSecureSession(sess);
        assertTlsVersion(sess, "TLSv1.2");
        sess.close();

        props.remove(PropertyKey.tlsVersions.getKeyName());
        props.setProperty(PropertyKey.xdevapiTlsVersions.getKeyName(), "FOO,TLSv1,TLSv1.1,TLSv1.2");
        sess = this.fact.getSession(props);
        assertSecureSession(sess);
        assertTlsVersion(sess, "TLSv1.2");
        sess.close();
        props.remove(PropertyKey.xdevapiTlsVersions.getKeyName());

        // TS.FR.9_1. Create an X DevAPI session with the property tlsVersions=TLSv1,TLSv1.1.
        //            Assess that the operation fails with the error message TLS protocols TLSv1 and TLSv1.1 are not supported. Accepted values are TLSv1.2 and TLSv1.3.
        props.setProperty(PropertyKey.tlsVersions.getKeyName(), "TLSv1,TLSv1.1");
        assertThrows(CJCommunicationsException.class, ".+TLS protocols TLSv1 and TLSv1.1 are not supported. Accepted values are TLSv1.2 and TLSv1.3.+",
                () -> this.fact.getSession(props));

        // TS.FR.9_2. Create an Connection X DevAPI session with the property tlsVersions= (empty value).
        //            Assess that the operation fails with the error message Specified list of TLS versions is empty. Accepted values are TLSv1.2 and TLSv13.
        props.setProperty(PropertyKey.tlsVersions.getKeyName(), "");
        assertThrows(CJCommunicationsException.class, ".+Specified list of TLS versions is empty. Accepted values are TLSv1.2 and TLSv1.3.+",
                () -> this.fact.getSession(props));

        props.setProperty(PropertyKey.tlsVersions.getKeyName(), "   ");
        assertThrows(CJCommunicationsException.class, ".+Specified list of TLS versions is empty. Accepted values are TLSv1.2 and TLSv1.3.+",
                () -> this.fact.getSession(props));

        props.setProperty(PropertyKey.tlsVersions.getKeyName(), ",,,");
        assertThrows(CJCommunicationsException.class, ".+Specified list of TLS versions is empty. Accepted values are TLSv1.2 and TLSv1.3.+",
                () -> this.fact.getSession(props));

        props.setProperty(PropertyKey.tlsVersions.getKeyName(), ",  ,,");
        assertThrows(CJCommunicationsException.class, ".+Specified list of TLS versions is empty. Accepted values are TLSv1.2 and TLSv1.3.+",
                () -> this.fact.getSession(props));
        props.remove(PropertyKey.tlsVersions.getKeyName());

        // TS.FR.10. Create an X DevAPI session with the property tlsVersions=TLSv1.2 and xdevapi.ssl-mode=DISABLED.
        //           Assess that the session is created successfully and it is not using encryption.
        props.setProperty(PropertyKey.tlsVersions.getKeyName(), testTlsVersion);
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        sess = this.fact.getSession(props);
        assertNonSecureSession(sess);
        sess.close();
    }

    /**
     * Tests WL#14835, Align TLS option checking across connectors
     *
     * @throws Exception
     */
    @Test
    public void testTSLConflictingOptions() throws Exception {
        Session testSess = null;

        // FR.1b.1 Create an X DevAPI Session to a MySQL server with the options xdevapi.ssl-mode=DISABLED and xdevapi.fallback-to-system-keystore=false.
        //         Assess that the connection is established successfully and not using encryption.
        testSess = this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, SslMode.DISABLED)
                + makeParam(PropertyKey.xdevapiFallbackToSystemKeyStore, "false"));
        assertNonSecureSession(testSess);
        testSess.close();
        // FR.1c.1 Repeat the FR.1b.* tests using the corresponding JDBC/global options in fallback mode. Assess the same results.
        testSess = this.fact
                .getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.sslMode, SslMode.DISABLED) + makeParam(PropertyKey.fallbackToSystemKeyStore, "false"));
        assertNonSecureSession(testSess);
        testSess.close();

        // FR.1b.2 Create an X DevAPI Session to a MySQL server with the options xdevapi.ssl-mode=DISABLED and xdevapi.fallback-to-system-truststore=false.
        //         Assess that the connection is established successfully and not using encryption.
        testSess = this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, SslMode.DISABLED)
                + makeParam(PropertyKey.xdevapiFallbackToSystemTrustStore, "false"));
        assertNonSecureSession(testSess);
        testSess.close();
        // FR.1c.1 Repeat the FR.1b.* tests using the corresponding JDBC/global options in fallback mode. Assess the same results.
        testSess = this.fact.getSession(
                this.sslFreeBaseUrl + makeParam(PropertyKey.sslMode, SslMode.DISABLED) + makeParam(PropertyKey.fallbackToSystemTrustStore, "false"));
        assertNonSecureSession(testSess);
        testSess.close();

        // FR.1b.3 Create an X DevAPI Session to a MySQL server with the options xdevapi.ssl-mode=DISABLED and xdevapi.ssl-keystore=foo.
        //         Assess that the connection is established successfully and not using encryption.
        testSess = this.fact.getSession(
                this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, SslMode.DISABLED) + makeParam(PropertyKey.xdevapiSslKeyStoreUrl, "foo"));
        assertNonSecureSession(testSess);
        testSess.close();
        // FR.1c.1 Repeat the FR.1b.* tests using the corresponding JDBC/global options in fallback mode. Assess the same results.
        testSess = this.fact.getSession(
                this.sslFreeBaseUrl + makeParam(PropertyKey.sslMode, SslMode.DISABLED) + makeParam(PropertyKey.clientCertificateKeyStoreUrl, "foo"));
        assertNonSecureSession(testSess);
        testSess.close();

        // FR.1b.4 Create an X DevAPI Session to a MySQL server with the options xdevapi.ssl-mode=DISABLED and xdevapi.ssl-keystore-password=foo.
        //         Assess that the connection is established successfully and not using encryption.
        testSess = this.fact.getSession(
                this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, SslMode.DISABLED) + makeParam(PropertyKey.xdevapiSslKeyStorePassword, "foo"));
        assertNonSecureSession(testSess);
        testSess.close();
        // FR.1c.1 Repeat the FR.1b.* tests using the corresponding JDBC/global options in fallback mode. Assess the same results.
        testSess = this.fact.getSession(
                this.sslFreeBaseUrl + makeParam(PropertyKey.sslMode, SslMode.DISABLED) + makeParam(PropertyKey.clientCertificateKeyStorePassword, "foo"));
        assertNonSecureSession(testSess);
        testSess.close();

        // FR.1b.5 Create an X DevAPI Session to a MySQL server with the options xdevapi.ssl-mode=DISABLED and xdevapi.ssl-keystore-type=foo.
        //         Assess that the connection is established successfully and not using encryption.
        testSess = this.fact.getSession(
                this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, SslMode.DISABLED) + makeParam(PropertyKey.xdevapiSslKeyStoreType, "foo"));
        assertNonSecureSession(testSess);
        testSess.close();
        // FR.1c.1 Repeat the FR.1b.* tests using the corresponding JDBC/global options in fallback mode. Assess the same results.
        testSess = this.fact.getSession(
                this.sslFreeBaseUrl + makeParam(PropertyKey.sslMode, SslMode.DISABLED) + makeParam(PropertyKey.clientCertificateKeyStoreType, "foo"));
        assertNonSecureSession(testSess);
        testSess.close();

        // FR.1b.6 Create an X DevAPI Session to a MySQL server with the options xdevapi.ssl-mode=DISABLED and xdevapi.ssl-truststore=foo.
        //         Assess that the connection is established successfully and not using encryption.
        testSess = this.fact.getSession(
                this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, SslMode.DISABLED) + makeParam(PropertyKey.xdevapiSslTrustStoreUrl, "foo"));
        assertNonSecureSession(testSess);
        testSess.close();
        // FR.1c.1 Repeat the FR.1b.* tests using the corresponding JDBC/global options in fallback mode. Assess the same results.
        testSess = this.fact
                .getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.sslMode, SslMode.DISABLED) + makeParam(PropertyKey.trustCertificateKeyStoreUrl, "foo"));
        assertNonSecureSession(testSess);
        testSess.close();

        // FR.1b.7 Create an X DevAPI Session to a MySQL server with the options xdevapi.ssl-mode=DISABLED and xdevapi.ssl-truststore-password=foo.
        //         Assess that the connection is established successfully and not using encryption.
        testSess = this.fact.getSession(
                this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, SslMode.DISABLED) + makeParam(PropertyKey.xdevapiSslTrustStorePassword, "foo"));
        assertNonSecureSession(testSess);
        testSess.close();
        // FR.1c.1 Repeat the FR.1b.* tests using the corresponding JDBC/global options in fallback mode. Assess the same results.
        testSess = this.fact.getSession(
                this.sslFreeBaseUrl + makeParam(PropertyKey.sslMode, SslMode.DISABLED) + makeParam(PropertyKey.trustCertificateKeyStorePassword, "foo"));
        assertNonSecureSession(testSess);
        testSess.close();

        // FR.1b.8 Create an X DevAPI Session to a MySQL server with the options xdevapi.ssl-mode=DISABLED and xdevapi.ssl-truststore-type=foo.
        //         Assess that the connection is established successfully and not using encryption.
        testSess = this.fact.getSession(
                this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, SslMode.DISABLED) + makeParam(PropertyKey.xdevapiSslTrustStoreType, "foo"));
        assertNonSecureSession(testSess);
        testSess.close();
        // FR.1c.1 Repeat the FR.1b.* tests using the corresponding JDBC/global options in fallback mode. Assess the same results.
        testSess = this.fact.getSession(
                this.sslFreeBaseUrl + makeParam(PropertyKey.sslMode, SslMode.DISABLED) + makeParam(PropertyKey.trustCertificateKeyStoreType, "foo"));
        assertNonSecureSession(testSess);
        testSess.close();

        // FR.1b.9 Create an X DevAPI Session to a MySQL server with the options xdevapi.ssl-mode=DISABLED and xdevapi.tls-ciphersuites=foo.
        //         Assess that the connection is established successfully and not using encryption.
        testSess = this.fact.getSession(
                this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, SslMode.DISABLED) + makeParam(PropertyKey.xdevapiTlsCiphersuites, "foo"));
        assertNonSecureSession(testSess);
        testSess.close();
        // FR.1c.1 Repeat the FR.1b.* tests using the corresponding JDBC/global options in fallback mode. Assess the same results.
        testSess = this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.sslMode, SslMode.DISABLED) + makeParam(PropertyKey.tlsCiphersuites, "foo"));
        assertNonSecureSession(testSess);
        testSess.close();

        // FR.1b.10 Create an X DevAPI Session to a MySQL server with the options xdevapi.ssl-mode=DISABLED and xdevapi.tls-versions=false.
        //          Assess that the connection is established successfully and not using encryption.
        testSess = this.fact
                .getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, SslMode.DISABLED) + makeParam(PropertyKey.xdevapiTlsVersions, "foo"));
        assertNonSecureSession(testSess);
        testSess.close();
        // FR.1c.1 Repeat the FR.1b.* tests using the corresponding JDBC/global options in fallback mode. Assess the same results.
        testSess = this.fact.getSession(this.sslFreeBaseUrl + makeParam(PropertyKey.sslMode, SslMode.DISABLED) + makeParam(PropertyKey.tlsVersions, "foo"));
        assertNonSecureSession(testSess);
        testSess.close();

        // FR.2.3 Create an X DevAPI connection to a MySQL server with the options xdevapi.ssl-mode=DISABLED and xdevapi.ssl-mode=REQUIRED by this order.
        //        Assess that the connection is established successfully. It is acceptable that the connection is established over a secure or non secure
        //        channel, but it is usually expected that the next test delivers the opposite result.
        testSess = this.fact.getSession(
                this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, SslMode.DISABLED) + makeParam(PropertyKey.xdevapiSslMode, SslMode.REQUIRED));
        assertTrue(testSess.getUri().contains(makeParam(PropertyKey.sslMode, SslMode.REQUIRED).substring(1))); // Last one remains and replaced by global prop.
        assertSecureSession(testSess);
        testSess.close();

        // FR.2.4 Create an X DevAPI connection to a MySQL server with the options xdevapi.ssl-mode=REQUIRED and xdevapi.ssl-mode=DISABLED by this order.
        //        Assess that the connection is established successfully. It is acceptable that the connection is established over a secure or non secure
        //        channel, but it is usually expected that the previous test delivers the opposite result.
        testSess = this.fact.getSession(
                this.sslFreeBaseUrl + makeParam(PropertyKey.xdevapiSslMode, SslMode.REQUIRED) + makeParam(PropertyKey.xdevapiSslMode, SslMode.DISABLED));
        assertTrue(testSess.getUri().contains(makeParam(PropertyKey.sslMode, SslMode.DISABLED).substring(1))); // Last one remains and replaced by global prop.
        assertNonSecureSession(testSess);
        testSess.close();
    }

}
