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"