Skip to content

Commit 71a5a12

Browse files
committed
Use trustme for test TLS certificate and key
With the current approach TLS certificate and key are hardcoded in separate files and distributed as part of wheels. This is unnecessary for end users of the package, also this brings difficulties while supporting Python 3.13, see: aio-libs#473 Use trustme for generating TLS certificate and key on the fly, as it is done in other aio-libs packages. Initially proposed in: aio-libs#473 (comment)
1 parent 64ed2b0 commit 71a5a12

File tree

9 files changed

+65
-132
lines changed

9 files changed

+65
-132
lines changed

aiosmtpd/tests/certs/__init__.py

Whitespace-only changes.

aiosmtpd/tests/certs/server.crt

Lines changed: 0 additions & 23 deletions
This file was deleted.

aiosmtpd/tests/certs/server.key

Lines changed: 0 additions & 28 deletions
This file was deleted.

aiosmtpd/tests/certs/server_alt.crt

Lines changed: 0 additions & 23 deletions
This file was deleted.

aiosmtpd/tests/certs/server_alt.key

Lines changed: 0 additions & 28 deletions
This file was deleted.

aiosmtpd/tests/conftest.py

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from contextlib import suppress
1010
from functools import wraps
1111
from smtplib import SMTP as SMTPClient
12-
from typing import Any, Callable, Generator, NamedTuple, Optional, Type, TypeVar
12+
from typing import Any, Callable, Generator, Iterator, NamedTuple, Optional, Type, TypeVar
1313

1414
import pytest
1515
from pkg_resources import resource_filename
@@ -26,14 +26,22 @@
2626
_ProactorBasePipeTransport = None
2727
HAS_PROACTOR = False
2828

29+
try:
30+
import trustme
31+
32+
# Check if the CA is available in runtime, MacOS on Py3.10 fails somehow
33+
trustme.CA()
34+
35+
TRUSTME: bool = True
36+
except ImportError:
37+
TRUSTME = False
38+
2939

3040
__all__ = [
3141
"controller_data",
3242
"handler_data",
3343
"Global",
3444
"AUTOSTOP_DELAY",
35-
"SERVER_CRT",
36-
"SERVER_KEY",
3745
]
3846

3947

@@ -73,8 +81,6 @@ def set_addr_from(cls, contr: Controller):
7381
# If less than 1.0, might cause intermittent error if test system
7482
# is too busy/overloaded.
7583
AUTOSTOP_DELAY = 1.5
76-
SERVER_CRT = resource_filename("aiosmtpd.tests.certs", "server.crt")
77-
SERVER_KEY = resource_filename("aiosmtpd.tests.certs", "server.key")
7884

7985
# endregion
8086

@@ -315,27 +321,52 @@ def client(request: pytest.FixtureRequest) -> Generator[SMTPClient, None, None]:
315321

316322

317323
@pytest.fixture
318-
def ssl_context_server() -> ssl.SSLContext:
324+
def tls_certificate_authority() -> trustme.CA:
325+
if not TRUSTME:
326+
pytest.xfail("trustme is not supported")
327+
return trustme.CA()
328+
329+
330+
@pytest.fixture
331+
def tls_certificate(tls_certificate_authority: trustme.CA) -> trustme.LeafCert:
332+
return tls_certificate_authority.issue_cert(
333+
"localhost",
334+
"xn--prklad-4va.localhost",
335+
"127.0.0.1",
336+
"::1",
337+
)
338+
339+
340+
@pytest.fixture
341+
def ssl_context_server(tls_certificate: trustme.LeafCert) -> ssl.SSLContext:
319342
"""
320343
Provides a server-side SSL Context
321344
"""
322-
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
323-
context.check_hostname = False
324-
context.load_cert_chain(SERVER_CRT, SERVER_KEY)
325-
#
326-
return context
345+
ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
346+
tls_certificate.configure_cert(ssl_ctx)
347+
return ssl_ctx
327348

328349

329350
@pytest.fixture
330-
def ssl_context_client() -> ssl.SSLContext:
351+
def ssl_context_client(tls_certificate_authority: trustme.CA) -> ssl.SSLContext:
331352
"""
332353
Provides a client-side SSL Context
333354
"""
334-
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
335-
context.check_hostname = False
336-
context.load_verify_locations(SERVER_CRT)
337-
#
338-
return context
355+
ssl_ctx = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH)
356+
tls_certificate_authority.configure_trust(ssl_ctx)
357+
return ssl_ctx
358+
359+
360+
@pytest.fixture
361+
def tls_cert_pem_path(tls_certificate: trustme.LeafCert) -> Iterator[str]:
362+
with tls_certificate.cert_chain_pems[0].tempfile() as cert_pem:
363+
yield cert_pem
364+
365+
366+
@pytest.fixture
367+
def tls_key_pem_path(tls_certificate: trustme.LeafCert) -> Iterator[str]:
368+
with tls_certificate.private_key_pem.tempfile() as key_pem:
369+
yield key_pem
339370

