Skip to content

Commit 2a1c560

Browse files
committed
build!: add ssl tests
@see #77
1 parent 2f02107 commit 2a1c560

File tree

6 files changed

+390
-9
lines changed

6 files changed

+390
-9
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ Options:
6565
--mysql-charset TEXT MySQL database and table character set
6666
[default: utf8mb4]
6767
--mysql-collation TEXT MySQL database and table collation
68+
--mysql-ssl-ca PATH Path to SSL CA certificate file.
6869
--mysql-ssl-cert PATH Path to SSL certificate file.
6970
--mysql-ssl-key PATH Path to SSL key file.
70-
--mysql-ssl-ca PATH Path to SSL CA certificate file.
7171
-S, --skip-ssl Disable MySQL connection encryption.
7272
-c, --chunk INTEGER Chunk reading/writing SQL records
7373
-l, --log-file PATH Log file

docs/README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ Connection Options
4646
- ``-P, --mysql-port INTEGER``: MySQL port. Defaults to 3306.
4747
- ``--mysql-charset TEXT``: MySQL database and table character set. The default is utf8mb4.
4848
- ``--mysql-collation TEXT``: MySQL database and table collation.
49+
- ``--mysql-ssl-ca PATH``: Path to SSL CA certificate file.
4950
- ``--mysql-ssl-cert PATH``: Path to SSL certificate file.
5051
- ``--mysql-ssl-key PATH``: Path to SSL key file.
51-
- ``--mysql-ssl-ca PATH``: Path to SSL CA certificate file.
5252
- ``-S, --skip-ssl``: Disable MySQL connection encryption.
5353

5454
Other Options

