mirror of
https://github.com/chatmail/relay.git
synced 2026-05-10 07:54:36 +00:00
cleanup: remove CFFI deltachat bindings usage, and consolidate test support with rpc-bindings (#872)
* cleanup: remove CFFI deltachat bindings usage, and consolidate test support with rpc-bindings major simplification: all chatmail fixtures used in the test are now created inside the cmdeploy plugin, and do not inherit anything from other fixture machineries, let alone the legacy deltachat CFFI ones. also fix that pytest report headers show correct chatmail domains under test
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,7 +4,7 @@ __pycache__/
|
|||||||
*$py.class
|
*$py.class
|
||||||
*.swp
|
*.swp
|
||||||
*qr-*.png
|
*qr-*.png
|
||||||
chatmail.ini
|
chatmail*.ini
|
||||||
|
|
||||||
|
|
||||||
# C extensions
|
# C extensions
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ dependencies = [
|
|||||||
"pytest-xdist",
|
"pytest-xdist",
|
||||||
"execnet",
|
"execnet",
|
||||||
"imap_tools",
|
"imap_tools",
|
||||||
|
"deltachat-rpc-client",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ along with command line option and subcommand parsing.
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import importlib.resources
|
import importlib.resources
|
||||||
import importlib.util
|
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import shutil
|
import shutil
|
||||||
@@ -214,14 +213,8 @@ def test_cmd_options(parser):
|
|||||||
|
|
||||||
|
|
||||||
def test_cmd(args, out):
|
def test_cmd(args, out):
|
||||||
"""Run local and online tests for chatmail deployment.
|
"""Run local and online tests for chatmail deployment."""
|
||||||
|
|
||||||
This will automatically pip-install 'deltachat' if it's not available.
|
|
||||||
"""
|
|
||||||
|
|
||||||
x = importlib.util.find_spec("deltachat")
|
|
||||||
if x is None:
|
|
||||||
out.check_call(f"{sys.executable} -m pip install deltachat")
|
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
if args.ssh_host:
|
if args.ssh_host:
|
||||||
env["CHATMAIL_SSH"] = args.ssh_host
|
env["CHATMAIL_SSH"] = args.ssh_host
|
||||||
|
|||||||
@@ -42,9 +42,9 @@ class TestDC:
|
|||||||
|
|
||||||
def dc_ping_pong():
|
def dc_ping_pong():
|
||||||
chat.send_text("ping")
|
chat.send_text("ping")
|
||||||
msg = ac2._evtracker.wait_next_incoming_message()
|
msg = ac2.wait_for_incoming_msg()
|
||||||
msg.chat.send_text("pong")
|
msg.get_snapshot().chat.send_text("pong")
|
||||||
ac1._evtracker.wait_next_incoming_message()
|
ac1.wait_for_incoming_msg()
|
||||||
|
|
||||||
benchmark(dc_ping_pong, 5)
|
benchmark(dc_ping_pong, 5)
|
||||||
|
|
||||||
@@ -56,6 +56,6 @@ class TestDC:
|
|||||||
for i in range(10):
|
for i in range(10):
|
||||||
chat.send_text(f"hello {i}")
|
chat.send_text(f"hello {i}")
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
ac2._evtracker.wait_next_incoming_message()
|
ac2.wait_for_incoming_msg()
|
||||||
|
|
||||||
benchmark(dc_send_10_receive_10, 5, cooldown="auto")
|
benchmark(dc_send_10_receive_10, 5, cooldown="auto")
|
||||||
|
|||||||
@@ -86,10 +86,8 @@ def test_remote(remote, imap_or_smtp):
|
|||||||
|
|
||||||
|
|
||||||
def test_use_two_chatmailservers(cmfactory, maildomain2):
|
def test_use_two_chatmailservers(cmfactory, maildomain2):
|
||||||
ac1 = cmfactory.new_online_configuring_account(cache=False)
|
ac1 = cmfactory.get_online_account()
|
||||||
cmfactory.switch_maildomain(maildomain2)
|
ac2 = cmfactory.get_online_account(domain=maildomain2)
|
||||||
ac2 = cmfactory.new_online_configuring_account(cache=False)
|
|
||||||
cmfactory.bring_accounts_online()
|
|
||||||
cmfactory.get_accepted_chat(ac1, ac2)
|
cmfactory.get_accepted_chat(ac1, ac2)
|
||||||
domain1 = ac1.get_config("addr").split("@")[1]
|
domain1 = ac1.get_config("addr").split("@")[1]
|
||||||
domain2 = ac2.get_config("addr").split("@")[1]
|
domain2 = ac2.get_config("addr").split("@")[1]
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ class TestEndToEndDeltaChat:
|
|||||||
chat.send_text("message0")
|
chat.send_text("message0")
|
||||||
|
|
||||||
lp.sec("wait for ac2 to receive message")
|
lp.sec("wait for ac2 to receive message")
|
||||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
msg2 = ac2.wait_for_incoming_msg()
|
||||||
assert msg2.text == "message0"
|
assert msg2.get_snapshot().text == "message0"
|
||||||
|
|
||||||
def test_exceed_quota(
|
def test_exceed_quota(
|
||||||
self, cmfactory, lp, tmpdir, remote, chatmail_config, sshdomain
|
self, cmfactory, lp, tmpdir, remote, chatmail_config, sshdomain
|
||||||
@@ -110,26 +110,22 @@ class TestEndToEndDeltaChat:
|
|||||||
return
|
return
|
||||||
|
|
||||||
def test_securejoin(self, cmfactory, lp, maildomain2):
|
def test_securejoin(self, cmfactory, lp, maildomain2):
|
||||||
ac1 = cmfactory.new_online_configuring_account(cache=False)
|
ac1 = cmfactory.get_online_account()
|
||||||
cmfactory.switch_maildomain(maildomain2)
|
ac2 = cmfactory.get_online_account(domain=maildomain2)
|
||||||
ac2 = cmfactory.new_online_configuring_account(cache=False)
|
|
||||||
cmfactory.bring_accounts_online()
|
|
||||||
|
|
||||||
lp.sec("ac1: create QR code and let ac2 scan it, starting the securejoin")
|
lp.sec("ac1: create QR code and let ac2 scan it, starting the securejoin")
|
||||||
qr = ac1.get_setup_contact_qr()
|
qr = ac1.get_qr_code()
|
||||||
|
|
||||||
lp.sec("ac2: start QR-code based setup contact protocol")
|
lp.sec("ac2: start QR-code based setup contact protocol")
|
||||||
ch = ac2.qr_setup_contact(qr)
|
ch = ac2.secure_join(qr)
|
||||||
assert ch.id >= 10
|
assert ch.id >= 10
|
||||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
ac1.wait_for_securejoin_inviter_success()
|
||||||
|
|
||||||
def test_dkim_header_stripped(self, cmfactory, maildomain2, lp, imap_mailbox):
|
def test_dkim_header_stripped(self, cmfactory, maildomain2, lp, imap_mailbox):
|
||||||
"""Test that if a DC address receives a message, it has no
|
"""Test that if a DC address receives a message, it has no
|
||||||
DKIM-Signature and Authentication-Results headers."""
|
DKIM-Signature and Authentication-Results headers."""
|
||||||
ac1 = cmfactory.new_online_configuring_account(cache=False)
|
ac1 = cmfactory.get_online_account()
|
||||||
cmfactory.switch_maildomain(maildomain2)
|
ac2 = cmfactory.get_online_account(domain=maildomain2)
|
||||||
ac2 = cmfactory.new_online_configuring_account(cache=False)
|
|
||||||
cmfactory.bring_accounts_online()
|
|
||||||
chat = cmfactory.get_accepted_chat(ac1, imap_mailbox.dc_ac)
|
chat = cmfactory.get_accepted_chat(ac1, imap_mailbox.dc_ac)
|
||||||
chat.send_text("message0")
|
chat.send_text("message0")
|
||||||
chat2 = cmfactory.get_accepted_chat(ac2, imap_mailbox.dc_ac)
|
chat2 = cmfactory.get_accepted_chat(ac2, imap_mailbox.dc_ac)
|
||||||
@@ -146,29 +142,28 @@ class TestEndToEndDeltaChat:
|
|||||||
assert "dkim-signature" not in msg.headers
|
assert "dkim-signature" not in msg.headers
|
||||||
|
|
||||||
def test_read_receipts_between_instances(self, cmfactory, lp, maildomain2):
|
def test_read_receipts_between_instances(self, cmfactory, lp, maildomain2):
|
||||||
ac1 = cmfactory.new_online_configuring_account(cache=False)
|
ac1 = cmfactory.get_online_account()
|
||||||
cmfactory.switch_maildomain(maildomain2)
|
ac2 = cmfactory.get_online_account(domain=maildomain2)
|
||||||
ac2 = cmfactory.new_online_configuring_account(cache=False)
|
|
||||||
cmfactory.bring_accounts_online()
|
|
||||||
|
|
||||||
lp.sec("setup encrypted comms between ac1 and ac2 on different instances")
|
lp.sec("setup encrypted comms between ac1 and ac2 on different instances")
|
||||||
qr = ac1.get_setup_contact_qr()
|
qr = ac1.get_qr_code()
|
||||||
ch = ac2.qr_setup_contact(qr)
|
ch = ac2.secure_join(qr)
|
||||||
assert ch.id >= 10
|
assert ch.id >= 10
|
||||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
ac1.wait_for_securejoin_inviter_success()
|
||||||
|
|
||||||
lp.sec("ac1 sends a message and ac2 marks it as seen")
|
lp.sec("ac1 sends a message and ac2 marks it as seen")
|
||||||
chat = ac1.create_chat(ac2)
|
chat = ac1.create_chat(ac2)
|
||||||
msg = chat.send_text("hi")
|
msg = chat.send_text("hi")
|
||||||
m = ac2._evtracker.wait_next_incoming_message()
|
m = ac2.wait_for_incoming_msg()
|
||||||
m.mark_seen()
|
m.mark_seen()
|
||||||
# we can only indirectly wait for mark-seen to cause an smtp-error
|
# we can only indirectly wait for mark-seen to cause an smtp-error
|
||||||
lp.sec("try to wait for markseen to complete and check error states")
|
lp.sec("try to wait for markseen to complete and check error states")
|
||||||
deadline = time.time() + 3.1
|
deadline = time.time() + 3.1
|
||||||
while time.time() < deadline:
|
while time.time() < deadline:
|
||||||
msgs = m.chat.get_messages()
|
m_snap = m.get_snapshot()
|
||||||
|
msgs = m_snap.chat.get_messages()
|
||||||
for msg in msgs:
|
for msg in msgs:
|
||||||
assert "error" not in m.get_message_info()
|
assert "error" not in m.get_info()
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
@@ -180,7 +175,7 @@ def test_hide_senders_ip_address(cmfactory, ssl_context):
|
|||||||
chat = cmfactory.get_accepted_chat(user1, user2)
|
chat = cmfactory.get_accepted_chat(user1, user2)
|
||||||
|
|
||||||
chat.send_text("testing submission header cleanup")
|
chat.send_text("testing submission header cleanup")
|
||||||
user2._evtracker.wait_next_incoming_message()
|
user2.wait_for_incoming_msg()
|
||||||
addr = user2.get_config("addr")
|
addr = user2.get_config("addr")
|
||||||
host = addr.split("@")[1]
|
host = addr.split("@")[1]
|
||||||
pw = user2.get_config("mail_pw")
|
pw = user2.get_config("mail_pw")
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import imaplib
|
import imaplib
|
||||||
import io
|
|
||||||
import itertools
|
import itertools
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
@@ -35,17 +34,24 @@ def pytest_runtest_setup(item):
|
|||||||
pytest.skip("skipping slow test, use --slow to run")
|
pytest.skip("skipping slow test, use --slow to run")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
def _get_chatmail_config():
|
||||||
def chatmail_config(pytestconfig):
|
current = Path().resolve()
|
||||||
current = basedir = Path().resolve()
|
|
||||||
while 1:
|
while 1:
|
||||||
path = current.joinpath("chatmail.ini").resolve()
|
path = current.joinpath("chatmail.ini").resolve()
|
||||||
if path.exists():
|
if path.exists():
|
||||||
return read_config(path)
|
return read_config(path), path
|
||||||
if current == current.parent:
|
if current == current.parent:
|
||||||
break
|
break
|
||||||
current = current.parent
|
current = current.parent
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def chatmail_config(pytestconfig):
|
||||||
|
config, path = _get_chatmail_config()
|
||||||
|
if config:
|
||||||
|
return config
|
||||||
|
basedir = Path().resolve()
|
||||||
pytest.skip(f"no chatmail.ini file found in {basedir} or parent dirs")
|
pytest.skip(f"no chatmail.ini file found in {basedir} or parent dirs")
|
||||||
|
|
||||||
|
|
||||||
@@ -73,10 +79,17 @@ def sshdomain2(maildomain2):
|
|||||||
|
|
||||||
|
|
||||||
def pytest_report_header():
|
def pytest_report_header():
|
||||||
domain = os.environ.get("CHATMAIL_DOMAIN")
|
config, path = _get_chatmail_config()
|
||||||
if domain:
|
domain2 = os.environ.get("CHATMAIL_DOMAIN2", "NOT SET")
|
||||||
text = f"chatmail test instance: {domain}"
|
domain = config.mail_domain if config else "NOT SET"
|
||||||
return ["-" * len(text), text, "-" * len(text)]
|
path = path if path else "NOT SET"
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
f"chatmail.ini {domain} location: {path}",
|
||||||
|
f"chatmail2: {domain2}",
|
||||||
|
]
|
||||||
|
sep = "-" * max(map(len, lines))
|
||||||
|
return [sep, *lines, sep]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -283,79 +296,95 @@ def gencreds(chatmail_config):
|
|||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Delta Chat testplugin re-use
|
# Delta Chat RPC-based test support
|
||||||
# use the cmfactory fixture to get chatmail instance accounts
|
# use the cmfactory fixture to get chatmail instance accounts
|
||||||
#
|
#
|
||||||
|
|
||||||
|
from deltachat_rpc_client import DeltaChat, Rpc
|
||||||
|
|
||||||
class ChatmailTestProcess:
|
|
||||||
"""Provider for chatmail instance accounts as used by deltachat.testplugin.acfactory"""
|
|
||||||
|
|
||||||
def __init__(self, pytestconfig, maildomain, gencreds, chatmail_config):
|
class ChatmailACFactory:
|
||||||
self.pytestconfig = pytestconfig
|
"""RPC-based account factory for chatmail testing."""
|
||||||
self.maildomain = maildomain
|
|
||||||
assert "." in self.maildomain, maildomain
|
def __init__(self, rpc, maildomain, gencreds, chatmail_config):
|
||||||
|
self.dc = DeltaChat(rpc)
|
||||||
|
self.rpc = rpc
|
||||||
|
self._maildomain = maildomain
|
||||||
self.gencreds = gencreds
|
self.gencreds = gencreds
|
||||||
self.chatmail_config = chatmail_config
|
self.chatmail_config = chatmail_config
|
||||||
self._addr2files = {}
|
|
||||||
|
|
||||||
def get_liveconfig_producer(self):
|
def _make_transport(self, domain):
|
||||||
while 1:
|
"""Build a transport config dict for the given domain."""
|
||||||
user, password = self.gencreds(self.maildomain)
|
addr, password = self.gencreds(domain)
|
||||||
config = {
|
transport = {
|
||||||
"addr": user,
|
"addr": addr,
|
||||||
"mail_pw": password,
|
"password": password,
|
||||||
}
|
# Setting server explicitly skips requesting autoconfig XML,
|
||||||
# speed up account configuration
|
# see https://datatracker.ietf.org/doc/draft-ietf-mailmaint-autoconfig/
|
||||||
config["mail_server"] = self.maildomain
|
"imapServer": domain,
|
||||||
config["send_server"] = self.maildomain
|
"smtpServer": domain,
|
||||||
if self.chatmail_config.tls_cert_mode == "self":
|
}
|
||||||
# Accept self-signed TLS certificates
|
if self.chatmail_config.tls_cert_mode == "self":
|
||||||
config["imap_certificate_checks"] = "3"
|
transport["certificateChecks"] = "acceptInvalidCertificates"
|
||||||
yield config
|
return transport
|
||||||
|
|
||||||
def cache_maybe_retrieve_configured_db_files(self, cache_addr, db_target_path):
|
def get_online_account(self, domain=None):
|
||||||
pass
|
"""Create, configure and bring online a single account."""
|
||||||
|
return self.get_online_accounts(1, domain)[0]
|
||||||
|
|
||||||
def cache_maybe_store_configured_db_files(self, acc):
|
def get_online_accounts(self, num, domain=None):
|
||||||
pass
|
"""Create multiple online accounts in parallel."""
|
||||||
|
domain = domain or self._maildomain
|
||||||
|
futures = []
|
||||||
|
accounts = []
|
||||||
|
for _ in range(num):
|
||||||
|
account = self.dc.add_account()
|
||||||
|
future = account.add_or_update_transport.future(
|
||||||
|
self._make_transport(domain)
|
||||||
|
)
|
||||||
|
futures.append(future)
|
||||||
|
|
||||||
|
# ensure messages stay in INBOX so that they can be
|
||||||
|
# concurrently fetched via extra IMAP connections during tests
|
||||||
|
account.set_config("delete_server_after", "10")
|
||||||
|
accounts.append(account)
|
||||||
|
|
||||||
|
for future in futures:
|
||||||
|
future()
|
||||||
|
|
||||||
|
for account in accounts:
|
||||||
|
account.bring_online()
|
||||||
|
return accounts
|
||||||
|
|
||||||
|
def get_accepted_chat(self, ac1, ac2):
|
||||||
|
"""Create a 1:1 chat between ac1 and ac2 accepted on both sides."""
|
||||||
|
ac2.create_chat(ac1)
|
||||||
|
return ac1.create_chat(ac2)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def rpc(tmp_path_factory):
|
||||||
|
"""Start a deltachat-rpc-server process for the test session."""
|
||||||
|
|
||||||
|
# NB: accounts_dir must NOT already exist as directory --
|
||||||
|
# core-rust only creates accounts.toml if the dir doesn't exist yet.
|
||||||
|
accounts_dir = str(tmp_path_factory.mktemp("dc") / "accounts")
|
||||||
|
rpc = Rpc(accounts_dir=accounts_dir)
|
||||||
|
rpc.start()
|
||||||
|
yield rpc
|
||||||
|
rpc.close()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def cmfactory(request, gencreds, tmpdir, maildomain, chatmail_config):
|
def cmfactory(rpc, gencreds, maildomain, chatmail_config):
|
||||||
# cloned from deltachat.testplugin.amfactory
|
"""Return a ChatmailACFactory for creating online Delta Chat accounts."""
|
||||||
pytest.importorskip("deltachat")
|
return ChatmailACFactory(
|
||||||
from deltachat.testplugin import ACFactory
|
rpc=rpc,
|
||||||
|
maildomain=maildomain,
|
||||||
testproc = ChatmailTestProcess(
|
gencreds=gencreds,
|
||||||
request.config, maildomain, gencreds, chatmail_config
|
chatmail_config=chatmail_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
class Data:
|
|
||||||
def read_path(self, path):
|
|
||||||
return
|
|
||||||
|
|
||||||
am = ACFactory(request=request, tmpdir=tmpdir, testprocess=testproc, data=Data())
|
|
||||||
|
|
||||||
# Skip upstream's init_imap to prevent extra imap connections not
|
|
||||||
# needed for relay testing
|
|
||||||
am._acsetup.init_imap = lambda acc: None
|
|
||||||
|
|
||||||
# nb. a bit hacky
|
|
||||||
# would probably be better if deltachat's test machinery grows native support
|
|
||||||
def switch_maildomain(maildomain2):
|
|
||||||
am.testprocess.maildomain = maildomain2
|
|
||||||
|
|
||||||
am.switch_maildomain = switch_maildomain
|
|
||||||
|
|
||||||
yield am
|
|
||||||
if hasattr(request.node, "rep_call") and request.node.rep_call.failed:
|
|
||||||
if testproc.pytestconfig.getoption("--extra-info"):
|
|
||||||
logfile = io.StringIO()
|
|
||||||
am.dump_imap_summary(logfile=logfile)
|
|
||||||
print(logfile.getvalue())
|
|
||||||
# request.node.add_report_section("call", "imap-server-state", s)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def remote(sshdomain):
|
def remote(sshdomain):
|
||||||
|
|||||||
Reference in New Issue
Block a user