340371

341372
# Please keep the scope as "module"; setting it as "function" (the default) somehow

aiosmtpd/tests/test_main.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from aiosmtpd.main import main, parseargs
2222
from aiosmtpd.testing.helpers import catchup_delay
2323
from aiosmtpd.testing.statuscodes import SMTP_STATUS_CODES as S
24-
from aiosmtpd.tests.conftest import AUTOSTOP_DELAY, SERVER_CRT, SERVER_KEY
24+
from aiosmtpd.tests.conftest import AUTOSTOP_DELAY
2525

2626
try:
2727
import pwd
@@ -199,24 +199,24 @@ def test_debug_3(self):
199199

200200
@pytest.mark.skipif(sys.platform == "darwin", reason="No idea why these are failing")
201201
class TestMainByWatcher:
202-
def test_tls(self, temp_event_loop):
202+
def test_tls(self, temp_event_loop, tls_cert_pem_path, tls_key_pem_path):
203203
with watcher_process(watch_for_tls) as retq:
204204
temp_event_loop.call_later(AUTOSTOP_DELAY, temp_event_loop.stop)
205-
main_n("--tlscert", str(SERVER_CRT), "--tlskey", str(SERVER_KEY))
205+
main_n("--tlscert", tls_cert_pem_path, "--tlskey", tls_key_pem_path)
206206
catchup_delay()
207207
has_starttls = retq.get()
208208
assert has_starttls is True
209209
require_tls = retq.get()
210210
assert require_tls is True
211211

212-
def test_tls_noreq(self, temp_event_loop):
212+
def test_tls_noreq(self, temp_event_loop, tls_cert_pem_path, tls_key_pem_path):
213213
with watcher_process(watch_for_tls) as retq:
214214
temp_event_loop.call_later(AUTOSTOP_DELAY, temp_event_loop.stop)
215215
main_n(
216216
"--tlscert",
217-
str(SERVER_CRT),
217+
tls_cert_pem_path,
218218
"--tlskey",
219-
str(SERVER_KEY),
219+
tls_key_pem_path,
220220
"--no-requiretls",
221221
)
222222
catchup_delay()
@@ -225,10 +225,10 @@ def test_tls_noreq(self, temp_event_loop):
225225
require_tls = retq.get()
226226
assert require_tls is False
227227

228-
def test_smtps(self, temp_event_loop):
228+
def test_smtps(self, temp_event_loop, tls_cert_pem_path, tls_key_pem_path):
229229
with watcher_process(watch_for_smtps) as retq:
230230
temp_event_loop.call_later(AUTOSTOP_DELAY, temp_event_loop.stop)
231-
main_n("--smtpscert", str(SERVER_CRT), "--smtpskey", str(SERVER_KEY))
231+
main_n("--smtpscert", tls_cert_pem_path, "--smtpskey", tls_key_pem_path)
232232
catchup_delay()
233233
has_smtps = retq.get()
234234
assert has_smtps is True
@@ -335,16 +335,18 @@ def test_norequiretls(self, capsys, mocker):
335335
assert args.requiretls is False
336336

337337
@pytest.mark.parametrize(
338-
("certfile", "keyfile", "expect"),
338+
("certfile_present", "keyfile_present", "expect"),
339339
[
340-
("x", "x", "Cert file x not found"),
341-
(SERVER_CRT, "x", "Key file x not found"),
342-
("x", SERVER_KEY, "Cert file x not found"),
340+
(False, True, "Cert file x not found"),
341+
(True, False, "Key file x not found"),
342+
(False, True, "Cert file x not found"),
343343
],
344344
ids=["x-x", "cert-x", "x-key"],
345345
)
346346
@pytest.mark.parametrize("meth", ["smtps", "tls"])
347-
def test_ssl_files_err(self, capsys, mocker, meth, certfile, keyfile, expect):
347+
def test_ssl_files_err(self, capsys, mocker, meth, certfile_present, keyfile_present, expect, request):
348+
certfile = request.getfixturevalue("tls_cert_pem_path") if certfile_present else "x"
349+
keyfile = request.getfixturevalue("tls_key_pem_path") if keyfile_present else "x"
348350
mocker.patch("aiosmtpd.main.PROGRAM", "smtpd")
349351
with pytest.raises(SystemExit) as exc:
350352
parseargs((f"--{meth}cert", certfile, f"--{meth}key", keyfile))

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ coverage==7.6.1
44
pytest==8.3.4
55
pytest-cov==5.0.0
66
pytest-mock==3.14.0
7+
trustme==1.2.1

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ deps =
2828
pytest-profiling
2929
pytest-sugar
3030
py # needed for pytest-sugar as it doesn't declare dependency on it.
31+
trustme
3132
!nocov: coverage>=7.0.1
3233
!nocov: coverage[toml]
3334
!nocov: coverage-conditional-plugin

0 commit comments

Comments
 (0)