Mercurial > hg-stable
changeset 50729:b3a5af04da35 stable
tests: use simple mock smtp server instead of deprecated asyncore smtpd
test-patchbomb-tls.t would fail with:
.../hg/tests/dummysmtpd.py:6: DeprecationWarning: The asyncore module is deprecated and will be removed in Python 3.12. The recommended replacement is asyncio
import asyncore
.../hg/tests/dummysmtpd.py:8: DeprecationWarning: The smtpd module is deprecated and unmaintained and will be removed in Python 3.12. Please see aiosmtpd (https://aiosmtpd.readthedocs.io/) for the recommended replacement.
import smtpd
The recommended migration path to the standalone asiosmtpd would be overkill.
The tests do not need a full smtp server - we can just use a very simple mock
hack to preserve the existing test coverage.
author | Mads Kiilerich <mads@kiilerich.com> |
---|---|
date | Thu, 23 Mar 2023 16:45:12 +0100 |
parents | 8823e4d411ba |
children | b1ac55606eb7 |
files | tests/dummysmtpd.py tests/test-patchbomb-tls.t |
diffstat | 2 files changed, 96 insertions(+), 57 deletions(-) [+] |
line wrap: on
line diff
--- a/tests/dummysmtpd.py Mon Jun 26 16:45:13 2023 +0200 +++ b/tests/dummysmtpd.py Thu Mar 23 16:45:12 2023 +0100 @@ -3,12 +3,11 @@ """dummy SMTP server for use in tests""" -import asyncore import optparse -import smtpd +import os +import socket import ssl import sys -import traceback from mercurial import ( pycompat, @@ -18,57 +17,97 @@ ) +if os.environ.get('HGIPV6', '0') == '1': + family = socket.AF_INET6 +else: + family = socket.AF_INET + + def log(msg): sys.stdout.write(msg) sys.stdout.flush() -class dummysmtpserver(smtpd.SMTPServer): - def __init__(self, localaddr): - smtpd.SMTPServer.__init__(self, localaddr, remoteaddr=None) +def mocksmtpserversession(conn, addr): + conn.send(b'220 smtp.example.com ESMTP\r\n') + + line = conn.recv(1024) + if not line.lower().startswith(b'ehlo '): + log('no hello: %s\n' % line) + return + + conn.send(b'250 Hello\r\n') + + line = conn.recv(1024) + if not line.lower().startswith(b'mail from:'): + log('no mail from: %s\n' % line) + return + mailfrom = line[10:].decode().rstrip() + if mailfrom.startswith('<') and mailfrom.endswith('>'): + mailfrom = mailfrom[1:-1] + + conn.send(b'250 Ok\r\n') - def process_message(self, peer, mailfrom, rcpttos, data, **kwargs): - log( - '%s from=%s to=%s\n%s\n' - % (peer[0], mailfrom, ', '.join(rcpttos), data.decode()) - ) + rcpttos = [] + while True: + line = conn.recv(1024) + if not line.lower().startswith(b'rcpt to:'): + break + rcptto = line[8:].decode().rstrip() + if rcptto.startswith('<') and rcptto.endswith('>'): + rcptto = rcptto[1:-1] + rcpttos.append(rcptto) + + conn.send(b'250 Ok\r\n') + + if not line.lower().strip() == b'data': + log('no rcpt to or data: %s' % line) + + conn.send(b'354 Go ahead\r\n') - def handle_error(self): - # On Windows, a bad SSL connection sometimes generates a WSAECONNRESET. - # The default handler will shutdown this server, and then both the - # current connection and subsequent ones fail on the client side with - # "No connection could be made because the target machine actively - # refused it". If we eat the error, then the client properly aborts in - # the expected way, and the server is available for subsequent requests. - traceback.print_exc() + data = b'' + while True: + line = conn.recv(1024) + if not line: + log('connection closed before end of data') + break + data += line + if data.endswith(b'\r\n.\r\n'): + data = data[:-5] + break + + conn.send(b'250 Ok\r\n') + + log( + '%s from=%s to=%s\n%s\n' + % (addr[0], mailfrom, ', '.join(rcpttos), data.decode()) + ) -class dummysmtpsecureserver(dummysmtpserver): - def __init__(self, localaddr, certfile): - dummysmtpserver.__init__(self, localaddr) - self._certfile = certfile - - def handle_accept(self): - pair = self.accept() - if not pair: - return - conn, addr = pair - ui = uimod.ui.load() +def run(host, port, certificate): + ui = uimod.ui.load() + with socket.socket(family, socket.SOCK_STREAM) as s: + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind((host, port)) + # log('listening at %s:%d\n' % (host, port)) + s.listen(1) try: - # wrap_socket() would block, but we don't care - conn = sslutil.wrapserversocket(conn, ui, certfile=self._certfile) - except ssl.SSLError as e: - log('%s ssl error: %s\n' % (addr[0], e)) - conn.close() - return - smtpd.SMTPChannel(self, conn, addr) - - -def run(): - try: - asyncore.loop() - except KeyboardInterrupt: - pass + while True: + conn, addr = s.accept() + if certificate: + try: + conn = sslutil.wrapserversocket( + conn, ui, certfile=certificate + ) + except ssl.SSLError as e: + log('%s ssl error: %s\n' % (addr[0], e)) + conn.close() + continue + log("connection from %s:%s\n" % addr) + mocksmtpserversession(conn, addr) + conn.close() + except KeyboardInterrupt: + pass def _encodestrsonly(v): @@ -102,19 +141,9 @@ if (opts.tls == 'smtps') != bool(opts.certificate): op.error('--certificate must be specified with --tls=smtps') - addr = (opts.address, opts.port) - - def init(): - if opts.tls == 'none': - dummysmtpserver(addr) - else: - dummysmtpsecureserver(addr, opts.certificate) - log('listening at %s:%d\n' % addr) - server.runservice( bytesvars(opts), - initfn=init, - runfn=run, + runfn=lambda: run(opts.address, opts.port, opts.certificate), runargs=[pycompat.sysexecutable, pycompat.fsencode(__file__)] + pycompat.sysargv[1:], logfile=opts.logfile,
--- a/tests/test-patchbomb-tls.t Mon Jun 26 16:45:13 2023 +0200 +++ b/tests/test-patchbomb-tls.t Thu Mar 23 16:45:12 2023 +0100 @@ -7,7 +7,6 @@ $ "$PYTHON" "$TESTDIR/dummysmtpd.py" -p $HGPORT --pid-file a.pid --logfile log -d \ > --tls smtps --certificate `pwd`/server.pem - listening at localhost:$HGPORT (?) $ cat a.pid >> $DAEMON_PIDS Set up repository: @@ -47,6 +46,11 @@ (an attempt was made to load CA certificates but none were loaded; see https://mercurial-scm.org/wiki/SecureConnections for how to configure Mercurial to avoid this error) (?i)abort: .*?certificate.verify.failed.* (re) [255] + + $ cat ../log + * ssl error: * (glob) + $ : > ../log + #endif #if defaultcacertsloaded @@ -58,6 +62,10 @@ (?i)abort: .*?certificate.verify.failed.* (re) [255] + $ cat ../log + * ssl error: * (glob) + $ : > ../log + #endif $ DISABLECACERTS="--config devel.disableloaddefaultcerts=true" @@ -76,7 +84,8 @@ [150] $ cat ../log - * ssl error: * (glob) + connection from * (glob) + no hello: b'' $ : > ../log With global certificates: @@ -91,6 +100,7 @@ sending [PATCH] a ... $ cat ../log + connection from * (glob) * from=quux to=foo, bar (glob) MIME-Version: 1.0 Content-Type: text/plain; charset="us-ascii"