Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-sqlite3-to-mysql for 
openSUSE:Factory checked in at 2026-05-20 15:24:48
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-sqlite3-to-mysql (Old)
 and      /work/SRC/openSUSE:Factory/.python-sqlite3-to-mysql.new.1966 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-sqlite3-to-mysql"

Wed May 20 15:24:48 2026 rev:9 rq:1354113 version:2.6.0

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-sqlite3-to-mysql/python-sqlite3-to-mysql.changes
  2026-04-26 21:14:13.560561654 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-sqlite3-to-mysql.new.1966/python-sqlite3-to-mysql.changes
        2026-05-20 15:25:52.809499008 +0200
@@ -1,0 +2,26 @@
+Tue May 19 21:22:14 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 2.6.0:
+  * [FEAT] add MySQL SSL certificate options `--mysql-ssl-ca`,
+    `--mysql-ssl-cert`, and `--mysql-ssl-key`,
+  * forward them to MySQL Connector/Python, and enable
+    certificate-chain verification when a CA is provided
+  * [FIX] validate MySQL SSL option combinations in both the CLI
+    and transporter, including conflicts with
+  * `--skip-ssl` and `--mysql-socket` plus the client cert/key
+    pairing requirement
+  * [FIX] normalize and validate MySQL SSL certificate paths for
+    direct transporter callers, including `PathLike`
+  * values and `~` home-directory paths
+  * [CHORE] update README and Sphinx docs for the MySQL SSL
+    options, socket conflicts, cert/key behavior, and
+  * certificate verification semantics
+  * [CHORE] add unit and functional SSL coverage, including
+    Docker-generated CA/client certificate tests and
+  * configured certificate paths for CI
+  * [CHORE] harden Docker test fixtures by closing Docker
+    clients, retrying transient MySQL startup errors, handling
+  * Docker API errors, cleaning partial SSL cert extraction, and
+    fixing the MySQL container port binding
+
+-------------------------------------------------------------------

Old:
----
  sqlite3_to_mysql-2.5.8.tar.gz

New:
----
  sqlite3_to_mysql-2.6.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-sqlite3-to-mysql.spec ++++++
--- /var/tmp/diff_new_pack.0AB4qm/_old  2026-05-20 15:25:54.389564115 +0200
+++ /var/tmp/diff_new_pack.0AB4qm/_new  2026-05-20 15:25:54.393564280 +0200
@@ -22,7 +22,7 @@
 %bcond_with libalternatives
 %endif
 Name:           python-sqlite3-to-mysql
-Version:        2.5.8
+Version:        2.6.0
 Release:        0
 Summary:        A Python tool to transfer data from SQLite 3 to MySQL
 License:        MIT

++++++ sqlite3_to_mysql-2.5.8.tar.gz -> sqlite3_to_mysql-2.6.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sqlite3_to_mysql-2.5.8/CHANGELOG.md 
new/sqlite3_to_mysql-2.6.0/CHANGELOG.md
--- old/sqlite3_to_mysql-2.5.8/CHANGELOG.md     2020-02-02 01:00:00.000000000 
+0100
+++ new/sqlite3_to_mysql-2.6.0/CHANGELOG.md     2020-02-02 01:00:00.000000000 
+0100
@@ -1,3 +1,18 @@
+# 2.6.0
+
+* [FEAT] add MySQL SSL certificate options `--mysql-ssl-ca`, 
`--mysql-ssl-cert`, and `--mysql-ssl-key`,
+  forward them to MySQL Connector/Python, and enable certificate-chain 
verification when a CA is provided
+* [FIX] validate MySQL SSL option combinations in both the CLI and 
transporter, including conflicts with
+  `--skip-ssl` and `--mysql-socket` plus the client cert/key pairing 
requirement
+* [FIX] normalize and validate MySQL SSL certificate paths for direct 
transporter callers, including `PathLike`
+  values and `~` home-directory paths
+* [CHORE] update README and Sphinx docs for the MySQL SSL options, socket 
conflicts, cert/key behavior, and
+  certificate verification semantics
+* [CHORE] add unit and functional SSL coverage, including Docker-generated 
CA/client certificate tests and
+  configured certificate paths for CI
+* [CHORE] harden Docker test fixtures by closing Docker clients, retrying 
transient MySQL startup errors, handling
+  Docker API errors, cleaning partial SSL cert extraction, and fixing the 
MySQL container port binding
+
 # 2.5.8
 
 * [CHORE] pin sqlglot to >=30.0.0,<31.0.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sqlite3_to_mysql-2.5.8/PKG-INFO 
new/sqlite3_to_mysql-2.6.0/PKG-INFO
--- old/sqlite3_to_mysql-2.5.8/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
+++ new/sqlite3_to_mysql-2.6.0/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: sqlite3-to-mysql
-Version: 2.5.8
+Version: 2.6.0
 Summary: A simple Python tool to transfer data from SQLite 3 to MySQL
 Project-URL: Homepage, https://techouse.github.io/sqlite3-to-mysql/
 Project-URL: Documentation, https://techouse.github.io/sqlite3-to-mysql/
@@ -128,8 +128,19 @@
   --mysql-password TEXT           MySQL password
   -h, --mysql-host TEXT           MySQL host. Defaults to localhost.
   -P, --mysql-port INTEGER        MySQL port. Defaults to 3306.
-  -k, --mysql-socket PATH         Path to MySQL unix socket file.
-  -S, --skip-ssl                  Disable MySQL connection encryption.
+  -k, --mysql-socket PATH         Path to MySQL unix socket file. Cannot be
+                                  used with --mysql-ssl-* options.
+  --mysql-ssl-ca PATH             Path to SSL CA certificate file. Cannot be
+                                  used with --mysql-socket or --skip-ssl.
+  --mysql-ssl-cert PATH           Path to SSL certificate file. Must be
+                                  provided together with --mysql-ssl-key.
+                                  Cannot be used with --mysql-socket or
+                                  --skip-ssl.
+  --mysql-ssl-key PATH            Path to SSL key file. Must be provided
+                                  together with --mysql-ssl-cert. Cannot be
+                                  used with --mysql-socket or --skip-ssl.
+  -S, --skip-ssl                  Disable MySQL connection encryption. Cannot
+                                  be used with --mysql-ssl-* options.
   -i, --mysql-insert-method [DEFAULT|IGNORE|UPDATE]
                                   MySQL insert method. DEFAULT will throw
                                   errors when encountering duplicate records;
@@ -162,6 +173,11 @@
   --help                          Show this message and exit.
 ```
 
+MySQL SSL note: when `--mysql-ssl-ca` is provided, MySQL Connector/Python 
verifies the server
+certificate chain. `--mysql-ssl-cert` and `--mysql-ssl-key` enable client 
certificate authentication.
+These options do not enable hostname identity verification. If you provide 
only the client certificate
+and key without `--mysql-ssl-ca`, the server certificate is not verified.
+
 #### Docker
 
 If you don't want to install the tool on your system, you can use the Docker 
image instead.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sqlite3_to_mysql-2.5.8/README.md 
new/sqlite3_to_mysql-2.6.0/README.md
--- old/sqlite3_to_mysql-2.5.8/README.md        2020-02-02 01:00:00.000000000 
+0100
+++ new/sqlite3_to_mysql-2.6.0/README.md        2020-02-02 01:00:00.000000000 
+0100
@@ -59,8 +59,19 @@
   --mysql-password TEXT           MySQL password
   -h, --mysql-host TEXT           MySQL host. Defaults to localhost.
   -P, --mysql-port INTEGER        MySQL port. Defaults to 3306.
-  -k, --mysql-socket PATH         Path to MySQL unix socket file.
-  -S, --skip-ssl                  Disable MySQL connection encryption.
+  -k, --mysql-socket PATH         Path to MySQL unix socket file. Cannot be
+                                  used with --mysql-ssl-* options.
+  --mysql-ssl-ca PATH             Path to SSL CA certificate file. Cannot be
+                                  used with --mysql-socket or --skip-ssl.
+  --mysql-ssl-cert PATH           Path to SSL certificate file. Must be
+                                  provided together with --mysql-ssl-key.
+                                  Cannot be used with --mysql-socket or
+                                  --skip-ssl.
+  --mysql-ssl-key PATH            Path to SSL key file. Must be provided
+                                  together with --mysql-ssl-cert. Cannot be
+                                  used with --mysql-socket or --skip-ssl.
+  -S, --skip-ssl                  Disable MySQL connection encryption. Cannot
+                                  be used with --mysql-ssl-* options.
   -i, --mysql-insert-method [DEFAULT|IGNORE|UPDATE]
                                   MySQL insert method. DEFAULT will throw
                                   errors when encountering duplicate records;
@@ -93,6 +104,11 @@
   --help                          Show this message and exit.
 ```
 
