#!/usr/bin/env python3 """ Testing webxdc iroh connectivity If you want to debug iroh at rust-trace/log level set RUST_LOG=iroh_net=trace,iroh_gossip=trace """ import itertools import logging import os import threading from contextlib import contextmanager import pytest from deltachat_rpc_client import EventType # Relays on underscore domains advertise themselves as iroh relay # but serve a self-signed certificate that iroh's TLS stack rejects. # Skipping instead of xfailing keeps the run fast: # these tests only fail after waiting for realtime connections to time out. pytestmark = pytest.mark.skipif( os.environ.get("CHATMAIL_DOMAIN", "").startswith("_"), reason="iroh does not accept the self-signed certificate of an underscore domain", ) @pytest.fixture def path_to_webxdc(request): p = request.path.parent.parent.parent.joinpath("test-data/webxdc/chess.xdc") assert p.exists() return str(p) @pytest.fixture def path_to_large_webxdc(request): p = request.path.parent.parent.parent.joinpath("test-data/webxdc/realtime-check.xdc") assert p.exists() return str(p) def log(msg): logging.info(msg) # payload used to probe/establish realtime connectivity, filtered out by tests SETUP_DATA = b"realtime-setup" def setup_realtime_webxdc(ac1, ac2, path_to_webxdc, wait=True): assert ac1.get_config("webxdc_realtime_enabled") == "1" assert ac2.get_config("webxdc_realtime_enabled") == "1" ac1_ac2_chat = ac1.create_chat(ac2) ac2.create_chat(ac1) # share a webxdc app between ac1 and ac2 ac1_webxdc_msg = ac1_ac2_chat.send_message(text="play", file=path_to_webxdc) ac2_webxdc_msg = ac2.wait_for_incoming_msg() assert ac2_webxdc_msg.get_snapshot().text == "play" # send iroh announcements simultaneously log("sending ac1 -> ac2 realtime advertisement and additional message") ac1_webxdc_msg.send_webxdc_realtime_advertisement() log("sending ac2 -> ac1 realtime advertisement and additional message") ac2_webxdc_msg.send_webxdc_realtime_advertisement() if wait: wait_realtime_connected([(ac1_webxdc_msg, ac2_webxdc_msg)]) return ac1_webxdc_msg, ac2_webxdc_msg @contextmanager def send_realtime_data_forever(msgs, data=None): stop = threading.Event() data = data or [SETUP_DATA] * len(msgs) def thread_run(msg, payload): for i in itertools.count(): msg.send_webxdc_realtime_data(payload(i) if callable(payload) else payload) if stop.wait(1): return for msg_payload in zip(msgs, data, strict=True): threading.Thread(target=thread_run, args=msg_payload, daemon=True).start() try: yield finally: stop.set() def wait_realtime_connected(msg_pairs): with send_realtime_data_forever([sender for sender, _ in msg_pairs]): for _, receiver in msg_pairs: receiver.account.wait_for_realtime_data(receiver.id) def test_realtime_sequentially(acf, path_to_webxdc): """Test two peers trying to establish connection sequentially.""" ac1, ac2 = acf.get_online_accounts(2) ac1.create_chat(ac2) ac2.create_chat(ac1) # share a webxdc app between ac1 and ac2 ac1_webxdc_msg = acf.send_message(from_account=ac1, to_account=ac2, text="play", file=path_to_webxdc) ac2_webxdc_msg = ac2.wait_for_incoming_msg() snapshot = ac2_webxdc_msg.get_snapshot() assert snapshot.text == "play" # send iroh announcements sequentially log("sending ac1 -> ac2 realtime advertisement and additional message") ac1_webxdc_msg.send_webxdc_realtime_advertisement() acf.send_message(from_account=ac1, to_account=ac2, text="ping1") log("waiting for incoming message on ac2") snapshot = ac2.wait_for_incoming_msg().get_snapshot() assert snapshot.text == "ping1" log("sending ac2 -> ac1 realtime advertisement and additional message") ac2_webxdc_msg.send_webxdc_realtime_advertisement() acf.send_message(from_account=ac2, to_account=ac1, text="ping2") log("waiting for incoming message on ac1") snapshot = ac1.wait_for_incoming_msg().get_snapshot() assert snapshot.text == "ping2" log("sending realtime data ac1 -> ac2") # Test that 128 KB of data can be sent in a single message. data = os.urandom(128000) ac1_webxdc_msg.send_webxdc_realtime_data(data) assert ac2.wait_for_realtime_data(ac2_webxdc_msg.id) == data def test_realtime_simultaneously(acf, path_to_webxdc): """Test two peers trying to establish connection simultaneously.""" ac1, ac2 = acf.get_online_accounts(2) setup_realtime_webxdc(ac1, ac2, path_to_webxdc) def test_two_parallel_realtime_simultaneously(acf, path_to_webxdc): """Test two peers trying to establish connection simultaneously.""" ac1, ac2 = acf.get_online_accounts(2) ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc, wait=False) ac1_webxdc_msg2, ac2_webxdc_msg2 = setup_realtime_webxdc(ac1, ac2, path_to_webxdc, wait=False) wait_realtime_connected([(ac1_webxdc_msg, ac2_webxdc_msg), (ac2_webxdc_msg, ac1_webxdc_msg)]) wait_realtime_connected([(ac1_webxdc_msg2, ac2_webxdc_msg2), (ac2_webxdc_msg2, ac1_webxdc_msg2)]) def test_no_duplicate_messages(acf, path_to_webxdc): """Test that messages are received only once.""" ac1, ac2 = acf.get_online_accounts(2) ac1_ac2_chat = ac1.create_chat(ac2) ac1_webxdc_msg = ac1_ac2_chat.send_message(text="webxdc", file=path_to_webxdc) ac2_webxdc_msg = ac2.wait_for_incoming_msg() ac2_webxdc_msg.get_snapshot().chat.accept() assert ac2_webxdc_msg.get_snapshot().text == "webxdc" # Issue a "send" call in parallel with sending advertisement. # Previously due to a bug this caused subscribing to the channel twice. ac2_webxdc_msg.send_webxdc_realtime_data.future(b"foobar") ac2_webxdc_msg.send_webxdc_realtime_advertisement() with send_realtime_data_forever([ac1_webxdc_msg], data=[lambda i: str(i).encode()]): n = int(ac2.wait_for_realtime_data(ac2_webxdc_msg.id).decode()) assert int(ac2.wait_for_realtime_data(ac2_webxdc_msg.id).decode()) > n def test_no_reordering(acf, path_to_webxdc): """Test that sending a lot of realtime messages does not result in reordering.""" ac1, ac2 = acf.get_online_accounts(2) ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc, wait=True) for i in range(200): ac1_webxdc_msg.send_webxdc_realtime_data([i]) for i in range(200): # lingering SETUP_DATA payloads from the wait_realtime_connected() barrier may still arrive while (data := ac2.wait_for_realtime_data(ac2_webxdc_msg.id)) == SETUP_DATA: pass assert data == bytes([i]), "Reordering detected" def test_advertisement_after_chatting(acf, path_to_webxdc): """Test that realtime advertisement is assigned to the correct message after chatting.""" ac1, ac2 = acf.get_online_accounts(2) ac1_ac2_chat = ac1.create_chat(ac2) ac1_webxdc_msg = ac1_ac2_chat.send_message(text="WebXDC", file=path_to_webxdc) ac2_webxdc_msg = ac2.wait_for_incoming_msg() ac2_webxdc_msg_snapshot = ac2_webxdc_msg.get_snapshot() assert ac2_webxdc_msg_snapshot.text == "WebXDC" ac2_webxdc_msg_snapshot.chat.accept() ac1_ac2_chat.send_text("Hello!") ac2_hello_msg = ac2.wait_for_incoming_msg() ac2_hello_msg_snapshot = ac2_hello_msg.get_snapshot() assert ac2_hello_msg_snapshot.text == "Hello!" ac2_hello_msg_snapshot.chat.accept() ac2_webxdc_msg.send_webxdc_realtime_advertisement() event = ac1.wait_for_event(EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED) assert event.msg_id == ac1_webxdc_msg.id def test_realtime_large_webxdc(acf, path_to_large_webxdc): """Tests initializing realtime channel on a large webxdc. This is a regression test for a bug that existed in version 2.42.0. Large webxdc is split into pre- and post- message, and this previously resulted in failure to initialize realtime. """ ac1, ac2 = acf.get_online_accounts(2) ac2.create_chat(ac1) ac1_ac2_chat = ac1.create_chat(ac2) ac1_webxdc_msg = ac1_ac2_chat.send_message(text="realtime check", file=path_to_large_webxdc) # Receive pre-message. ac2_webxdc_msg = ac2.wait_for_incoming_msg() # Receive post-message. ac2_webxdc_msg = ac2.wait_for_msg(EventType.MSGS_CHANGED) ac2_webxdc_msg.send_webxdc_realtime_advertisement() event = ac1.wait_for_event(EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED) assert event.msg_id == ac1_webxdc_msg.id