diff --git a/scripts/test.sh b/scripts/test.sh index 7df894ac..6889c407 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,4 +1,4 @@ #!/bin/bash venv/bin/tox -c chatmaild venv/bin/tox -c deploy-chatmail -venv/bin/pytest tests/online -vrx --durations=5 $@ +venv/bin/pytest tests/online -rs -vrx --durations=5 $@ diff --git a/tests/chatmaild/test_dictproxy.py b/tests/chatmaild/test_dictproxy.py index 0eec33cb..409eeb7d 100644 --- a/tests/chatmaild/test_dictproxy.py +++ b/tests/chatmaild/test_dictproxy.py @@ -1,21 +1,15 @@ -import os import json import pytest +import threading +import queue +import traceback import chatmaild.dictproxy from chatmaild.dictproxy import get_user_data, lookup_passdb, handle_dovecot_request from chatmaild.database import Database, DBError -@pytest.fixture() -def db(tmpdir): - db_path = tmpdir / "passdb.sqlite" - print("database path:", db_path) - return Database(db_path) - - - def test_basic(db): lookup_passdb(db, "link2xt@c1.testrun.org", "Pieg9aeToe3eghuthe5u") data = get_user_data(db, "link2xt@c1.testrun.org") @@ -53,8 +47,10 @@ def test_too_high_db_version(db): def test_handle_dovecot_request(db): - msg = ('Lshared/passdb/laksjdlaksjdlaksjdlk12j3l1k2j3123/' - 'some42@c3.testrun.org\tsome42@c3.testrun.org') + msg = ( + "Lshared/passdb/laksjdlaksjdlaksjdlk12j3l1k2j3123/" + "some42@c3.testrun.org\tsome42@c3.testrun.org" + ) res = handle_dovecot_request(msg, db, "c3.testrun.org") assert res assert res[0] == "O" and res.endswith("\n") @@ -62,3 +58,29 @@ def test_handle_dovecot_request(db): assert userdata["home"] == "/home/vmail/some42@c3.testrun.org" assert userdata["uid"] == userdata["gid"] == "vmail" assert userdata["password"].startswith("{SHA512-CRYPT}") + + +def test_100_concurrent_lookups(db): + num = 100 + dbs = [Database(db.path) for i in range(num)] + print(f"created {num} databases") + results = queue.Queue() + + def lookup(db): + try: + lookup_passdb(db, "something@c1.testrun.org", "Pieg9aeToe3eghuthe5u") + except Exception: + results.put(traceback.format_exc()) + else: + results.put(None) + + threads = [threading.Thread(target=lookup, args=(db,), daemon=True) for db in dbs] + + print(f"created {num} threads, starting them and waiting for results") + for thread in threads: + thread.start() + + for _ in dbs: + res = results.get() + if res is not None: + pytest.fail(f"concurrent lookup failed\n{res}") diff --git a/tests/conftest.py b/tests/conftest.py index 8d8f0227..b81ab8d4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,9 +9,10 @@ import itertools from email.parser import BytesParser from email import policy from pathlib import Path -from math import ceil import pytest +from chatmaild.database import Database + conftestdir = Path(__file__).parent @@ -71,7 +72,7 @@ def pytest_report_header(): @pytest.fixture def benchmark(request): - def bench(func, num, name=None): + def bench(func, num, name=None, reportfunc=None): if name is None: name = func.__name__ durations = [] @@ -80,7 +81,7 @@ def benchmark(request): func() durations.append(time.time() - now) durations.sort() - request.config._benchresults[name] = durations + request.config._benchresults[name] = (reportfunc, durations) return bench @@ -101,7 +102,9 @@ def pytest_terminal_summary(terminalreporter): headers = f"{'benchmark name': <30} " + fcol(float_names) tr.write_line(headers) tr.write_line("-" * len(headers)) - for name, durations in results.items(): + summary_lines = [] + + for name, (reportfunc, durations) in results.items(): measures = [ sorted(durations)[len(durations) // 2], min(durations), @@ -110,13 +113,31 @@ def pytest_terminal_summary(terminalreporter): line = f"{name: <30} " line += fcol(f"{float: 2.2f}" for float in measures) tr.write_line(line) + vmedian, vmin, vmax = measures + for line in reportfunc(vmin=vmin, vmedian=vmedian, vmax=vmax): + summary_lines.append(line) + if summary_lines: + tr.write_line("") + tr.section("benchmark summary measures") + for line in summary_lines: + tr.write_line(line) @pytest.fixture def imap(maildomain): return ImapConn(maildomain) +@pytest.fixture +def make_imap_connection(maildomain): + def make_imap_connection(): + conn = ImapConn(maildomain) + conn.connect() + return conn + + return make_imap_connection + + class ImapConn: AuthError = imaplib.IMAP4.error logcmd = "journalctl -f -u dovecot" @@ -157,6 +178,16 @@ def smtp(maildomain): return SmtpConn(maildomain) +@pytest.fixture +def make_smtp_connection(maildomain): + def make_smtp_connection(): + conn = SmtpConn(maildomain) + conn.connect() + return conn + + return make_smtp_connection + + class SmtpConn: AuthError = smtplib.SMTPAuthenticationError logcmd = "journalctl -f -t postfix/smtpd -t postfix/smtp -t postfix/lmtp" @@ -202,6 +233,14 @@ def gencreds(maildomain): return lambda domain=None: next(gen(domain)) +@pytest.fixture() +def db(tmpdir): + db_path = tmpdir / "passdb.sqlite" + print("database path:", db_path) + return Database(db_path) + + + # # Delta Chat testplugin re-use # use the cmfactory fixture to get chatmail instance accounts @@ -272,7 +311,7 @@ class Remote: self.sshdomain = sshdomain def iter_output(self, logcmd=""): - getjournal = f"journalctl -f" if not logcmd else logcmd + getjournal = "journalctl -f" if not logcmd else logcmd self.popen = subprocess.Popen( ["ssh", f"root@{self.sshdomain}", getjournal], stdout=subprocess.PIPE, diff --git a/tests/online/test_0_login.py b/tests/online/test_0_login.py index 929c59b0..86319e54 100644 --- a/tests/online/test_0_login.py +++ b/tests/online/test_0_login.py @@ -1,5 +1,6 @@ import pytest -import smtplib +import threading +import queue def test_login_basic_functioning(imap_or_smtp, gencreds, lp): @@ -23,7 +24,7 @@ def test_login_basic_functioning(imap_or_smtp, gencreds, lp): with pytest.raises(imap_or_smtp.AuthError): imap_or_smtp.login(user, password + "wrong") - lp.sec(f"creating users with a short password is not allowed") + lp.sec("creating users with a short password is not allowed") user, _password = gencreds() with pytest.raises(imap_or_smtp.AuthError): imap_or_smtp.login(user, "admin") @@ -40,3 +41,30 @@ def test_login_same_password(imap_or_smtp, gencreds): imap_or_smtp.login(user1, password1) imap_or_smtp.connect() imap_or_smtp.login(user2, password1) + + +def test_concurrent_logins_same_account( + make_imap_connection, make_smtp_connection, gencreds +): + """Test concurrent smtp and imap logins + and check remote server succeeds on each connection. + """ + user1, password1 = gencreds() + login_results = queue.Queue() + + def login_smtp_imap(smtp, imap): + try: + imap.login(user1, password1) + except Exception: + login_results.put(False) + else: + login_results.put(True) + + conns = [(make_smtp_connection(), make_imap_connection()) for i in range(10)] + + for args in conns: + thread = threading.Thread(target=login_smtp_imap, args=args, daemon=True) + thread.start() + + for _ in conns: + assert login_results.get() diff --git a/tests/online/test_2_deltachat.py b/tests/online/test_2_deltachat.py index 8fa358ff..c1802704 100644 --- a/tests/online/test_2_deltachat.py +++ b/tests/online/test_2_deltachat.py @@ -91,7 +91,7 @@ class TestEndToEndDeltaChat: lp.sec("setup encrypted comms between ac1 and ac2 on different instances") qr = ac1.get_setup_contact_qr() - ch = ac2.qr_setup_contact(qr) + ac2.qr_setup_contact(qr) msg = ac2.wait_next_incoming_message() assert "verified" in msg.text