+MySQL SSL note: when `--mysql-ssl-ca` is provided, MySQL Connector/Python 
verifies the server
+certificate chain. `--mysql-ssl-cert` and `--mysql-ssl-key` enable client 
certificate authentication.
+These options do not enable hostname identity verification. If you provide 
only the client certificate
+and key without `--mysql-ssl-ca`, the server certificate is not verified.
+
 #### Docker
 
 If you don't want to install the tool on your system, you can use the Docker 
image instead.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/sqlite3_to_mysql-2.5.8/src/sqlite3_to_mysql/__init__.py 
new/sqlite3_to_mysql-2.6.0/src/sqlite3_to_mysql/__init__.py
--- old/sqlite3_to_mysql-2.5.8/src/sqlite3_to_mysql/__init__.py 2020-02-02 
01:00:00.000000000 +0100
+++ new/sqlite3_to_mysql-2.6.0/src/sqlite3_to_mysql/__init__.py 2020-02-02 
01:00:00.000000000 +0100
@@ -1,5 +1,5 @@
 """Utility to transfer data from SQLite 3 to MySQL."""
 
-__version__ = "2.5.8"
+__version__ = "2.6.0"
 
 from .transporter import SQLite3toMySQL
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sqlite3_to_mysql-2.5.8/src/sqlite3_to_mysql/cli.py 
new/sqlite3_to_mysql-2.6.0/src/sqlite3_to_mysql/cli.py
--- old/sqlite3_to_mysql-2.5.8/src/sqlite3_to_mysql/cli.py      2020-02-02 
01:00:00.000000000 +0100
+++ new/sqlite3_to_mysql-2.6.0/src/sqlite3_to_mysql/cli.py      2020-02-02 
01:00:00.000000000 +0100
@@ -80,11 +80,39 @@
 @click.option(
     "-k",
     "--mysql-socket",
-    type=click.Path(exists=True),
+    type=click.Path(exists=True, dir_okay=False, file_okay=True),
     default=None,
-    help="Path to MySQL unix socket file.",
+    help="Path to MySQL unix socket file. Cannot be used with --mysql-ssl-* 
options.",
+)
[email protected](
+    "--mysql-ssl-ca",
+    type=click.Path(exists=True, dir_okay=False, file_okay=True, 
readable=True),
+    metavar="PATH",
+    default=None,
+    help="Path to SSL CA certificate file. Cannot be used with --mysql-socket 
or --skip-ssl.",
+)
[email protected](
+    "--mysql-ssl-cert",
+    type=click.Path(exists=True, dir_okay=False, file_okay=True, 
readable=True),
+    metavar="PATH",
+    default=None,
+    help="Path to SSL certificate file. Must be provided together with 
--mysql-ssl-key. "
+    "Cannot be used with --mysql-socket or --skip-ssl.",
+)
[email protected](
+    "--mysql-ssl-key",
+    type=click.Path(exists=True, dir_okay=False, file_okay=True, 
readable=True),
+    metavar="PATH",
+    default=None,
+    help="Path to SSL key file. Must be provided together with 
--mysql-ssl-cert. "
+    "Cannot be used with --mysql-socket or --skip-ssl.",
+)
[email protected](
+    "-S",
+    "--skip-ssl",
+    is_flag=True,
+    help="Disable MySQL connection encryption. Cannot be used with 
--mysql-ssl-* options.",
 )