src/mysql_to_sqlite3/cli.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,9 @@
126126
default=None,
127127
help="MySQL database and table collation",
128128
)
129+
@click.option("--mysql-ssl-ca", type=click.Path(), help="Path to SSL CA certificate file.")
129130
@click.option("--mysql-ssl-cert", type=click.Path(), help="Path to SSL certificate file.")
130131
@click.option("--mysql-ssl-key", type=click.Path(), help="Path to SSL key file.")
131-
@click.option("--mysql-ssl-ca", type=click.Path(), help="Path to SSL CA certificate file.")
132132
@click.option("-S", "--skip-ssl", is_flag=True, help="Disable MySQL connection encryption.")
133133
@click.option(
134134
"-c",
@@ -174,9 +174,9 @@ def cli(
174174
mysql_port: int,
175175
mysql_charset: str,
176176
mysql_collation: str,
177+
mysql_ssl_ca: t.Optional[str],
177178
mysql_ssl_cert: t.Optional[str],
178179
mysql_ssl_key: t.Optional[str],
179-
mysql_ssl_ca: t.Optional[str],
180180
skip_ssl: bool,
181181
chunk: int,
182182
log_file: t.Union[str, "os.PathLike[t.Any]"],
@@ -225,9 +225,9 @@ def cli(
225225
mysql_port=mysql_port,
226226
mysql_charset=mysql_charset,
227227
mysql_collation=mysql_collation,
228+
mysql_ssl_ca=mysql_ssl_ca,
228229
mysql_ssl_cert=mysql_ssl_cert,
229230
mysql_ssl_key=mysql_ssl_key,
230-
mysql_ssl_ca=mysql_ssl_ca,
231231
mysql_ssl_disabled=skip_ssl,
232232
chunk=chunk,
233233
json_as_text=json_as_text,

tests/conftest.py

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import json
22
import os
33
import socket
4+
import subprocess
5+
import threading
46
import typing as t
7+
import uuid
58
from codecs import open
69
from contextlib import contextmanager
7-
from os.path import abspath, dirname, isfile, join
10+
from os.path import abspath, basename, dirname, isfile, join
811
from pathlib import Path
912
from random import choice
1013
from string import ascii_lowercase, ascii_uppercase, digits
@@ -31,6 +34,7 @@
3134
from sqlalchemy_utils import database_exists, drop_database
3235

3336
from . import database, factories, models
37+
from .utils import generate_ssl_certs, stream_logs
3438

3539

3640
def pytest_addoption(parser: "Parser"):
@@ -70,6 +74,27 @@ def pytest_addoption(parser: "Parser"):
7074
help="The TCP port of the MySQL server.",
7175
)
7276

77+
parser.addoption(
78+
"--mysql-ssl-ca",
79+
dest="mysql_ssl_ca",
80+
default=None,
81+
help="Path to SSL CA certificate file.",
82+
)
83+
84+
parser.addoption(
85+
"--mysql-ssl-cert",
86+
dest="mysql_ssl_cert",
87+
default=None,
88+
help="Path to SSL certificate file.",
89+
)
90+
91+
parser.addoption(
92+
"--mysql-ssl-key",
93+
dest="mysql_ssl_key",
94+
default=None,
95+
help="Path to SSL key file.",
96+
)
97+
7398
parser.addoption(
7499
"--no-docker",
75100
dest="use_docker",
@@ -159,10 +184,13 @@ class MySQLCredentials(t.NamedTuple):
159184
host: str
160185
port: int
161186
database: str
187+
ssl_ca: t.Optional[str] = None
188+
ssl_cert: t.Optional[str] = None
189+
ssl_key: t.Optional[str] = None
162190

163191

164192
@pytest.fixture(scope="session")
165-
def mysql_credentials(pytestconfig: Config) -> MySQLCredentials:
193+
def mysql_credentials(request, pytestconfig: Config, tmp_path_factory: pytest.TempPathFactory) -> MySQLCredentials:
166194
db_credentials_file: str = abspath(join(dirname(__file__), "db_credentials.json"))
167195
if isfile(db_credentials_file):
168196
with open(db_credentials_file, "r", "utf-8") as fh:
@@ -173,6 +201,9 @@ def mysql_credentials(pytestconfig: Config) -> MySQLCredentials:
173201
database=db_credentials["mysql_database"],
174202
host=db_credentials["mysql_host"],
175203
port=db_credentials["mysql_port"],
204+
ssl_ca=db_credentials.get("mysql_ssl_ca"),
205+
ssl_cert=db_credentials.get("mysql_ssl_cert"),
206+
ssl_key=db_credentials.get("mysql_ssl_key"),
176207
)
177208

178209
port: int = pytestconfig.getoption("mysql_port") or 3306
@@ -182,12 +213,35 @@ def mysql_credentials(pytestconfig: Config) -> MySQLCredentials:
182213
pytest.fail(f"No ports appear to be available on the host {pytestconfig.getoption('mysql_host')}")
183214
port += 1
184215

216+
ssl_credentials = {
217+
"ssl_ca": pytestconfig.getoption("mysql_ssl_ca") or None,
218+
"ssl_cert": pytestconfig.getoption("mysql_ssl_cert") or None,
219+
"ssl_key": pytestconfig.getoption("mysql_ssl_key") or None,
220+
}
221+
222+
if hasattr(request, "param") and request.param == "ssl":
223+
certs_dir = tmp_path_factory.getbasetemp() / "certs"
224+
if not certs_dir.exists():
225+
certs_dir.mkdir(parents=True)
226+
generate_ssl_certs(certs_dir)
227+
228+
# FIXED: docker perms
229+
subprocess.call(["chmod", "0644", str(certs_dir / "ca-key.pem")])
230+
subprocess.call(["chmod", "0644", str(certs_dir / "server-key.pem")])
231+
232+
ssl_credentials = {
233+
"ssl_ca": str(certs_dir / "ca.pem"),
234+
"ssl_cert": str(certs_dir / "server-cert.pem"),
235+
"ssl_key": str(certs_dir / "server-key.pem"),
236+
}
237+
185238
return MySQLCredentials(
186239
user=pytestconfig.getoption("mysql_user") or "tester",
187240
password=pytestconfig.getoption("mysql_password") or "testpass",
188241
database=pytestconfig.getoption("mysql_database") or "test_db",
189242
host=pytestconfig.getoption("mysql_host") or "0.0.0.0",
190243
port=port,
244+
**ssl_credentials,
191245
)
192246

193247

@@ -222,33 +276,72 @@ def mysql_instance(mysql_credentials: MySQLCredentials, pytestconfig: Config) ->
222276
except (HTTPError, NotFound) as err:
223277
pytest.fail(str(err))
224278

279+
ssl_cmds = []
280+
ssl_args = {}
281+
ssl_volumes = {}
282+
host_certs_dir = None
283+
container_certs_dir = "/etc/mysql/certs"
284+
285+
if mysql_credentials.ssl_ca:
286+
host_certs_dir = dirname(mysql_credentials.ssl_ca)
287+
ssl_cmds.append(f"--ssl-ca={container_certs_dir}/{basename(mysql_credentials.ssl_ca)}")
288+
ssl_args["ssl_ca"] = mysql_credentials.ssl_ca
289+
290+
if mysql_credentials.ssl_cert:
291+
host_certs_dir = dirname(mysql_credentials.ssl_cert)
292+
ssl_cmds.append(f"--ssl-cert={container_certs_dir}/{basename(mysql_credentials.ssl_cert)}")
293+
ssl_args["ssl_cert"] = f"{host_certs_dir}/client-cert.pem"
294+
295+
if mysql_credentials.ssl_key:
296+
host_certs_dir = dirname(mysql_credentials.ssl_key)
297+
ssl_cmds.append(f"--ssl-key={container_certs_dir}/{basename(mysql_credentials.ssl_key)}")
298+
ssl_args["ssl_key"] = f"{host_certs_dir}/client-key.pem"
299+
300+
if host_certs_dir:
301+
ssl_volumes[host_certs_dir] = {"bind": container_certs_dir, "mode": "ro"}
302+
303+
if ssl_args:
304+
ssl_args["ssl_verify_cert"] = True
305+
306+
container_name = f"pytest_mysql_to_sqlite3_{uuid.uuid4().hex[:10]}"
307+
225308
container = client.containers.run(
226309
image=docker_mysql_image,
227-
name="pytest_mysql_to_sqlite3",
310+
name=container_name,
228311
ports={"3306/tcp": (mysql_credentials.host, f"{mysql_credentials.port}/tcp")},
229312
environment={
230313
"MYSQL_RANDOM_ROOT_PASSWORD": "yes",
231314
"MYSQL_USER": mysql_credentials.user,
232315
"MYSQL_PASSWORD": mysql_credentials.password,
233316
"MYSQL_DATABASE": mysql_credentials.database,
234317
},
318+
volumes=ssl_volumes,
235319
command=[
236320
"--character-set-server=utf8mb4",
237321
"--collation-server=utf8mb4_unicode_ci",
238-
],
322+
]
323+
+ ssl_cmds,
239324
detach=True,
240325
auto_remove=True,
241326
)
242327

328+
log_thread = threading.Thread(target=stream_logs, args=(container,))
329+
# The thread will terminate when the main program terminates
330+
log_thread.daemon = True
331+
log_thread.start()
332+
243333
while not mysql_available and mysql_connection_retries > 0:
244334
try:
335+
print(f"Attempt #{mysql_connection_retries} to connect to MySQL...")
336+
245337
mysql_connection = mysql.connector.connect(
246338
user=mysql_credentials.user,
247339
password=mysql_credentials.password,
248340
host=mysql_credentials.host,
249341
port=mysql_credentials.port,
250342
charset="utf8mb4",
251343
collation="utf8mb4_unicode_ci",
344+
**ssl_args,
252345
)
253346
except mysql.connector.Error as err:
254347
if err.errno == errorcode.CR_SERVER_LOST:
@@ -270,6 +363,10 @@ def mysql_instance(mysql_credentials: MySQLCredentials, pytestconfig: Config) ->
270363
if use_docker and container is not None:
271364
container.kill()
272365

366+
# Wait for the log thread to finish (optional)
367+
if "log_thread" in locals() and log_thread.is_alive():
368+
log_thread.join(timeout=5)
369+
273370

274371
@pytest.fixture(scope="session")
275372
def mysql_database(

0 commit comments

Comments
 (0)