[email protected]("-S", "--skip-ssl", is_flag=True, help="Disable MySQL connection 
encryption.")
 @click.option(
     "-i",
     "--mysql-insert-method",
@@ -162,6 +190,9 @@
     mysql_host: str,
     mysql_port: int,
     mysql_socket: t.Optional[str],
+    mysql_ssl_ca: t.Optional[str],
+    mysql_ssl_cert: t.Optional[str],
+    mysql_ssl_key: t.Optional[str],
     skip_ssl: bool,
     mysql_insert_method: str,
     mysql_truncate_tables: bool,
@@ -205,6 +236,19 @@
                 "Please use only one of them."
             )
 
+        if skip_ssl and any((mysql_ssl_ca, mysql_ssl_cert, mysql_ssl_key)):
+            raise click.ClickException(
+                "--skip-ssl and 
--mysql-ssl-ca/--mysql-ssl-cert/--mysql-ssl-key are mutually exclusive."
+            )
+
+        if mysql_socket and any((mysql_ssl_ca, mysql_ssl_cert, mysql_ssl_key)):
+            raise click.ClickException(
+                "--mysql-socket and 
--mysql-ssl-ca/--mysql-ssl-cert/--mysql-ssl-key are mutually exclusive."
+            )
+
+        if bool(mysql_ssl_cert) != bool(mysql_ssl_key):
+            raise click.ClickException("--mysql-ssl-cert and --mysql-ssl-key 
must be provided together.")
+
         SQLite3toMySQL(
             sqlite_file=sqlite_file,
             sqlite_tables=sqlite_tables or tuple(),
@@ -217,6 +261,9 @@
             mysql_host=mysql_host,
             mysql_port=None if mysql_socket else mysql_port,
             mysql_socket=mysql_socket,
+            mysql_ssl_ca=mysql_ssl_ca,
+            mysql_ssl_cert=mysql_ssl_cert,
+            mysql_ssl_key=mysql_ssl_key,
             mysql_ssl_disabled=skip_ssl,
             mysql_insert_method=mysql_insert_method,
             mysql_truncate_tables=mysql_truncate_tables,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/sqlite3_to_mysql-2.5.8/src/sqlite3_to_mysql/transporter.py 
new/sqlite3_to_mysql-2.6.0/src/sqlite3_to_mysql/transporter.py
--- old/sqlite3_to_mysql-2.5.8/src/sqlite3_to_mysql/transporter.py      
2020-02-02 01:00:00.000000000 +0100
+++ new/sqlite3_to_mysql-2.6.0/src/sqlite3_to_mysql/transporter.py      
2020-02-02 01:00:00.000000000 +0100
@@ -10,6 +10,7 @@
 from itertools import chain
 from math import ceil
 from os.path import isfile, realpath
+from pathlib import Path
 from sys import stdout
 
 import mysql.connector
@@ -92,6 +93,22 @@
 
     MYSQL_CONNECTOR_VERSION: version.Version = 
version.parse(mysql_connector_version_string)
 
+    @staticmethod
+    def _normalize_mysql_ssl_file(
+        path: t.Optional[t.Union[str, "os.PathLike[t.Any]"]],
+        description: str,
+    ) -> t.Optional[str]:
+        """Validate and normalize a MySQL SSL certificate path."""
+        if not path:
+            return None
+
+        resolved_path = Path(path).expanduser().resolve()
+        if not resolved_path.is_file():
+            raise FileNotFoundError(f"{description} does not exist")
+        if not os.access(resolved_path, os.R_OK):
+            raise PermissionError(f"{description} is not readable")
+        return str(resolved_path)
+
     def __init__(self, **kwargs: Unpack[SQLite3toMySQLParams]):
         """Constructor."""
         if kwargs.get("sqlite_file") is None:
@@ -138,8 +155,24 @@
         else:
             self._without_foreign_keys = 
bool(kwargs.get("without_foreign_keys", False))
 
+        mysql_ssl_ca = kwargs.get("mysql_ssl_ca") or None
+        mysql_ssl_cert = kwargs.get("mysql_ssl_cert") or None
+        mysql_ssl_key = kwargs.get("mysql_ssl_key") or None
         self._mysql_ssl_disabled = bool(kwargs.get("mysql_ssl_disabled", 
False))
 
+        if self._mysql_ssl_disabled and any((mysql_ssl_ca, mysql_ssl_cert, 
mysql_ssl_key)):
+            raise ValueError("Cannot use SSL certificate options when SSL is 
disabled")
+
+        if self._mysql_socket and any((mysql_ssl_ca, mysql_ssl_cert, 
mysql_ssl_key)):
+            raise ValueError("Cannot use SSL certificate options when 
connecting through a MySQL socket")
+
+        if bool(mysql_ssl_cert) != bool(mysql_ssl_key):
+            raise ValueError("mysql_ssl_cert and mysql_ssl_key must be 
provided together")
+
+        self._mysql_ssl_ca = self._normalize_mysql_ssl_file(mysql_ssl_ca, 
"MySQL SSL CA certificate file")
+        self._mysql_ssl_cert = self._normalize_mysql_ssl_file(mysql_ssl_cert, 
"MySQL SSL certificate file")
+        self._mysql_ssl_key = self._normalize_mysql_ssl_file(mysql_ssl_key, 
"MySQL SSL key file")
+
         # Expect an integer chunk size; normalize to None when unset/invalid 
or <= 0
         _chunk = kwargs.get("chunk")
         self._chunk_size = _chunk if isinstance(_chunk, int) and _chunk > 0 
else None
@@ -213,6 +246,10 @@
         else:
             connection_args["host"] = self._mysql_host
             connection_args["port"] = self._mysql_port
+            connection_args["ssl_ca"] = self._mysql_ssl_ca
+            connection_args["ssl_cert"] = self._mysql_ssl_cert
+            connection_args["ssl_key"] = self._mysql_ssl_key
+            connection_args["ssl_verify_cert"] = self._mysql_ssl_ca is not None
             if self._mysql_ssl_disabled:
                 connection_args["ssl_disabled"] = True
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sqlite3_to_mysql-2.5.8/src/sqlite3_to_mysql/types.py 
new/sqlite3_to_mysql-2.6.0/src/sqlite3_to_mysql/types.py
--- old/sqlite3_to_mysql-2.5.8/src/sqlite3_to_mysql/types.py    2020-02-02 
01:00:00.000000000 +0100
+++ new/sqlite3_to_mysql-2.6.0/src/sqlite3_to_mysql/types.py    2020-02-02 
01:00:00.000000000 +0100
@@ -29,6 +29,9 @@
     mysql_host: t.Optional[str]
     mysql_port: t.Optional[int]
     mysql_socket: t.Optional[t.Union[str, "os.PathLike[t.Any]"]]
+    mysql_ssl_ca: t.Optional[t.Union[str, "os.PathLike[t.Any]"]]
+    mysql_ssl_cert: t.Optional[t.Union[str, "os.PathLike[t.Any]"]]
+    mysql_ssl_key: t.Optional[t.Union[str, "os.PathLike[t.Any]"]]
     mysql_ssl_disabled: t.Optional[bool]
     chunk: t.Optional[int]
     quiet: t.Optional[bool]
@@ -61,6 +64,9 @@
     _mysql_host: t.Optional[str]
     _mysql_port: t.Optional[int]
     _mysql_socket: t.Optional[t.Union[str, "os.PathLike[t.Any]"]]
+    _mysql_ssl_ca: t.Optional[str]
+    _mysql_ssl_cert: t.Optional[str]
+    _mysql_ssl_key: t.Optional[str]
     _mysql_ssl_disabled: bool
     _chunk_size: t.Optional[int]
     _quiet: bool
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sqlite3_to_mysql-2.5.8/tests/conftest.py 
new/sqlite3_to_mysql-2.6.0/tests/conftest.py
--- old/sqlite3_to_mysql-2.5.8/tests/conftest.py        2020-02-02 
01:00:00.000000000 +0100
+++ new/sqlite3_to_mysql-2.6.0/tests/conftest.py        2020-02-02 
01:00:00.000000000 +0100
@@ -1,5 +1,8 @@
+import io
 import json
+import os
 import socket
+import tarfile
 import typing as t
 from codecs import open
 from contextlib import contextmanager
@@ -16,7 +19,7 @@
 from _pytest.legacypath import TempdirFactory
 from click.testing import CliRunner
 from docker import DockerClient
-from docker.errors import NotFound
+from docker.errors import APIError, NotFound
 from docker.models.containers import Container
 from faker import Faker
 from mysql.connector import MySQLConnection, errorcode
@@ -103,21 +106,28 @@
 def cleanup_hanged_docker_containers() -> None:
     try:
         client: DockerClient = docker.from_env()
-        for container in client.containers.list():
-            if container.name == "pytest_sqlite3_to_mysql":
-                container.kill()
-                break
+        try:
+            for container in client.containers.list():
+                if container.name == "pytest_sqlite3_to_mysql":
+                    container.kill()
+                    break
+        finally:
+            client.close()
     except Exception:
         pass
 
 
-def pytest_keyboard_interrupt() -> None:
+def pytest_keyboard_interrupt(excinfo: t.Any) -> None:
+    del excinfo
     try:
         client: DockerClient = docker.from_env()
-        for container in client.containers.list():
-            if container.name == "pytest_sqlite3_to_mysql":
-                container.kill()
-                break
+        try:
+            for container in client.containers.list():
+                if container.name == "pytest_sqlite3_to_mysql":
+                    container.kill()
+                    break
+        finally:
+            client.close()
     except Exception:
         pass
 
@@ -239,6 +249,7 @@
 
 @pytest.fixture(scope="session")
 def mysql_instance(mysql_credentials: MySQLCredentials, pytestconfig: Config) 
-> t.Iterator[MySQLConnection]:
+    client: t.Optional[DockerClient] = None
     container: t.Optional[Container] = None
     mysql_connection: t.Optional[t.Union[PooledMySQLConnection, 
MySQLConnection, CMySQLConnection]] = None
     mysql_available: bool = False
@@ -250,76 +261,221 @@
     else:
         use_docker = pytestconfig.getoption("use_docker")
 
-    if use_docker:
-        """Connecting to a MySQL server within a Docker container is quite 
tricky :P
-        Read more on the issue here 
https://hub.docker.com/_/mysql#no-connections-until-mysql-init-completes
-        """
-        try:
-            client = docker.from_env()
-        except Exception as err:
-            pytest.fail(str(err))
-
-        docker_mysql_image = pytestconfig.getoption("docker_mysql_image") or 
"mysql:latest"
-
-        if not any(docker_mysql_image in image.tags for image in 
client.images.list()):
-            print(f"Attempting to download Docker image {docker_mysql_image}'")
+    try:
+        if use_docker:
+            """Connecting to a MySQL server within a Docker container is quite 
tricky :P
+            Read more on the issue here 
https://hub.docker.com/_/mysql#no-connections-until-mysql-init-completes
+            """
             try:
-                client.images.pull(docker_mysql_image)
-            except (HTTPError, NotFound) as err:
+                client = docker.from_env()
+            except Exception as err:
                 pytest.fail(str(err))
 
-        container = client.containers.run(
-            image=docker_mysql_image,
-            name="pytest_sqlite3_to_mysql",
-            ports={
-                "3306/tcp": (
-                    mysql_credentials.host,
-                    f"{mysql_credentials.port}/tcp",
-                )
-            },
-            environment={
-                "MYSQL_RANDOM_ROOT_PASSWORD": "yes",
-                "MYSQL_USER": mysql_credentials.user,
-                "MYSQL_PASSWORD": mysql_credentials.password,
-                "MYSQL_DATABASE": mysql_credentials.database,
-            },
-            command=[
-                "--character-set-server=utf8mb4",
-                "--collation-server=utf8mb4_unicode_ci",
-            ],
-            detach=True,
-            auto_remove=True,
-        )
+            docker_mysql_image = pytestconfig.getoption("docker_mysql_image") 
or "mysql:latest"
 
-    while not mysql_available and mysql_connection_retries > 0:
-        try:
-            mysql_connection = mysql.connector.connect(
-                user=mysql_credentials.user,
-                password=mysql_credentials.password,
-                host=mysql_credentials.host,
-                port=mysql_credentials.port,
-                charset="utf8mb4",
-                collation="utf8mb4_unicode_ci",
+            if not any(docker_mysql_image in image.tags for image in 
client.images.list()):
+                print(f"Attempting to download Docker image 
{docker_mysql_image}")
+                try:
+                    client.images.pull(docker_mysql_image)
+                except (APIError, HTTPError, NotFound) as err:
+                    pytest.fail(str(err))
+
+            container = client.containers.run(
+                image=docker_mysql_image,
+                name="pytest_sqlite3_to_mysql",
+                ports={
+                    "3306/tcp": (
+                        mysql_credentials.host,
+                        mysql_credentials.port,
+                    )
+                },
+                environment={
+                    "MYSQL_RANDOM_ROOT_PASSWORD": "yes",
+                    "MYSQL_USER": mysql_credentials.user,
+                    "MYSQL_PASSWORD": mysql_credentials.password,
+                    "MYSQL_DATABASE": mysql_credentials.database,
+                },
+                command=[
+                    "--character-set-server=utf8mb4",
+                    "--collation-server=utf8mb4_unicode_ci",
+                ],
+                detach=True,
+                auto_remove=True,
             )
-        except mysql.connector.Error as err:
-            if err.errno == errorcode.CR_SERVER_LOST:
-                # sleep for two seconds and retry the connection
-                sleep(2)
-            else:
-                raise
-        finally:
-            mysql_connection_retries -= 1
-            if mysql_connection and mysql_connection.is_connected():
-                mysql_available = True
-                mysql_connection.close()
-    else:
+
+        while not mysql_available and mysql_connection_retries > 0:
+            try:
+                mysql_connection = mysql.connector.connect(
+                    user=mysql_credentials.user,
+                    password=mysql_credentials.password,
+                    host=mysql_credentials.host,
+                    port=mysql_credentials.port,
+                    charset="utf8mb4",
+                    collation="utf8mb4_unicode_ci",
+                )
+            except mysql.connector.Error as err:
+                if err.errno in (errorcode.CR_SERVER_LOST, 
errorcode.CR_CONN_HOST_ERROR):
+                    # sleep for two seconds and retry the connection
+                    sleep(2)
+                else:
+                    raise
+            finally:
+                mysql_connection_retries -= 1
+                if mysql_connection and mysql_connection.is_connected():
+                    mysql_available = True
+                    mysql_connection.close()
         if not mysql_available and mysql_connection_retries <= 0:
             raise ConnectionAbortedError("Maximum MySQL connection retries 
exhausted! Are you sure MySQL is running?")
 
-    yield  # type: ignore[misc]
+        yield  # type: ignore[misc]
+    finally:
+        if use_docker:
+            try:
+                if container is not None:
+                    try:
+                        container.kill()
+                    except (APIError, NotFound):
+                        pass
+            finally:
+                if client is not None:
+                    client.close()
+
+
+class MySQLSSLCerts(t.NamedTuple):
+    """Paths to MySQL SSL certificate files for functional tests."""
+
+    ca: str
+    client_cert: str
+    client_key: str
+
+
+def _mysql_ssl_certs_from_paths(
+    ca: t.Union[str, Path],
+    client_cert: t.Union[str, Path],
+    client_key: t.Union[str, Path],
+    source: str,
+) -> MySQLSSLCerts:
+    certs = MySQLSSLCerts(
+        ca=str(Path(ca).expanduser().resolve()),
+        client_cert=str(Path(client_cert).expanduser().resolve()),
+        client_key=str(Path(client_key).expanduser().resolve()),
+    )
+    cert_paths = (Path(certs.ca), Path(certs.client_cert), 
Path(certs.client_key))
+    missing_paths = [str(cert_path) for cert_path in cert_paths if not 
cert_path.is_file()]
+    if missing_paths:
+        pytest.fail(f"{source} MySQL SSL cert file does not exist: {', 
'.join(missing_paths)}")
+
+    unreadable_paths = [str(cert_path) for cert_path in cert_paths if not 
os.access(cert_path, os.R_OK)]
+    if unreadable_paths:
+        pytest.fail(f"{source} MySQL SSL cert file is not readable: {', 
'.join(unreadable_paths)}")
+
+    return certs
+
+
+def _mysql_ssl_certs_from_environment() -> t.Optional[MySQLSSLCerts]:
+    ca = os.environ.get("MYSQL_SSL_CA")
+    client_cert = os.environ.get("MYSQL_SSL_CERT")
+    client_key = os.environ.get("MYSQL_SSL_KEY")
+    if not any((ca, client_cert, client_key)):
+        return None
+
+    if not all((ca, client_cert, client_key)):
+        pytest.fail("MYSQL_SSL_CA, MYSQL_SSL_CERT, and MYSQL_SSL_KEY must be 
set together")
+
+    assert ca is not None
+    assert client_cert is not None
+    assert client_key is not None
+    return _mysql_ssl_certs_from_paths(ca, client_cert, client_key, 
"Configured")
+
+
+def _mysql_ssl_certs_from_home() -> t.Optional[MySQLSSLCerts]:
+    home = Path.home()
+    ca = home / "ca.pem"
+    client_cert = home / "client-cert.pem"
+    client_key = home / "client-key.pem"
+    if not all(cert_path.is_file() for cert_path in (ca, client_cert, 
client_key)):
+        return None
+
+    return _mysql_ssl_certs_from_paths(ca, client_cert, client_key, "Home 
directory")
+
+
[email protected](scope="session")
+def mysql_ssl_certs(
+    mysql_instance: MySQLConnection,
+    pytestconfig: Config,
+    tmp_path_factory: pytest.TempPathFactory,
+) -> t.Optional[MySQLSSLCerts]:
+    # This dependency starts MySQL before we collect matching SSL certs.
+    del mysql_instance
+    configured_ssl_certs = _mysql_ssl_certs_from_environment() or 
_mysql_ssl_certs_from_home()
+    if configured_ssl_certs is not None:
+        return configured_ssl_certs
+
+    db_credentials_file = abspath(join(dirname(__file__), 
"db_credentials.json"))
+    if isfile(db_credentials_file):
+        pytest.skip(
+            "SSL cert paths are required when using tests/db_credentials.json; 
set MYSQL_SSL_CA, MYSQL_SSL_CERT, "
+            "and MYSQL_SSL_KEY or copy ca.pem, client-cert.pem, and 
client-key.pem to $HOME"
+        )
+
+    if not pytestconfig.getoption("use_docker"):
+        pytest.skip("SSL cert extraction requires Docker or configured SSL 
cert paths")
 
-    if use_docker and container is not None:
-        container.kill()
+    client: DockerClient = docker.from_env()
+    try:
+        try:
+            container: Container = 
client.containers.get("pytest_sqlite3_to_mysql")
+        except NotFound:
+            pytest.fail("MySQL test container is running, but SSL cert 
extraction could not find it")
+
+        ssl_dir = tmp_path_factory.mktemp("mysql_ssl_certs")
+        cert_files = {
+            "ca.pem": "ca.pem",
+            "client-cert.pem": "client-cert.pem",
+            "client-key.pem": "client-key.pem",
+        }
+
+        extracted: t.Dict[str, str] = {}
+        written_paths: t.List[Path] = []
+        try:
+            for filename, dest_name in cert_files.items():
+                data_stream, _stat = 
container.get_archive(f"/var/lib/mysql/{filename}")
+
+                buf = io.BytesIO()
+                for chunk in data_stream:
+                    buf.write(chunk)
+                buf.seek(0)
+                with tarfile.open(fileobj=buf) as tar:
+                    member = next(
+                        (tar_member for tar_member in tar.getmembers() if 
Path(tar_member.name).name == filename),
+                        None,
+                    )
+                    if member is None:
+                        raise RuntimeError(f"Docker returned an archive for 
{filename}, but the file was not present")
+
+                    fobj = tar.extractfile(member)
+                    if fobj is None:
+                        raise RuntimeError(f"Could not read {filename} from 
the Docker archive")
+
+                    with fobj:
+                        dest_path = ssl_dir / dest_name
+                        dest_path.write_bytes(fobj.read())
+                        written_paths.append(dest_path)
+                        if dest_name == "client-key.pem":
+                            dest_path.chmod(0o600)
+                        extracted[filename] = str(dest_path)
+        except (APIError, NotFound, OSError, RuntimeError, tarfile.TarError) 
as err:
+            for dest_path in written_paths:
+                dest_path.unlink(missing_ok=True)
+            pytest.fail(f"Could not extract MySQL SSL certs from the Docker 
container: {err}")
+
+        return MySQLSSLCerts(
+            ca=extracted["ca.pem"],
+            client_cert=extracted["client-cert.pem"],
+            client_key=extracted["client-key.pem"],
+        )
+    finally:
+        client.close()
 
 
 @pytest.fixture()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sqlite3_to_mysql-2.5.8/tests/func/test_cli.py 
new/sqlite3_to_mysql-2.6.0/tests/func/test_cli.py
--- old/sqlite3_to_mysql-2.5.8/tests/func/test_cli.py   2020-02-02 
01:00:00.000000000 +0100
+++ new/sqlite3_to_mysql-2.6.0/tests/func/test_cli.py   2020-02-02 
01:00:00.000000000 +0100
@@ -13,7 +13,7 @@
 from sqlite3_to_mysql import SQLite3toMySQL
 from sqlite3_to_mysql import __version__ as package_version
 from sqlite3_to_mysql.cli import cli as sqlite3mysql
-from tests.conftest import MySQLCredentials
+from tests.conftest import MySQLCredentials, MySQLSSLCerts
 
 
 @pytest.mark.cli
@@ -842,3 +842,181 @@
             f"{sqlite3mysql.name} version {package_version} Copyright (c) 
2018-{datetime.now().year} Klemen Tusar"
             in result.output
         )
+
+
[email protected]
[email protected]("mysql_instance")
+class TestSQLite3toMySQLSSL:
+    def test_ssl_connection_with_all_options(
+        self,
+        cli_runner: CliRunner,
+        sqlite_database: str,
+        mysql_database: Engine,
+        mysql_credentials: MySQLCredentials,
+        mysql_ssl_certs: t.Optional["MySQLSSLCerts"],
+    ) -> None:
+        del mysql_database
+        if mysql_ssl_certs is None:
+            pytest.skip("SSL certs not available for this environment")
+
+        result: Result = cli_runner.invoke(
+            sqlite3mysql,
+            [
+                "-f",
+                sqlite_database,
+                "-d",
+                mysql_credentials.database,
+                "-u",
+                mysql_credentials.user,
+                "--mysql-password",
+                mysql_credentials.password,
+                "-h",
+                mysql_credentials.host,
+                "-P",
+                str(mysql_credentials.port),
+                "--mysql-ssl-ca",
+                str(mysql_ssl_certs.ca),
+                "--mysql-ssl-cert",
+                str(mysql_ssl_certs.client_cert),
+                "--mysql-ssl-key",
+                str(mysql_ssl_certs.client_key),
+            ],
+        )
+
+        assert result.exit_code == 0
+
+    def test_ssl_connection_ca_only(
+        self,
+        cli_runner: CliRunner,
+        sqlite_database: str,
+        mysql_database: Engine,
+        mysql_credentials: MySQLCredentials,
+        mysql_ssl_certs: t.Optional["MySQLSSLCerts"],
+    ) -> None:
+        del mysql_database
+        if mysql_ssl_certs is None:
+            pytest.skip("SSL certs not available for this environment")
+
+        result: Result = cli_runner.invoke(
+            sqlite3mysql,
+            [
+                "-f",
+                sqlite_database,
+                "-d",
+                mysql_credentials.database,
+                "-u",
+                mysql_credentials.user,
+                "--mysql-password",
+                mysql_credentials.password,
+                "-h",
+                mysql_credentials.host,
+                "-P",
+                str(mysql_credentials.port),
+                "--mysql-ssl-ca",
+                str(mysql_ssl_certs.ca),
+            ],
+        )
+
+        assert result.exit_code == 0
+
+    def test_ssl_connection_cert_and_key_without_ca(
+        self,
+        cli_runner: CliRunner,
+        sqlite_database: str,
+        mysql_database: Engine,
+        mysql_credentials: MySQLCredentials,
+        mysql_ssl_certs: t.Optional["MySQLSSLCerts"],
+    ) -> None:
+        del mysql_database
+        if mysql_ssl_certs is None:
+            pytest.skip("SSL certs not available for this environment")
+
+        result: Result = cli_runner.invoke(
+            sqlite3mysql,
+            [
+                "-f",
+                sqlite_database,
+                "-d",
+                mysql_credentials.database,
+                "-u",
+                mysql_credentials.user,
+                "--mysql-password",
+                mysql_credentials.password,
+                "-h",
+                mysql_credentials.host,
+                "-P",
+                str(mysql_credentials.port),
+                "--mysql-ssl-cert",
+                str(mysql_ssl_certs.client_cert),
+                "--mysql-ssl-key",
+                str(mysql_ssl_certs.client_key),
+            ],
+        )
+
+        assert result.exit_code == 0
+
+    def test_ssl_connection_cert_without_key_fails(
+        self,
+        cli_runner: CliRunner,
+        sqlite_database: str,
+        mysql_credentials: MySQLCredentials,
+        mysql_ssl_certs: t.Optional["MySQLSSLCerts"],
+    ) -> None:
+        if mysql_ssl_certs is None:
+            pytest.skip("SSL certs not available for this environment")
+
+        result: Result = cli_runner.invoke(
+            sqlite3mysql,
+            [
+                "-f",
+                sqlite_database,
+                "-d",
+                mysql_credentials.database,
+                "-u",
+                mysql_credentials.user,
+                "--mysql-password",
+                mysql_credentials.password,
+                "-h",
+                mysql_credentials.host,
+                "-P",
+                str(mysql_credentials.port),
+                "--mysql-ssl-cert",
+                str(mysql_ssl_certs.client_cert),
+            ],
+        )
+
+        assert result.exit_code > 0
+        assert "must be provided together" in result.output
+
+    def test_ssl_connection_key_without_cert_fails(
+        self,
+        cli_runner: CliRunner,
+        sqlite_database: str,
+        mysql_credentials: MySQLCredentials,
+        mysql_ssl_certs: t.Optional["MySQLSSLCerts"],
+    ) -> None:
+        if mysql_ssl_certs is None:
+            pytest.skip("SSL certs not available for this environment")
+
+        result: Result = cli_runner.invoke(
+            sqlite3mysql,
+            [
+                "-f",
+                sqlite_database,
+                "-d",
+                mysql_credentials.database,
+                "-u",
+                mysql_credentials.user,
+                "--mysql-password",
+                mysql_credentials.password,
+                "-h",
+                mysql_credentials.host,
+                "-P",
+                str(mysql_credentials.port),
+                "--mysql-ssl-key",
+                str(mysql_ssl_certs.client_key),
+            ],
+        )
+
+        assert result.exit_code > 0
+        assert "must be provided together" in result.output
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/sqlite3_to_mysql-2.5.8/tests/unit/mysql_ssl_certs_fixture_test.py 
new/sqlite3_to_mysql-2.6.0/tests/unit/mysql_ssl_certs_fixture_test.py
--- old/sqlite3_to_mysql-2.5.8/tests/unit/mysql_ssl_certs_fixture_test.py       
1970-01-01 01:00:00.000000000 +0100
+++ new/sqlite3_to_mysql-2.6.0/tests/unit/mysql_ssl_certs_fixture_test.py       
2020-02-02 01:00:00.000000000 +0100
@@ -0,0 +1,55 @@
+from pathlib import Path
+
+import pytest
+
+from tests.conftest import (
+    MySQLSSLCerts,
+    _mysql_ssl_certs_from_environment,
+    _mysql_ssl_certs_from_home,
+)
+
+
+def _write_ssl_cert_files(directory: Path) -> MySQLSSLCerts:
+    ca = directory / "ca.pem"
+    client_cert = directory / "client-cert.pem"
+    client_key = directory / "client-key.pem"
+    for cert_path in (ca, client_cert, client_key):
+        cert_path.write_text("fake")
+
+    return MySQLSSLCerts(
+        ca=str(ca.resolve()),
+        client_cert=str(client_cert.resolve()),
+        client_key=str(client_key.resolve()),
+    )
+
+
+def test_mysql_ssl_certs_from_environment(monkeypatch: pytest.MonkeyPatch, 
tmp_path: Path) -> None:
+    monkeypatch.delenv("MYSQL_SSL_CA", raising=False)
+    monkeypatch.delenv("MYSQL_SSL_CERT", raising=False)
+    monkeypatch.delenv("MYSQL_SSL_KEY", raising=False)
+    certs = _write_ssl_cert_files(tmp_path)
+    monkeypatch.setenv("MYSQL_SSL_CA", certs.ca)
+    monkeypatch.setenv("MYSQL_SSL_CERT", certs.client_cert)
+    monkeypatch.setenv("MYSQL_SSL_KEY", certs.client_key)
+
+    assert _mysql_ssl_certs_from_environment() == certs
+
+
+def test_mysql_ssl_certs_from_environment_requires_all_paths(
+    monkeypatch: pytest.MonkeyPatch,
+    tmp_path: Path,
+) -> None:
+    monkeypatch.delenv("MYSQL_SSL_CA", raising=False)
+    monkeypatch.delenv("MYSQL_SSL_CERT", raising=False)
+    monkeypatch.delenv("MYSQL_SSL_KEY", raising=False)
+    monkeypatch.setenv("MYSQL_SSL_CA", str(tmp_path / "ca.pem"))
+
+    with pytest.raises(pytest.fail.Exception, match="must be set together"):
+        _mysql_ssl_certs_from_environment()
+
+
+def test_mysql_ssl_certs_from_home(monkeypatch: pytest.MonkeyPatch, tmp_path: 
Path) -> None:
+    certs = _write_ssl_cert_files(tmp_path)
+    monkeypatch.setattr(Path, "home", staticmethod(lambda: tmp_path), 
raising=True)
+
+    assert _mysql_ssl_certs_from_home() == certs
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/sqlite3_to_mysql-2.5.8/tests/unit/sqlite3_to_mysql_test.py 
new/sqlite3_to_mysql-2.6.0/tests/unit/sqlite3_to_mysql_test.py
--- old/sqlite3_to_mysql-2.5.8/tests/unit/sqlite3_to_mysql_test.py      
2020-02-02 01:00:00.000000000 +0100
+++ new/sqlite3_to_mysql-2.6.0/tests/unit/sqlite3_to_mysql_test.py      
2020-02-02 01:00:00.000000000 +0100
@@ -103,6 +103,231 @@
     assert "Invalid value for '--collation'" in result.output
 
 
+def test_cli_ssl_options_passed_to_transporter(
+    cli_runner: CliRunner,
+    sqlite_database: str,
+    mysql_credentials: MySQLCredentials,
+    tmp_path,
+    mocker: MockerFixture,
+) -> None:
+    transporter_ctor = mocker.patch("sqlite3_to_mysql.cli.SQLite3toMySQL", 
autospec=True)
+    transporter_ctor.return_value.transfer.return_value = None
+    ca_file = tmp_path / "ca.pem"
+    cert_file = tmp_path / "client-cert.pem"
+    key_file = tmp_path / "client-key.pem"
+    for cert_path in (ca_file, cert_file, key_file):
+        cert_path.write_text("fake")
+
+    result = cli_runner.invoke(
+        sqlite3mysql,
+        [
+            "-f",
+            sqlite_database,
+            "-d",
+            mysql_credentials.database,
+            "-u",
+            mysql_credentials.user,
+            "--mysql-ssl-ca",
+            str(ca_file),
+            "--mysql-ssl-cert",
+            str(cert_file),
+            "--mysql-ssl-key",
+            str(key_file),
+        ],
+    )
+
+    assert result.exit_code == 0
+    assert transporter_ctor.call_args.kwargs["mysql_ssl_ca"] == str(ca_file)
+    assert transporter_ctor.call_args.kwargs["mysql_ssl_cert"] == 
str(cert_file)
+    assert transporter_ctor.call_args.kwargs["mysql_ssl_key"] == str(key_file)
+    assert transporter_ctor.call_args.kwargs["mysql_ssl_disabled"] is False
+
+
+def test_cli_ssl_options_default_to_none(
+    cli_runner: CliRunner,
+    sqlite_database: str,
+    mysql_credentials: MySQLCredentials,
+    mocker: MockerFixture,
+) -> None:
+    transporter_ctor = mocker.patch("sqlite3_to_mysql.cli.SQLite3toMySQL", 
autospec=True)
+    transporter_ctor.return_value.transfer.return_value = None
+
+    result = cli_runner.invoke(
+        sqlite3mysql,
+        [
+            "-f",
+            sqlite_database,
+            "-d",
+            mysql_credentials.database,
+            "-u",
+            mysql_credentials.user,
+        ],
+    )
+
+    assert result.exit_code == 0
+    assert transporter_ctor.call_args.kwargs["mysql_ssl_ca"] is None
+    assert transporter_ctor.call_args.kwargs["mysql_ssl_cert"] is None
+    assert transporter_ctor.call_args.kwargs["mysql_ssl_key"] is None
+    assert transporter_ctor.call_args.kwargs["mysql_ssl_disabled"] is False
+
+
+def test_cli_skip_ssl_with_ssl_options_rejected(
+    cli_runner: CliRunner,
+    sqlite_database: str,
+    mysql_credentials: MySQLCredentials,
+    tmp_path,
+    mocker: MockerFixture,
+) -> None:
+    transporter_ctor = mocker.patch("sqlite3_to_mysql.cli.SQLite3toMySQL", 
autospec=True)
+    ca_file = tmp_path / "ca.pem"
+    ca_file.write_text("fake")
+
+    result = cli_runner.invoke(
+        sqlite3mysql,
+        [
+            "-f",
+            sqlite_database,
+            "-d",
+            mysql_credentials.database,
+            "-u",
+            mysql_credentials.user,
+            "--skip-ssl",
+            "--mysql-ssl-ca",
+            str(ca_file),
+        ],
+    )
+
+    assert result.exit_code > 0
+    assert "mutually exclusive" in result.output
+    assert "Error: Error:" not in result.output
+    transporter_ctor.assert_not_called()
+
+
+def test_cli_mysql_socket_with_ssl_options_rejected(
+    cli_runner: CliRunner,
+    sqlite_database: str,
+    mysql_credentials: MySQLCredentials,
+    tmp_path,
+    mocker: MockerFixture,
+) -> None:
+    transporter_ctor = mocker.patch("sqlite3_to_mysql.cli.SQLite3toMySQL", 
autospec=True)
+    socket_file = tmp_path / "mysql.sock"
+    ca_file = tmp_path / "ca.pem"
+    socket_file.write_text("")
+    ca_file.write_text("fake")
+
+    result = cli_runner.invoke(
+        sqlite3mysql,
+        [
+            "-f",
+            sqlite_database,
+            "-d",
+            mysql_credentials.database,
+            "-u",
+            mysql_credentials.user,
+            "--mysql-socket",
+            str(socket_file),
+            "--mysql-ssl-ca",
+            str(ca_file),
+        ],
+    )
+
+    assert result.exit_code > 0
+    assert "mutually exclusive" in result.output
+    assert "Error: Error:" not in result.output
+    transporter_ctor.assert_not_called()
+
+
+def test_cli_mysql_socket_directory_rejected(
+    cli_runner: CliRunner,
+    sqlite_database: str,
+    mysql_credentials: MySQLCredentials,
+    tmp_path,
+    mocker: MockerFixture,
+) -> None:
+    transporter_ctor = mocker.patch("sqlite3_to_mysql.cli.SQLite3toMySQL", 
autospec=True)
+
+    result = cli_runner.invoke(
+        sqlite3mysql,
+        [
+            "-f",
+            sqlite_database,
+            "-d",
+            mysql_credentials.database,
+            "-u",
+            mysql_credentials.user,
+            "--mysql-socket",
+            str(tmp_path),
+        ],
+    )
+
+    assert result.exit_code > 0
+    assert "is a directory" in result.output
+    transporter_ctor.assert_not_called()
+
+
+def test_cli_ssl_cert_without_key_rejected(
+    cli_runner: CliRunner,
+    sqlite_database: str,
+    mysql_credentials: MySQLCredentials,
+    tmp_path,
+    mocker: MockerFixture,
+) -> None:
+    transporter_ctor = mocker.patch("sqlite3_to_mysql.cli.SQLite3toMySQL", 
autospec=True)
+    cert_file = tmp_path / "client-cert.pem"
+    cert_file.write_text("fake")
+
+    result = cli_runner.invoke(
+        sqlite3mysql,
+        [
+            "-f",
+            sqlite_database,
+            "-d",
+            mysql_credentials.database,
+            "-u",
+            mysql_credentials.user,
+            "--mysql-ssl-cert",
+            str(cert_file),
+        ],
+    )
+
+    assert result.exit_code > 0
+    assert "must be provided together" in result.output
+    assert "Error: Error:" not in result.output
+    transporter_ctor.assert_not_called()
+
+
+def test_cli_ssl_key_without_cert_rejected(
+    cli_runner: CliRunner,
+    sqlite_database: str,
+    mysql_credentials: MySQLCredentials,
+    tmp_path,
+    mocker: MockerFixture,
+) -> None:
+    transporter_ctor = mocker.patch("sqlite3_to_mysql.cli.SQLite3toMySQL", 
autospec=True)
+    key_file = tmp_path / "client-key.pem"
+    key_file.write_text("fake")
+
+    result = cli_runner.invoke(
+        sqlite3mysql,
+        [
+            "-f",
+            sqlite_database,
+            "-d",
+            mysql_credentials.database,
+            "-u",
+            mysql_credentials.user,
+            "--mysql-ssl-key",
+            str(key_file),
+        ],
+    )
+
+    assert result.exit_code > 0
+    assert "must be provided together" in result.output
+    assert "Error: Error:" not in result.output
+    transporter_ctor.assert_not_called()
+
+
 def test_types_typed_dict_fallback(monkeypatch: pytest.MonkeyPatch) -> None:
     import typing
 
@@ -369,6 +594,276 @@
     assert isinstance(transformed.this, exp.UtcTimestamp)
 
 
+class TestSSLOptions:
+    """Tests for MySQL SSL certificate options."""
+
+    def _make_instance(
+        self,
+        sqlite_database: str,
+        mocker: MockerFixture,
+        **extra_kwargs: t.Any,
+    ) -> t.Tuple[SQLite3toMySQL, t.Dict[str, t.Any]]:
+        connect_kwargs: t.Dict[str, t.Any] = {}
+
+        class FakeCursor:
+            def execute(self, statement: t.Any) -> None:
+                del statement
+
+            def fetchone(self) -> t.Tuple[str, str]:
+                return ("version", "8.0.30")
+
+            def close(self) -> None:
+                pass
+
+        class FakeMySQLConnection:
+            def __init__(self) -> None:
+                self.database = None
+
+            def is_connected(self) -> bool:
+                return True
+
+            def cursor(self, *args: t.Any, **kwargs: t.Any) -> FakeCursor:
+                del args, kwargs
+                return FakeCursor()
+
+            def commit(self) -> None:
+                pass
+
+        def fake_connect(**kwargs: t.Any) -> FakeMySQLConnection:
+            connect_kwargs.update(kwargs)
+            return FakeMySQLConnection()
+
+        mocker.patch("sqlite3_to_mysql.transporter.mysql.connector.connect", 
side_effect=fake_connect)
+        
mocker.patch("sqlite3_to_mysql.transporter.mysql.connector.MySQLConnection", 
FakeMySQLConnection)
+        mocker.patch(
+            "sqlite3_to_mysql.transporter.CharacterSet.get_default_collation",
+            return_value=("utf8mb4_unicode_ci", None),
+        )
+
+        kwargs: t.Dict[str, t.Any] = {
+            "sqlite_file": sqlite_database,
+            "mysql_user": "user",
+            "mysql_password": "pass",
+            "mysql_database": "demo",
+        }
+        kwargs.update(extra_kwargs)
+
+        return SQLite3toMySQL(**kwargs), connect_kwargs
+
+    def test_ssl_params_passed_to_mysql_connector(
+        self,
+        sqlite_database: str,
+        tmp_path,
+        mocker: MockerFixture,
+    ) -> None:
+        ca_file = tmp_path / "ca.pem"
+        cert_file = tmp_path / "client-cert.pem"
+        key_file = tmp_path / "client-key.pem"
+        for cert_path in (ca_file, cert_file, key_file):
+            cert_path.write_text("fake")
+
+        instance, connect_kwargs = self._make_instance(
+            sqlite_database,
+            mocker,
+            mysql_ssl_ca=ca_file,
+            mysql_ssl_cert=cert_file,
+            mysql_ssl_key=key_file,
+        )
+
+        assert instance._mysql_ssl_ca == str(ca_file.resolve())
+        assert instance._mysql_ssl_cert == str(cert_file.resolve())
+        assert instance._mysql_ssl_key == str(key_file.resolve())
+        assert connect_kwargs["ssl_ca"] == str(ca_file.resolve())
+        assert connect_kwargs["ssl_cert"] == str(cert_file.resolve())
+        assert connect_kwargs["ssl_key"] == str(key_file.resolve())
+        assert connect_kwargs["ssl_verify_cert"] is True
+
+    def test_ssl_params_default_to_none(
+        self,
+        sqlite_database: str,
+        mocker: MockerFixture,
+    ) -> None:
+        instance, connect_kwargs = self._make_instance(sqlite_database, mocker)
+
+        assert instance._mysql_ssl_ca is None
+        assert instance._mysql_ssl_cert is None
+        assert instance._mysql_ssl_key is None
+        assert connect_kwargs["ssl_ca"] is None
+        assert connect_kwargs["ssl_cert"] is None
+        assert connect_kwargs["ssl_key"] is None
+        assert connect_kwargs["ssl_verify_cert"] is False
+
+    def test_ssl_ca_only(
+        self,
+        sqlite_database: str,
+        tmp_path,
+        mocker: MockerFixture,
+    ) -> None:
+        ca_file = tmp_path / "ca.pem"
+        ca_file.write_text("fake")
+
+        instance, connect_kwargs = self._make_instance(
+            sqlite_database,
+            mocker,
+            mysql_ssl_ca=ca_file,
+        )
+
+        assert instance._mysql_ssl_ca == str(ca_file.resolve())
+        assert instance._mysql_ssl_cert is None
+        assert instance._mysql_ssl_key is None
+        assert connect_kwargs["ssl_ca"] == str(ca_file.resolve())
+        assert connect_kwargs["ssl_verify_cert"] is True
+
+    def test_ssl_ca_expands_home_directory(
+        self,
+        sqlite_database: str,
+        tmp_path,
+        monkeypatch: pytest.MonkeyPatch,
+        mocker: MockerFixture,
+    ) -> None:
+        ca_file = tmp_path / "ca.pem"
+        ca_file.write_text("fake")
+        monkeypatch.setenv("HOME", str(tmp_path))
+
+        instance, connect_kwargs = self._make_instance(
+            sqlite_database,
+            mocker,
+            mysql_ssl_ca="~/ca.pem",
+        )
+
+        assert instance._mysql_ssl_ca == str(ca_file.resolve())
+        assert connect_kwargs["ssl_ca"] == str(ca_file.resolve())
+
+    def test_ssl_cert_and_key_without_ca(
+        self,
+        sqlite_database: str,
+        tmp_path,
+        mocker: MockerFixture,
+    ) -> None:
+        cert_file = tmp_path / "client-cert.pem"
+        key_file = tmp_path / "client-key.pem"
+        cert_file.write_text("fake")
+        key_file.write_text("fake")
+
+        instance, connect_kwargs = self._make_instance(
+            sqlite_database,
+            mocker,
+            mysql_ssl_cert=cert_file,
+            mysql_ssl_key=key_file,
+        )
+
+        assert instance._mysql_ssl_ca is None
+        assert instance._mysql_ssl_cert == str(cert_file.resolve())
+        assert instance._mysql_ssl_key == str(key_file.resolve())
+        assert connect_kwargs["ssl_ca"] is None
+        assert connect_kwargs["ssl_cert"] == str(cert_file.resolve())
+        assert connect_kwargs["ssl_key"] == str(key_file.resolve())
+        assert connect_kwargs["ssl_verify_cert"] is False
+
+    def test_ssl_empty_strings_treated_as_none(
+        self,
+        sqlite_database: str,
+        mocker: MockerFixture,
+    ) -> None:
+        instance, connect_kwargs = self._make_instance(
+            sqlite_database,
+            mocker,
+            mysql_ssl_ca="",
+            mysql_ssl_cert="",
+            mysql_ssl_key="",
+        )
+
+        assert instance._mysql_ssl_ca is None
+        assert instance._mysql_ssl_cert is None
+        assert instance._mysql_ssl_key is None
+        assert connect_kwargs["ssl_ca"] is None
+        assert connect_kwargs["ssl_cert"] is None
+        assert connect_kwargs["ssl_key"] is None
+
+    @pytest.mark.parametrize(
+        "ssl_kwargs,expected_message",
+        [
+            ({"mysql_ssl_ca": "missing-ca.pem"}, "MySQL SSL CA certificate 
file does not exist"),
+            (
+                {
+                    "mysql_ssl_cert": "missing-client-cert.pem",
+                    "mysql_ssl_key": "missing-client-key.pem",
+                },
+                "MySQL SSL certificate file does not exist",
+            ),
+        ],
+    )
+    def test_ssl_missing_files_raise(
+        self,
+        sqlite_database: str,
+        tmp_path,
+        mocker: MockerFixture,
+        ssl_kwargs: t.Dict[str, str],
+        expected_message: str,
+    ) -> None:
+        kwargs = {key: tmp_path / value for key, value in ssl_kwargs.items()}
+
+        with pytest.raises(FileNotFoundError, match=expected_message):
+            self._make_instance(sqlite_database, mocker, **kwargs)
+
+    def test_ssl_disabled_with_ssl_options_raises(
+        self,
+        sqlite_database: str,
+        mocker: MockerFixture,
+    ) -> None:
+        with pytest.raises(ValueError, match="Cannot use SSL certificate 
options when SSL is disabled"):
+            self._make_instance(
+                sqlite_database,
+                mocker,
+                mysql_ssl_ca="/path/to/ca.pem",
+                mysql_ssl_disabled=True,
+            )
+
+    def test_ssl_cert_without_key_raises(
+        self,
+        sqlite_database: str,
+        mocker: MockerFixture,
+    ) -> None:
+        with pytest.raises(ValueError, match="mysql_ssl_cert and mysql_ssl_key 
must be provided together"):
+            self._make_instance(
+                sqlite_database,
+                mocker,
+                mysql_ssl_cert="/path/to/client-cert.pem",
+            )
+
+    def test_ssl_key_without_cert_raises(
+        self,
+        sqlite_database: str,
+        mocker: MockerFixture,
+    ) -> None:
+        with pytest.raises(ValueError, match="mysql_ssl_cert and mysql_ssl_key 
must be provided together"):
+            self._make_instance(
+                sqlite_database,
+                mocker,
+                mysql_ssl_key="/path/to/client-key.pem",
+            )
+
+    def test_mysql_socket_with_ssl_options_raises(
+        self,
+        sqlite_database: str,
+        tmp_path,
+        mocker: MockerFixture,
+    ) -> None:
+        socket_file = tmp_path / "mysql.sock"
+        socket_file.write_text("")
+
+        with pytest.raises(
+            ValueError,
+            match="Cannot use SSL certificate options when connecting through 
a MySQL socket",
+        ):
+            self._make_instance(
+                sqlite_database,
+                mocker,
+                mysql_socket=str(socket_file),
+                mysql_ssl_ca="/path/to/ca.pem",
+            )
+
+
 def _make_transfer_stub(mocker: MockFixture) -> SQLite3toMySQL:
     instance = SQLite3toMySQL.__new__(SQLite3toMySQL)
     instance._sqlite_tables = tuple()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sqlite3_to_mysql-2.5.8/tests/unit/types_test.py 
new/sqlite3_to_mysql-2.6.0/tests/unit/types_test.py
--- old/sqlite3_to_mysql-2.5.8/tests/unit/types_test.py 2020-02-02 
01:00:00.000000000 +0100
+++ new/sqlite3_to_mysql-2.6.0/tests/unit/types_test.py 2020-02-02 
01:00:00.000000000 +0100
@@ -14,19 +14,22 @@
 class TestTypes:
     def test_sqlite3_to_mysql_params_typing(self) -> None:
         """Test SQLite3toMySQLParams typing."""
-        # Create a valid params dict
+        # Create a typing example with all declared fields and valid option 
combinations.
         params: SQLite3toMySQLParams = {
             "sqlite_file": "test.db",
             "sqlite_tables": ["table1", "table2"],
-            "exclude_sqlite_tables": ["skip_this"],
+            "exclude_sqlite_tables": None,
             "sqlite_views_as_tables": False,
             "without_foreign_keys": False,
             "mysql_user": "user",
             "mysql_password": "password",
             "mysql_host": "localhost",
             "mysql_port": 3306,
-            "mysql_socket": "/var/run/mysqld/mysqld.sock",
-            "mysql_ssl_disabled": True,
+            "mysql_socket": None,
+            "mysql_ssl_ca": "ca.pem",
+            "mysql_ssl_cert": "client-cert.pem",
+            "mysql_ssl_key": "client-key.pem",
+            "mysql_ssl_disabled": False,
             "chunk": 1000,
             "quiet": False,
             "log_file": "log.txt",
@@ -48,15 +51,18 @@
         # Test that all fields are accessible
         assert params["sqlite_file"] == "test.db"
         assert params["sqlite_tables"] == ["table1", "table2"]
-        assert params["exclude_sqlite_tables"] == ["skip_this"]
+        assert params["exclude_sqlite_tables"] is None
         assert params["sqlite_views_as_tables"] is False
         assert params["without_foreign_keys"] is False
         assert params["mysql_user"] == "user"
         assert params["mysql_password"] == "password"
         assert params["mysql_host"] == "localhost"
         assert params["mysql_port"] == 3306
-        assert params["mysql_socket"] == "/var/run/mysqld/mysqld.sock"
-        assert params["mysql_ssl_disabled"] is True
+        assert params["mysql_socket"] is None
+        assert params["mysql_ssl_ca"] == "ca.pem"
+        assert params["mysql_ssl_cert"] == "client-cert.pem"
+        assert params["mysql_ssl_key"] == "client-key.pem"
+        assert params["mysql_ssl_disabled"] is False
         assert params["chunk"] == 1000
         assert params["quiet"] is False
         assert params["log_file"] == "log.txt"
@@ -98,8 +104,11 @@
                 self._mysql_password = "password"
                 self._mysql_host = "localhost"
                 self._mysql_port = 3306
-                self._mysql_socket = "/var/run/mysqld/mysqld.sock"
-                self._mysql_ssl_disabled = True
+                self._mysql_socket = None
+                self._mysql_ssl_ca = "ca.pem"
+                self._mysql_ssl_cert = "client-cert.pem"
+                self._mysql_ssl_key = "client-key.pem"
+                self._mysql_ssl_disabled = False
                 self._chunk_size = 1000
                 self._quiet = False
                 self._logger = MagicMock(spec=Logger)
@@ -140,8 +149,11 @@
         assert instance._mysql_password == "password"
         assert instance._mysql_host == "localhost"
         assert instance._mysql_port == 3306
-        assert instance._mysql_socket == "/var/run/mysqld/mysqld.sock"
-        assert instance._mysql_ssl_disabled is True
+        assert instance._mysql_socket is None
+        assert instance._mysql_ssl_ca == "ca.pem"
+        assert instance._mysql_ssl_cert == "client-cert.pem"
+        assert instance._mysql_ssl_key == "client-key.pem"
+        assert instance._mysql_ssl_disabled is False
         assert instance._chunk_size == 1000
         assert instance._quiet is False
         assert instance._log_file == "log.txt"

Reply via email to