Compare commits

...

5 Commits

Author SHA1 Message Date
holger krekel
307ff9def1 fix(rpc): avoid hang when requests race a dying rpc-server
Requests now register before testing for shutdown,
so either the reader loop or the caller answers them.
Also failed start() winds down its threads, ignoring a broken pipe on stdin.
2026-08-17 21:07:19 +02:00
holger krekel
0119db85ad test!: rename rpc fixtures to disambiguate from ffi fixtures
fixes https://github.com/chatmail/core/issues/8583
2026-08-14 16:44:50 +02:00
Hocuri
b1c3615431 chore: Add script to show the sizes of futures (async Rust) (#8536)
Add the script from
https://github.com/chatmail/core/pull/8345/changes#r3696015000 with a
few small tweaks

TODO: Add to readme
2026-08-14 14:09:10 +02:00
link2xt
aa95d87568 chore: bump version to 2.60.0-dev 2026-08-14 12:02:17 +00:00
biørn
07e18c64af sanitize version_string we got from the wire (#8582)
`version_string` is meant to be displayed by UI, and comes from the
wire.

therefore, as a general precaution, ensure a string that is
regarded as a typical version string. all versions in scope are
currently v123.456.789-shortsuffix, where suffix is a-z and mostly
unused in production. that is the base. we can adapt if there is really
a need, but not for theoretical version strings. as we do not stop
processing, things are not bad even if we missed a valid usecase herr.

moreover, if `version_string` is empty, we skip the candidate - as we
cannot display something useful to the user. that little bit of care is
expected from relays :)
2026-08-14 12:01:15 +00:00
30 changed files with 429 additions and 295 deletions

10
Cargo.lock generated
View File

@@ -1328,7 +1328,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "2.59.0"
version = "2.60.0-dev"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -1436,7 +1436,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "2.59.0"
version = "2.60.0-dev"
dependencies = [
"anyhow",
"async-channel 2.5.0",
@@ -1457,7 +1457,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "2.59.0"
version = "2.60.0-dev"
dependencies = [
"anyhow",
"deltachat",
@@ -1473,7 +1473,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "2.59.0"
version = "2.60.0-dev"
dependencies = [
"anyhow",
"deltachat",
@@ -1502,7 +1502,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "2.59.0"
version = "2.60.0-dev"
dependencies = [
"anyhow",
"deltachat",

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "2.59.0"
version = "2.60.0-dev"
edition = "2024"
license = "MPL-2.0"
rust-version = "1.89"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "2.59.0"
version = "2.60.0-dev"
description = "Deltachat FFI"
edition = "2024"
readme = "README.md"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "2.59.0"
version = "2.60.0-dev"
description = "DeltaChat JSON-RPC API"
edition = "2024"
license = "MPL-2.0"

View File

@@ -9,7 +9,9 @@ pub struct JsonrpcAppSource {
/// Always increasing version number.
pub version_integer: u32,
/// Any version string.
/// Version string that should be shown to the user.
/// UI must not linkify the string
/// as it may be interpreted like a phone number or an IP address.
pub version_string: String,
/// Where to download that version.

View File

@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "2.59.0"
"version": "2.60.0-dev"
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "2.59.0"
version = "2.60.0-dev"
license = "MPL-2.0"
edition = "2024"
repository = "https://github.com/chatmail/core"

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "2.59.0"
version = "2.60.0-dev"
license = "MPL-2.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [

View File

@@ -12,7 +12,7 @@ import subprocess
import sys
import time
import urllib.parse
from typing import AsyncGenerator, Optional
from typing import Iterator, Optional
import pytest
@@ -62,7 +62,7 @@ def pytest_report_header():
return headers
class ACFactory:
class RPCAccountFactory:
"""Test account factory."""
def __init__(self, deltachat: DeltaChat) -> None:
@@ -175,7 +175,7 @@ class ACFactory:
@pytest.fixture
def rpc(tmp_path) -> AsyncGenerator:
def rpc(tmp_path) -> Iterator[Rpc]:
"""RPC client fixture."""
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
with rpc_server:
@@ -189,13 +189,13 @@ def dc(rpc) -> DeltaChat:
@pytest.fixture
def acfactory(dc) -> AsyncGenerator:
def acf(dc) -> RPCAccountFactory:
"""Return account factory fixture."""
return ACFactory(dc)
return RPCAccountFactory(dc)
@pytest.fixture
def data():
def rpcdata():
"""Test data."""
class Data:
@@ -292,7 +292,7 @@ def get_core_python_env(tmp_path_factory):
@pytest.fixture
def alice_and_remote_bob(tmp_path, acfactory, get_core_python_env):
def alice_and_remote_bob(tmp_path, acf, get_core_python_env):
"""return local Alice account, a contact to bob, and a remote 'eval' function for bob.
The 'eval' function allows to remote-execute arbitrary expressions
@@ -309,7 +309,7 @@ def alice_and_remote_bob(tmp_path, acfactory, get_core_python_env):
# old cores need "ic=3" to accept
# the self-signed cert of an underscore domain
addr, password = acfactory.get_credentials()
addr, password = acf.get_credentials()
dclogin_qr = f"dclogin://{urllib.parse.quote(addr, safe='@')}?p={urllib.parse.quote(password)}&v=1"
if os.environ["CHATMAIL_DOMAIN"].startswith("_"):
dclogin_qr += "&ic=3"
@@ -318,7 +318,7 @@ def alice_and_remote_bob(tmp_path, acfactory, get_core_python_env):
channel.send((accounts_dir, str(rpc_server_path), dclogin_qr))
# meanwhile get a local alice account
alice = acfactory.get_online_account()
alice = acf.get_online_account()
channel.send(alice.self_contact.make_vcard())
# wait for bob to have started
@@ -360,7 +360,7 @@ def remote_bob_loop(channel):
dc = DeltaChat(rpc)
channel.send(dc.rpc.get_system_info()["deltachat_core_version"])
# ACFactory would configure from a "dcaccount" QR,
# RPCAccountFactory would configure from a "dcaccount" QR,
# which old cores cannot use on underscore domains
bob = dc.add_account()
bob.add_transport_from_qr(dclogin_qr)

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import contextlib
import itertools
import json
import logging
@@ -38,8 +39,15 @@ class RpcMethod:
"params": args,
"id": request_id,
}
self.rpc.request_results[request_id] = queue = Queue()
self.rpc.request_queue.put(request)
queue: Queue = Queue()
# Register before testing for shutdown, so that either the reader loop
# finds this request while draining, or the test below catches it here.
# Testing first would race with the reader loop finishing in between.
self.rpc.request_results[request_id] = queue
if self.rpc.request_queue_closed:
self.rpc._fail_request(request_id)
else:
self.rpc.request_queue.put(request)
def rpc_future():
"""Wait for the request to receive a result."""
@@ -78,6 +86,10 @@ class Rpc:
# Map from request ID to a Queue which provides a single result
self.request_results: dict[int, Queue]
self.request_queue: Queue[Any]
# Emulates `request_queue.shutdown(immediate=False)`, which needs Python 3.13:
# https://github.com/python/cpython/blob/v3.13.0/Lib/queue.py#L236-L257
# Note that `request_queue_closed` is set by the reader loop.
self.request_queue_closed: bool
self.closing: bool
self.reader_thread: Thread
self.writer_thread: Thread
@@ -107,6 +119,7 @@ class Rpc:
self.event_queues = {}
self.request_results = {}
self.request_queue = Queue()
self.request_queue_closed = False
self.closing = False
self.reader_thread = Thread(target=self.reader_loop)
self.reader_thread.start()
@@ -123,6 +136,8 @@ class Rpc:
# The reader_loop already saw EOF on stdout, so the process
# has exited and stderr is available.
stderr = self.process.stderr.read().decode(errors="replace").strip()
self.closing = True
self._shutdown_loops()
if stderr:
raise JsonRpcError(f"RPC server failed to start: {stderr}") from e
raise JsonRpcError(f"RPC server startup check failed: {e}") from e
@@ -135,11 +150,31 @@ class Rpc:
"""Terminate RPC server process and wait until the reader loop finishes."""
self.closing = True
self.stop_io_for_all_accounts()
# Let `events_loop` stop cleanly on `closing` before the pipe goes away,
# otherwise it might exit through an "RPC server closed" error instead.
self.events_thread.join()
self.process.stdin.close()
self.reader_thread.join()
self._shutdown_loops()
def _shutdown_loops(self) -> None:
"""Close the server pipe and wait for the loop threads to finish.
The writer blocks on an empty request queue,
so it needs the sentinel to notice the shutdown.
"""
with contextlib.suppress(BrokenPipeError):
# An exited server may leave data unflushed,
# which close() would try to write out again.
self.process.stdin.close()
self.request_queue.put(None)
self.reader_thread.join()
self.writer_thread.join()
self.events_thread.join()
def _fail_request(self, request_id: int) -> None:
"""Answer a registered request with an error, unless it was answered already."""
queue = self.request_results.pop(request_id, None)
if queue is not None:
queue.put({"error": {"code": -32000, "message": "RPC server closed"}})
def __enter__(self):
self.start()
@@ -162,9 +197,11 @@ class Rpc:
# Log an exception if the reader loop dies.
logging.exception("Exception in the reader loop")
finally:
# Unblock any pending requests when the server closes stdout.
for _request_id, queue in self.request_results.items():
queue.put({"error": {"code": -32000, "message": "RPC server closed"}})
# Shut the request queue first, so that requests registered from now
# on are failed by their caller, then answer the pending ones here.
self.request_queue_closed = True
for request_id in list(self.request_results):
self._fail_request(request_id)
def writer_loop(self) -> None:
"""Writer loop ensuring only a single thread writes requests."""

View File

@@ -5,16 +5,16 @@ from typing import TYPE_CHECKING
from deltachat_rpc_client import EventType
if TYPE_CHECKING:
from deltachat_rpc_client.pytestplugin import ACFactory
from deltachat_rpc_client.pytestplugin import RPCAccountFactory
def test_event_on_configuration(acfactory: ACFactory) -> None:
def test_event_on_configuration(acf: RPCAccountFactory) -> None:
"""
Test if ACCOUNTS_ITEM_CHANGED event is emitted on configure
"""
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
addr, password = acf.get_credentials()
account = acf.get_unconfigured_account()
account.clear_all_events()
assert not account.is_configured()
future = account.add_or_update_transport.future({"addr": addr, "password": password})

View File

@@ -1,8 +1,8 @@
from deltachat_rpc_client import EventType, Message
def test_calls(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
def test_calls(acf) -> None:
alice, bob = acf.get_online_accounts(2)
place_call_info = "offer"
accept_call_info = "answer"
@@ -35,14 +35,14 @@ def test_calls(acfactory) -> None:
assert incoming_call_message.get_call_info().state.kind == "Completed"
def test_video_call(acfactory) -> None:
def test_video_call(acf) -> None:
# Example from <https://datatracker.ietf.org/doc/rfc9143/>
# with `s= ` replaced with `s=-`.
#
# `s=` cannot be empty according to RFC 3264,
# so it is more clear as `s=-`.
alice, bob = acfactory.get_online_accounts(2)
alice, bob = acf.get_online_accounts(2)
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
alice_contact_bob = alice.create_contact(bob, "Bob")
@@ -57,8 +57,8 @@ def test_video_call(acfactory) -> None:
assert incoming_call_message.get_call_info().has_video
def test_audio_call(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
def test_audio_call(acf) -> None:
alice, bob = acf.get_online_accounts(2)
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
alice_contact_bob = alice.create_contact(bob, "Bob")
@@ -73,15 +73,15 @@ def test_audio_call(acfactory) -> None:
assert not incoming_call_message.get_call_info().has_video
def test_ice_servers(acfactory) -> None:
alice = acfactory.get_online_account()
def test_ice_servers(acf) -> None:
alice = acf.get_online_account()
ice_servers = alice.ice_servers()
assert len(ice_servers) == 1
def test_no_contact_request_call(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
def test_no_contact_request_call(acf) -> None:
alice, bob = acf.get_online_accounts(2)
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
@@ -101,8 +101,8 @@ def test_no_contact_request_call(acfactory) -> None:
break
def test_who_can_call_me_nobody(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
def test_who_can_call_me_nobody(acf) -> None:
alice, bob = acf.get_online_accounts(2)
# Bob sets "who can call me" to "nobody" (2)
bob.set_config("who_can_call_me", "2")
@@ -128,9 +128,9 @@ def test_who_can_call_me_nobody(acfactory) -> None:
break
def test_who_can_call_me_everybody(acfactory) -> None:
def test_who_can_call_me_everybody(acf) -> None:
"""Test that if "who can call me" setting is set to "everybody", calls arrive even in contact request chats."""
alice, bob = acfactory.get_online_accounts(2)
alice, bob = acf.get_online_accounts(2)
# Bob sets "who can call me" to "nobody" (0)
bob.set_config("who_can_call_me", "0")

View File

@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
from deltachat_rpc_client import Account, EventType, const
if TYPE_CHECKING:
from deltachat_rpc_client.pytestplugin import ACFactory
from deltachat_rpc_client.pytestplugin import RPCAccountFactory
def wait_for_chatlist_and_specific_item(account, chat_id):
@@ -40,11 +40,11 @@ def wait_for_chatlist(account):
break
def test_delivery_status(acfactory: ACFactory) -> None:
def test_delivery_status(acf: RPCAccountFactory) -> None:
"""
Test change status on chatlistitem when status changes (delivered, read)
"""
alice, bob = acfactory.get_online_accounts(2)
alice, bob = acf.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
@@ -82,11 +82,11 @@ def test_delivery_status(acfactory: ACFactory) -> None:
assert chat_item["summaryStatus"] == const.MessageState.OUT_MDN_RCVD
def test_delivery_status_failed(acfactory: ACFactory) -> None:
def test_delivery_status_failed(acf: RPCAccountFactory) -> None:
"""
Test change status on chatlistitem when status changes failed
"""
(alice,) = acfactory.get_online_accounts(1)
(alice,) = acf.get_online_accounts(1)
alice.set_config("force_encryption", "0")
invalid_contact = alice.create_contact("example@example.com", "invalid address")
@@ -110,12 +110,12 @@ def test_delivery_status_failed(acfactory: ACFactory) -> None:
assert failing_message.get_snapshot().state == const.MessageState.OUT_FAILED
def test_download_on_demand(acfactory: ACFactory, data) -> None:
def test_download_on_demand(acf: RPCAccountFactory, rpcdata) -> None:
"""
Test if download on demand emits chatlist update events.
This is only needed for last message in chat, but finding that out is too expensive, so it's always emitted
"""
alice, bob = acfactory.get_online_accounts(2)
alice, bob = acf.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
@@ -128,7 +128,7 @@ def test_download_on_demand(acfactory: ACFactory, data) -> None:
msg.get_snapshot().chat.accept()
bob.get_chat_by_id(chat_id).send_message(
"Hello World, this message is bigger than 5 bytes",
file=data.get_path("image/screenshot.jpg"),
file=rpcdata.get_path("image/screenshot.jpg"),
)
message = alice.wait_for_incoming_msg()
@@ -144,8 +144,8 @@ def test_download_on_demand(acfactory: ACFactory, data) -> None:
wait_for_chatlist_specific_item(alice, chat_id)
def get_multi_account_test_setup(acfactory: ACFactory) -> [Account, Account, Account]:
alice, bob = acfactory.get_online_accounts(2)
def get_multi_account_test_setup(acf: RPCAccountFactory) -> [Account, Account, Account]:
alice, bob = acf.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
@@ -161,12 +161,12 @@ def get_multi_account_test_setup(acfactory: ACFactory) -> [Account, Account, Acc
return [alice, alice_second_device, bob, alice_chat_bob]
def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
def test_imap_sync_seen_msgs(acf: RPCAccountFactory) -> None:
"""
Test that chatlist changed events are emitted for the second device
when the message is marked as read on the first device
"""
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acf)
bob.create_chat(alice)
@@ -191,11 +191,11 @@ def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
wait_for_chatlist_specific_item(alice, alice_chat_bob.id)
def test_multidevice_sync_chat(acfactory: ACFactory) -> None:
def test_multidevice_sync_chat(acf: RPCAccountFactory) -> None:
"""
Test multidevice sync: syncing chat visibility and muting across multiple devices
"""
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acf)
alice_chat_bob.archive()
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)

View File

@@ -16,7 +16,7 @@ def test_install_venv_and_use_other_core(tmp_path, get_core_python_env):
@pytest.mark.parametrize("version", ["2.24.0"])
def test_qr_setup_contact(acfactory, alice_and_remote_bob, version) -> None:
def test_qr_setup_contact(acf, alice_and_remote_bob, version) -> None:
"""Test other-core Bob profile can do securejoin with Alice on current core."""
alice, alice_contact_bob, remote_eval = alice_and_remote_bob(version)
@@ -36,7 +36,7 @@ def test_qr_setup_contact(acfactory, alice_and_remote_bob, version) -> None:
# Test that Bob can also scan a QR code
# of Alice for which the key is not known yet.
# For the test above Bob already knew the key from a vCard.
alice2 = acfactory.get_online_account()
alice2 = acf.get_online_account()
qr_code = alice2.get_qr_code()
remote_eval(f"bob.secure_join({qr_code!r})")
remote_eval("bob.wait_for_securejoin_joiner_success()")
@@ -53,13 +53,13 @@ def test_send_and_receive_message(alice_and_remote_bob) -> None:
assert msg.get_snapshot().text == "hello"
def test_second_device(acfactory, alice_and_remote_bob) -> None:
def test_second_device(acf, alice_and_remote_bob) -> None:
"""Test setting up current version as a second device for old version."""
_alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.23.0")
remote_eval("locals().setdefault('future', bob._rpc.provide_backup.future(bob.id))")
qr = remote_eval("bob._rpc.get_backup_qr(bob.id)")
new_account = acfactory.get_unconfigured_account()
new_account = acf.get_unconfigured_account()
new_account._rpc.get_backup(new_account.id, qr)
remote_eval("locals()['future']()")

View File

@@ -5,12 +5,12 @@ from imap_tools import AND, U
from deltachat_rpc_client import EventType
def test_moved_markseen(acfactory, direct_imap, log):
def test_moved_markseen(acf, direct_imap, log):
"""Test that message already moved to DeltaChat folder is marked as seen."""
ac1 = acfactory.get_online_account()
ac1 = acf.get_online_account()
addr, password = acfactory.get_credentials()
ac2 = acfactory.get_unconfigured_account()
addr, password = acf.get_credentials()
ac2 = acf.get_unconfigured_account()
ac2.add_or_update_transport({"addr": addr, "password": password})
ac2.bring_online()
@@ -57,14 +57,14 @@ def test_moved_markseen(acfactory, direct_imap, log):
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True, uid=U(1, "*")), mark_seen=False))) == 1
def test_markseen_message_and_mdn(acfactory, direct_imap):
ac1, ac2 = acfactory.get_online_accounts(2)
def test_markseen_message_and_mdn(acf, direct_imap):
ac1, ac2 = acf.get_online_accounts(2)
# Make sure that messages are not immediately auto-deleted on the server:
ac1.set_config("bcc_self", "1")
ac2.set_config("bcc_self", "1")
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
acf.get_accepted_chat(ac1, ac2).send_text("hi")
msg = ac2.wait_for_incoming_msg()
msg.mark_seen()
@@ -91,8 +91,8 @@ def test_markseen_message_and_mdn(acfactory, direct_imap):
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 2
def test_trash_multiple_messages(acfactory, direct_imap, log):
ac1, ac2 = acfactory.get_online_accounts(2)
def test_trash_multiple_messages(acf, direct_imap, log):
ac1, ac2 = acf.get_online_accounts(2)
ac2.stop_io()
# Make sure that messages are not immediately auto-deleted on the server:
@@ -101,7 +101,7 @@ def test_trash_multiple_messages(acfactory, direct_imap, log):
ac2.set_config("sync_msgs", "0")
ac2.start_io()
chat12 = acfactory.get_accepted_chat(ac1, ac2)
chat12 = acf.get_accepted_chat(ac1, ac2)
log.section("ac1: sending 3 messages")
texts = ["first", "second", "third"]

View File

@@ -96,14 +96,14 @@ def wait_realtime_connected(msg_pairs):
receiver.account.wait_for_realtime_data(receiver.id)
def test_realtime_sequentially(acfactory, path_to_webxdc):
def test_realtime_sequentially(acf, path_to_webxdc):
"""Test two peers trying to establish connection sequentially."""
ac1, ac2 = acfactory.get_online_accounts(2)
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 = acfactory.send_message(from_account=ac1, to_account=ac2, text="play", file=path_to_webxdc)
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"
@@ -111,7 +111,7 @@ def test_realtime_sequentially(acfactory, path_to_webxdc):
# send iroh announcements sequentially
log("sending ac1 -> ac2 realtime advertisement and additional message")
ac1_webxdc_msg.send_webxdc_realtime_advertisement()
acfactory.send_message(from_account=ac1, to_account=ac2, text="ping1")
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()
@@ -119,7 +119,7 @@ def test_realtime_sequentially(acfactory, path_to_webxdc):
log("sending ac2 -> ac1 realtime advertisement and additional message")
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
acfactory.send_message(from_account=ac2, to_account=ac1, text="ping2")
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()
@@ -133,24 +133,24 @@ def test_realtime_sequentially(acfactory, path_to_webxdc):
assert ac2.wait_for_realtime_data(ac2_webxdc_msg.id) == data
def test_realtime_simultaneously(acfactory, path_to_webxdc):
def test_realtime_simultaneously(acf, path_to_webxdc):
"""Test two peers trying to establish connection simultaneously."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1, ac2 = acf.get_online_accounts(2)
setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
def test_two_parallel_realtime_simultaneously(acfactory, path_to_webxdc):
def test_two_parallel_realtime_simultaneously(acf, path_to_webxdc):
"""Test two peers trying to establish connection simultaneously."""
ac1, ac2 = acfactory.get_online_accounts(2)
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(acfactory, path_to_webxdc):
def test_no_duplicate_messages(acf, path_to_webxdc):
"""Test that messages are received only once."""
ac1, ac2 = acfactory.get_online_accounts(2)
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)
@@ -169,9 +169,9 @@ def test_no_duplicate_messages(acfactory, path_to_webxdc):
assert int(ac2.wait_for_realtime_data(ac2_webxdc_msg.id).decode()) > n
def test_no_reordering(acfactory, path_to_webxdc):
def test_no_reordering(acf, path_to_webxdc):
"""Test that sending a lot of realtime messages does not result in reordering."""
ac1, ac2 = acfactory.get_online_accounts(2)
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):
@@ -184,9 +184,9 @@ def test_no_reordering(acfactory, path_to_webxdc):
assert data == bytes([i]), "Reordering detected"
def test_advertisement_after_chatting(acfactory, path_to_webxdc):
def test_advertisement_after_chatting(acf, path_to_webxdc):
"""Test that realtime advertisement is assigned to the correct message after chatting."""
ac1, ac2 = acfactory.get_online_accounts(2)
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()
@@ -205,14 +205,14 @@ def test_advertisement_after_chatting(acfactory, path_to_webxdc):
assert event.msg_id == ac1_webxdc_msg.id
def test_realtime_large_webxdc(acfactory, path_to_large_webxdc):
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 = acfactory.get_online_accounts(2)
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)

View File

@@ -1,15 +1,15 @@
def test_set_location(dc, acfactory) -> None:
def test_set_location(dc, acf) -> None:
# Try setting location without any accounts.
assert not dc.set_location(1.0, 2.0, 0.1)
# Create one account that does not stream,
# set location.
acfactory.new_configured_account()
acf.new_configured_account()
assert not dc.set_location(3.0, 4.0, 0.1)
def test_send_locations_to_chat(dc, acfactory):
alice, bob = acfactory.get_online_accounts(2)
def test_send_locations_to_chat(dc, acf):
alice, bob = acf.get_online_accounts(2)
assert not alice.is_sending_locations()
alice_chat_bob = alice.create_chat(bob)

View File

@@ -4,8 +4,8 @@ from deltachat_rpc_client import EventType
from deltachat_rpc_client.const import MessageState
def test_bcc_self_is_enabled_when_setting_up_second_device(acfactory):
ac = acfactory.get_online_account()
def test_bcc_self_is_enabled_when_setting_up_second_device(acf):
ac = acf.get_online_account()
# Initially after getting online
# the setting bcc_self is set to 0 because there is only one device
@@ -29,8 +29,8 @@ def test_bcc_self_is_enabled_when_setting_up_second_device(acfactory):
assert ac.get_config("bcc_self") == "1"
def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
ac1, ac2 = acfactory.get_online_accounts(2)
def test_one_account_send_bcc_setting(acf, log, direct_imap):
ac1, ac2 = acf.get_online_accounts(2)
ac1_clone = ac1.clone()
ac1_clone.bring_online()
@@ -75,9 +75,9 @@ def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True)))) == 1
def test_multidevice_sync_seen(acfactory, log):
def test_multidevice_sync_seen(acf, log):
"""Test that message marked as seen on one device is marked as seen on another."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1, ac2 = acf.get_online_accounts(2)
ac1_clone = ac1.clone()
ac1_clone.bring_online()
@@ -129,9 +129,9 @@ def test_multidevice_sync_seen(acfactory, log):
assert "Expires: " in ac1_clone_message.get_info()
def test_multidevice_sync_seen_mdns_off(acfactory, log):
def test_multidevice_sync_seen_mdns_off(acf, log):
"""Test that MDNs to self are sent even if MDNs are disabled."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1, ac2 = acf.get_online_accounts(2)
ac1.set_config("mdns_enabled", "0")
ac1_clone = ac1.clone()

View File

@@ -5,11 +5,11 @@ from deltachat_rpc_client.const import ChatType, DownloadState
from deltachat_rpc_client.rpc import JsonRpcError
def test_add_second_address(acfactory) -> None:
account = acfactory.new_configured_account()
def test_add_second_address(acf) -> None:
account = acf.new_configured_account()
assert len(account.list_transports()) == 1
qr = acfactory.get_account_qr()
qr = acf.get_account_qr()
account.add_transport_from_qr(qr)
assert len(account.list_transports()) == 2
@@ -27,9 +27,9 @@ def test_add_second_address(acfactory) -> None:
assert len(account.list_transports()) == 2
def test_change_address(acfactory) -> None:
def test_change_address(acf) -> None:
"""Test Alice configuring a second transport and setting it as a primary one."""
alice, bob = acfactory.get_online_accounts(2)
alice, bob = acf.get_online_accounts(2)
bob_addr = bob.get_config("configured_addr")
bob.create_chat(alice)
@@ -44,7 +44,7 @@ def test_change_address(acfactory) -> None:
old_alice_addr = alice.get_config("configured_addr")
alice_vcard = alice.self_contact.make_vcard()
assert old_alice_addr in alice_vcard
qr = acfactory.get_account_qr()
qr = acf.get_account_qr()
alice.add_transport_from_qr(qr)
new_alice_addr = alice.list_transports()[1]["addr"]
with pytest.raises(JsonRpcError):
@@ -76,18 +76,18 @@ def test_change_address(acfactory) -> None:
assert sender_addr2 == new_alice_addr
def test_download_on_demand(acfactory, data) -> None:
alice, bob = acfactory.get_online_accounts(2)
def test_download_on_demand(acf, rpcdata) -> None:
alice, bob = acf.get_online_accounts(2)
alice.set_config("download_limit", "1")
alice.stop_io()
qr = acfactory.get_account_qr()
qr = acf.get_account_qr()
alice.add_transport_from_qr(qr)
alice.start_io()
alice.create_chat(bob)
chat_bob_alice = bob.create_chat(alice)
chat_bob_alice.send_message(file=data.get_path("image/screenshot.jpg"))
chat_bob_alice.send_message(file=rpcdata.get_path("image/screenshot.jpg"))
msg = alice.wait_for_incoming_msg()
snapshot = msg.get_snapshot()
assert snapshot.download_state == DownloadState.AVAILABLE
@@ -103,15 +103,15 @@ def test_download_on_demand(acfactory, data) -> None:
assert msg.get_snapshot().download_state == dstate
def test_reconfigure_transport(acfactory) -> None:
def test_reconfigure_transport(acf) -> None:
"""Test that reconfiguring the transport works."""
account = acfactory.get_online_account()
account = acf.get_online_account()
[transport] = account.list_transports()
account.add_or_update_transport(transport)
def test_transport_synchronization(acfactory, log) -> None:
def test_transport_synchronization(acf, log) -> None:
"""Test synchronization of transports between devices."""
def wait_for_io_started(ac):
@@ -120,11 +120,11 @@ def test_transport_synchronization(acfactory, log) -> None:
if "scheduler is running" in ev.msg:
return
ac1, ac2 = acfactory.get_online_accounts(2)
ac1, ac2 = acf.get_online_accounts(2)
ac1_clone = ac1.clone()
ac1_clone.bring_online()
qr = acfactory.get_account_qr()
qr = acf.get_account_qr()
ac1.add_transport_from_qr(qr)
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
@@ -170,13 +170,13 @@ def test_transport_synchronization(acfactory, log) -> None:
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "Hello!"
def test_transport_sync_new_as_primary(acfactory, log) -> None:
def test_transport_sync_new_as_primary(acf, log) -> None:
"""Test that a transport promoted on one device is usable on other devices."""
ac1, bob = acfactory.get_online_accounts(2)
ac1, bob = acf.get_online_accounts(2)
ac1_clone = ac1.clone()
ac1_clone.bring_online()
qr = acfactory.get_account_qr()
qr = acf.get_account_qr()
ac1.add_transport_from_qr(qr)
ac1_transports = ac1.list_transports()
@@ -202,12 +202,12 @@ def test_transport_sync_new_as_primary(acfactory, log) -> None:
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "hello back"
def test_recognize_self_address(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
def test_recognize_self_address(acf) -> None:
alice, bob = acf.get_online_accounts(2)
bob_chat = bob.create_chat(alice)
qr = acfactory.get_account_qr()
qr = acf.get_account_qr()
alice.add_transport_from_qr(qr)
new_alice_addr = alice.list_transports()[1]["addr"]
@@ -218,10 +218,10 @@ def test_recognize_self_address(acfactory) -> None:
assert msg.chat == alice.create_chat(bob)
def test_transport_limit(acfactory) -> None:
def test_transport_limit(acf) -> None:
"""Test transports limit."""
account = acfactory.get_online_account()
qr = acfactory.get_account_qr()
account = acf.get_online_account()
qr = acf.get_account_qr()
limit = 5
@@ -251,11 +251,11 @@ def test_transport_limit(acfactory) -> None:
account.add_transport_from_qr(qr)
def test_message_info_imap_urls(acfactory) -> None:
def test_message_info_imap_urls(acf) -> None:
"""Test that message info contains IMAP URLs of where the message was received."""
alice, bob = acfactory.get_online_accounts(2)
alice, bob = acf.get_online_accounts(2)
qr = acfactory.get_account_qr()
qr = acf.get_account_qr()
for i in range(3):
alice.add_transport_from_qr(qr)
# Wait for all transports to go IDLE after adding each one.
@@ -290,10 +290,10 @@ def test_message_info_imap_urls(acfactory) -> None:
assert f"{new_alice_addr}/INBOX" in msg_info
def test_remove_primary_transport(acfactory, log) -> None:
def test_remove_primary_transport(acf, log) -> None:
"""Test that after removing the primary relay, Alice can still receive messages."""
alice, bob = acfactory.get_online_accounts(2)
qr = acfactory.get_account_qr()
alice, bob = acf.get_online_accounts(2)
qr = acf.get_account_qr()
alice.add_transport_from_qr(qr)
alice.bring_online()

View File

@@ -7,8 +7,8 @@ from deltachat_rpc_client.const import ChatType
from deltachat_rpc_client.rpc import JsonRpcError
def test_qr_setup_contact(acfactory, tmp_path) -> None:
alice, bob = acfactory.get_online_accounts(2)
def test_qr_setup_contact(acf, tmp_path) -> None:
alice, bob = acf.get_online_accounts(2)
qr_code = alice.get_qr_code()
bob.secure_join(qr_code)
@@ -31,7 +31,7 @@ def test_qr_setup_contact(acfactory, tmp_path) -> None:
# backwards verification is not lost
# because default key is not changed.
logging.info("Bob 2 is created")
bob2 = acfactory.new_configured_account()
bob2 = acf.new_configured_account()
bob2.export_self_keys(tmp_path)
logging.info("Bob tries to import a key")
@@ -44,8 +44,8 @@ def test_qr_setup_contact(acfactory, tmp_path) -> None:
assert bob_contact_alice_snapshot.is_verified
def test_qr_setup_contact_svg(acfactory) -> None:
alice = acfactory.new_configured_account()
def test_qr_setup_contact_svg(acf) -> None:
alice = acf.new_configured_account()
_, _, domain = alice.get_config("addr").rpartition("@")
_qr_code, svg = alice.get_qr_code_svg()
@@ -59,8 +59,8 @@ def test_qr_setup_contact_svg(acfactory) -> None:
assert "Alice" in svg
def test_qr_securejoin(acfactory):
alice, bob, fiona = acfactory.get_online_accounts(3)
def test_qr_securejoin(acf):
alice, bob, fiona = acf.get_online_accounts(3)
# Setup second device for Alice
# to test observing securejoin protocol.
@@ -111,8 +111,8 @@ def test_qr_securejoin(acfactory):
@pytest.mark.parametrize("all_devices_online", [True, False])
def test_qr_securejoin_broadcast(acfactory, all_devices_online):
alice, bob, fiona = acfactory.get_online_accounts(3)
def test_qr_securejoin_broadcast(acf, all_devices_online):
alice, bob, fiona = acf.get_online_accounts(3)
alice2 = alice.clone()
bob2 = bob.clone()
@@ -252,9 +252,9 @@ def test_qr_securejoin_broadcast(acfactory, all_devices_online):
check_account(bob, bob.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
def test_qr_securejoin_contact_request(acfactory) -> None:
def test_qr_securejoin_contact_request(acf) -> None:
"""Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode."""
alice, bob = acfactory.get_online_accounts(2)
alice, bob = acf.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
@@ -278,8 +278,8 @@ def test_qr_securejoin_contact_request(acfactory) -> None:
assert bob_chat_alice.get_basic_snapshot().is_contact_request
def test_qr_readreceipt(acfactory) -> None:
alice, bob, charlie = acfactory.get_online_accounts(3)
def test_qr_readreceipt(acf) -> None:
alice, bob, charlie = acf.get_online_accounts(3)
logging.info("Bob and Charlie setup contact with Alice")
qr_code = alice.get_qr_code()
@@ -335,24 +335,24 @@ def test_qr_readreceipt(acfactory) -> None:
assert not bob.get_chat_by_contact(bob_contact_charlie)
def test_setup_contact_resetup(acfactory) -> None:
def test_setup_contact_resetup(acf) -> None:
"""Tests that setup contact works after Alice resets the device and changes the key."""
alice, bob = acfactory.get_online_accounts(2)
alice, bob = acf.get_online_accounts(2)
qr_code = alice.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
alice = acfactory.resetup_account(alice)
alice = acf.resetup_account(alice)
qr_code = alice.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
def test_verified_group_member_added_recovery(acfactory) -> None:
def test_verified_group_member_added_recovery(acf) -> None:
"""Tests verified group recovery by reverifying then removing and adding a member back."""
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
ac1, ac2, ac3 = acf.get_online_accounts(3)
logging.info("ac1 creates a group")
chat = ac1.create_group("Group")
@@ -374,7 +374,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
ac3_contact_ac2_old = ac3.create_contact(ac2)
logging.info("ac2 logs in on a new device")
ac2 = acfactory.resetup_account(ac2)
ac2 = acf.resetup_account(ac2)
logging.info("ac2 reverifies with ac3")
qr_code = ac3.get_qr_code()
@@ -425,11 +425,11 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
assert ac1_contact_ac2_snapshot.verifier_id != ac1_contact_ac3.id
def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
"""Regression test for
issue <https://github.com/chatmail/core/issues/4894>.
"""
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
ac1, ac2, ac3, ac4 = acf.get_online_accounts(4)
logging.info("ac3: verify with ac2")
qr_code = ac2.get_qr_code()
@@ -484,7 +484,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
return
def test_qr_new_group_unblocked(acfactory):
def test_qr_new_group_unblocked(acf):
"""Regression test for a bug introduced in core v1.113.0.
ac2 scans a verified group QR code created by ac1.
This results in creation of a blocked single chat with ac1 on ac2,
@@ -494,7 +494,7 @@ def test_qr_new_group_unblocked(acfactory):
Due to a bug previously ac2 created a blocked group.
"""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1, ac2 = acf.get_online_accounts(2)
ac1_chat = ac1.create_group("Group for joining")
qr_code = ac1_chat.get_qr_code()
ac2.secure_join(qr_code)
@@ -513,11 +513,11 @@ def test_qr_new_group_unblocked(acfactory):
@pytest.mark.skip(reason="AEAP is disabled for now")
def test_aeap_flow_verified(acfactory):
def test_aeap_flow_verified(acf):
"""Test that a new address is added to a contact when it changes its address."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1, ac2 = acf.get_online_accounts(2)
addr, password = acfactory.get_credentials()
addr, password = acf.get_credentials()
logging.info("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group("hello")
@@ -555,8 +555,8 @@ def test_aeap_flow_verified(acfactory):
assert addr in [contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()]
def test_gossip_verification(acfactory) -> None:
alice, bob, carol = acfactory.get_online_accounts(3)
def test_gossip_verification(acf) -> None:
alice, bob, carol = acf.get_online_accounts(3)
# Bob verifies Alice.
qr_code = alice.get_qr_code()
@@ -605,13 +605,13 @@ def test_gossip_verification(acfactory) -> None:
assert not carol_contact_alice_snapshot.is_verified
def test_securejoin_after_contact_resetup(acfactory) -> None:
def test_securejoin_after_contact_resetup(acf) -> None:
"""
Regression test for a bug that prevented joining verified group with a QR code
if the group is already created and contains
a contact with inconsistent (Autocrypt and verified keys exist but don't match) key state.
"""
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
ac1, ac2, ac3 = acf.get_online_accounts(3)
# ac3 creates protected group with ac1.
ac3_chat = ac3.create_group("Group")
@@ -636,7 +636,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
assert ac2_contact_ac1.get_snapshot().is_verified
# ac1 resetups the account.
ac1 = acfactory.resetup_account(ac1)
ac1 = acf.resetup_account(ac1)
ac2_contact_ac1 = ac2.create_contact(ac1, "")
assert not ac2_contact_ac1.get_snapshot().is_verified
@@ -668,8 +668,8 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
assert not ac2_contact_ac1.get_snapshot().is_verified
def test_withdraw_securejoin_qr(acfactory):
alice, bob = acfactory.get_online_accounts(2)
def test_withdraw_securejoin_qr(acf):
alice, bob = acf.get_online_accounts(2)
logging.info("Alice creates a group")
alice_chat = alice.create_group("Group")
@@ -706,8 +706,8 @@ def test_withdraw_securejoin_qr(acfactory):
break
def test_qr_scan_updates_new_relay_address(acfactory):
alice, bob = acfactory.get_online_accounts(2)
def test_qr_scan_updates_new_relay_address(acf):
alice, bob = acf.get_online_accounts(2)
bob_alice_chat = bob.secure_join(alice.get_qr_code())
alice.wait_for_securejoin_inviter_success()
@@ -715,7 +715,7 @@ def test_qr_scan_updates_new_relay_address(acfactory):
for ac in [alice, bob]:
old_addr = ac.get_config("configured_addr")
ac.add_transport_from_qr(acfactory.get_account_qr())
ac.add_transport_from_qr(acf.get_account_qr())
ac.set_config("configured_addr", ac.list_transports()[1]["addr"])
ac.delete_transport(old_addr)

View File

@@ -48,8 +48,8 @@ def test_email_address_validity(rpc) -> None:
assert not rpc.check_email_validity(addr)
def test_acfactory(acfactory) -> None:
account = acfactory.new_configured_account()
def test_acf(acf) -> None:
account = acf.new_configured_account()
while True:
event = account.wait_for_event()
if event.kind == EventType.CONFIGURE_PROGRESS:
@@ -61,9 +61,9 @@ def test_acfactory(acfactory) -> None:
logging.info("Successful configuration")
def test_configure_starttls(acfactory) -> None:
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
def test_configure_starttls(acf) -> None:
addr, password = acf.get_credentials()
account = acf.get_unconfigured_account()
account.add_or_update_transport(
{
"addr": addr,
@@ -75,10 +75,10 @@ def test_configure_starttls(acfactory) -> None:
assert account.is_configured()
def test_lowercase_address(acfactory) -> None:
addr, password = acfactory.get_credentials()
def test_lowercase_address(acf) -> None:
addr, password = acf.get_credentials()
addr_upper = addr.upper()
account = acfactory.get_unconfigured_account()
account = acf.get_unconfigured_account()
account.add_or_update_transport(
{
"addr": addr_upper,
@@ -103,9 +103,9 @@ def test_lowercase_address(acfactory) -> None:
assert addr_upper not in param
def test_configure_ip(acfactory) -> None:
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
def test_configure_ip(acf) -> None:
addr, password = acf.get_credentials()
account = acf.get_unconfigured_account()
ip_address = socket.gethostbyname(addr.rsplit("@")[-1])
with pytest.raises(JsonRpcError):
@@ -119,10 +119,10 @@ def test_configure_ip(acfactory) -> None:
)
def test_configure_alternative_port(acfactory) -> None:
def test_configure_alternative_port(acf) -> None:
"""Test that configuration with alternative port 443 works."""
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
addr, password = acf.get_credentials()
account = acf.get_unconfigured_account()
account.add_or_update_transport(
{
"addr": addr,
@@ -134,9 +134,9 @@ def test_configure_alternative_port(acfactory) -> None:
assert account.is_configured()
def test_list_transports(acfactory) -> None:
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
def test_list_transports(acf) -> None:
addr, password = acf.get_credentials()
account = acf.get_unconfigured_account()
account.add_or_update_transport(
{
"addr": addr,
@@ -152,8 +152,8 @@ def test_list_transports(acfactory) -> None:
assert params["imapUser"] == addr
def test_account(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
def test_account(acf) -> None:
alice, bob = acf.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob, "Bob")
@@ -221,8 +221,8 @@ def test_account(acfactory) -> None:
alice.stop_io()
def test_mark_fresh_vs_self_mdn(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
def test_mark_fresh_vs_self_mdn(acf) -> None:
alice, bob = acf.get_online_accounts(2)
bob.set_config("bcc_self", "1")
alice_contact_bob = alice.create_contact(bob)
@@ -245,8 +245,8 @@ def test_mark_fresh_vs_self_mdn(acfactory) -> None:
assert bob_chat.get_fresh_message_count() == 2
def test_chat(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
def test_chat(acf) -> None:
alice, bob = acf.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
@@ -315,8 +315,8 @@ def test_chat(acfactory) -> None:
group.get_locations()
def test_contact(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
def test_contact(acf) -> None:
alice, bob = acf.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob, "Bob")
@@ -332,8 +332,8 @@ def test_contact(acfactory) -> None:
alice_contact_bob.create_chat()
def test_message(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
def test_message(acf) -> None:
alice, bob = acf.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
@@ -363,8 +363,8 @@ def test_message(acfactory) -> None:
assert reactions == snapshot.reactions
def test_receive_imf_failure(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
def test_receive_imf_failure(acf) -> None:
alice, bob = acf.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
@@ -392,8 +392,8 @@ def test_receive_imf_failure(acfactory) -> None:
assert snapshot.error is None
def test_selfavatar_sync(acfactory, data, log) -> None:
alice = acfactory.get_online_account()
def test_selfavatar_sync(acf, rpcdata, log) -> None:
alice = acf.get_online_account()
log.section("Alice adds a second device")
alice2 = alice.clone()
@@ -402,7 +402,7 @@ def test_selfavatar_sync(acfactory, data, log) -> None:
alice2.start_io()
log.section("First device changes avatar")
image = data.get_path("image/avatar1000x1000.jpg")
image = rpcdata.get_path("image/avatar1000x1000.jpg")
alice.set_config("selfavatar", image)
avatar_config = alice.get_config("selfavatar")
avatar_hash = os.path.basename(avatar_config)
@@ -417,9 +417,9 @@ def test_selfavatar_sync(acfactory, data, log) -> None:
assert avatar_config != avatar_config2
def test_dont_move_sync_msgs(acfactory, direct_imap):
addr, password = acfactory.get_credentials()
ac1 = acfactory.get_unconfigured_account()
def test_dont_move_sync_msgs(acf, direct_imap):
addr, password = acf.get_credentials()
ac1 = acf.get_unconfigured_account()
ac1.set_config("bcc_self", "1")
ac1.set_config("fix_is_chatmail", "1")
ac1.add_or_update_transport({"addr": addr, "password": password})
@@ -448,8 +448,8 @@ def test_dont_move_sync_msgs(acfactory, direct_imap):
time.sleep(1)
def test_reaction_seen_on_another_dev(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
def test_reaction_seen_on_another_dev(acf) -> None:
alice, bob = acf.get_online_accounts(2)
alice2 = alice.clone()
alice2.start_io()
@@ -474,8 +474,8 @@ def test_reaction_seen_on_another_dev(acfactory) -> None:
assert chat_id == alice2_chat_bob.id
def test_2nd_device_events_when_msgs_are_seen(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
def test_2nd_device_events_when_msgs_are_seen(acf) -> None:
alice, bob = acf.get_online_accounts(2)
alice2 = alice.clone()
alice2.start_io()
@@ -503,9 +503,9 @@ def test_2nd_device_events_when_msgs_are_seen(acfactory) -> None:
assert chat_alice2.get_fresh_message_count() == 0
def test_is_bot(acfactory) -> None:
def test_is_bot(acf) -> None:
"""Test that we can recognize messages submitted by bots."""
alice, bob = acfactory.get_online_accounts(2)
alice, bob = acf.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
@@ -519,18 +519,18 @@ def test_is_bot(acfactory) -> None:
assert snapshot.is_bot
def test_bot(acfactory) -> None:
def test_bot(acf) -> None:
mock = MagicMock()
user = (acfactory.get_online_accounts(1))[0]
bot = acfactory.new_configured_bot()
bot2 = acfactory.new_configured_bot()
user = (acf.get_online_accounts(1))[0]
bot = acf.new_configured_bot()
bot2 = acf.new_configured_bot()
assert bot.is_configured()
assert bot.account.get_config("bot") == "1"
hook = lambda e: mock.hook(e.msg_id) and None, events.RawEvent(EventType.INCOMING_MSG)
bot.add_hook(*hook)
event = acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
event = acf.process_message(from_account=user, to_client=bot, text="Hello!")
snapshot = bot.account.get_message_by_id(event.msg_id).get_snapshot()
assert not snapshot.is_bot
mock.hook.assert_called_once_with(event.msg_id)
@@ -543,28 +543,28 @@ def test_bot(acfactory) -> None:
hook = track, events.NewMessage(r"hello")
bot.add_hook(*hook)
bot.add_hook(track, events.NewMessage(command="/help"))
event = acfactory.process_message(from_account=user, to_client=bot, text="hello")
event = acf.process_message(from_account=user, to_client=bot, text="hello")
mock.hook.assert_called_with(event.msg_id)
event = acfactory.process_message(from_account=user, to_client=bot, text="hello!")
event = acf.process_message(from_account=user, to_client=bot, text="hello!")
mock.hook.assert_called_with(event.msg_id)
acfactory.process_message(from_account=bot2.account, to_client=bot, text="hello")
acf.process_message(from_account=bot2.account, to_client=bot, text="hello")
assert len(mock.hook.mock_calls) == 2 # bot messages are ignored between bots
acfactory.process_message(from_account=user, to_client=bot, text="hey!")
acf.process_message(from_account=user, to_client=bot, text="hey!")
assert len(mock.hook.mock_calls) == 2
bot.remove_hook(*hook)
mock.hook.reset_mock()
acfactory.process_message(from_account=user, to_client=bot, text="hello")
event = acfactory.process_message(from_account=user, to_client=bot, text="/help")
acf.process_message(from_account=user, to_client=bot, text="hello")
event = acf.process_message(from_account=user, to_client=bot, text="/help")
mock.hook.assert_called_once_with(event.msg_id)
def test_wait_next_messages(acfactory) -> None:
alice = acfactory.get_online_account()
def test_wait_next_messages(acf) -> None:
alice = acf.get_online_account()
# Create a bot account so it does not receive device messages in the beginning.
addr, password = acfactory.get_credentials()
bot = acfactory.get_unconfigured_account()
addr, password = acf.get_credentials()
bot = acf.get_unconfigured_account()
bot.set_config("bot", "1")
bot.add_or_update_transport({"addr": addr, "password": password})
assert bot.is_configured()
@@ -590,19 +590,19 @@ def test_wait_next_messages(acfactory) -> None:
assert snapshot.text == "Hello!"
def test_import_export_backup(acfactory, tmp_path) -> None:
alice = acfactory.new_configured_account()
def test_import_export_backup(acf, tmp_path) -> None:
alice = acf.new_configured_account()
alice.export_backup(tmp_path)
files = list(tmp_path.glob("*.tar"))
alice2 = acfactory.get_unconfigured_account()
alice2 = acf.get_unconfigured_account()
alice2.import_backup(files[0])
assert alice2.manager.get_system_info()
def test_import_export_online_all(acfactory, tmp_path, data, log) -> None:
(ac1, some1) = acfactory.get_online_accounts(2)
def test_import_export_online_all(acf, tmp_path, rpcdata, log) -> None:
(ac1, some1) = acf.get_online_accounts(2)
log.section("create some chat content")
some1_addr = some1.get_config("addr")
@@ -610,7 +610,7 @@ def test_import_export_online_all(acfactory, tmp_path, data, log) -> None:
chat1.send_text("msg1")
assert len(ac1.get_contacts()) == 1
original_image_path = data.get_path("image/avatar64x64.png")
original_image_path = rpcdata.get_path("image/avatar64x64.png")
chat1.send_file(str(original_image_path))
# Add another 100KB file that ensures that the progress is smooth enough
@@ -661,7 +661,7 @@ def test_import_export_online_all(acfactory, tmp_path, data, log) -> None:
ac1.start_io()
log.section("get fresh empty account")
ac2 = acfactory.get_unconfigured_account()
ac2 = acf.get_unconfigured_account()
log.section("import backup and check it's proper")
ac2.import_backup(files_written[0])
@@ -698,8 +698,8 @@ def test_import_export_online_all(acfactory, tmp_path, data, log) -> None:
assert len(list(backupdir.glob("*.tar"))) == 2
def test_import_export_keys(acfactory, tmp_path) -> None:
alice, bob = acfactory.get_online_accounts(2)
def test_import_export_keys(acf, tmp_path) -> None:
alice, bob = acf.get_online_accounts(2)
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.send_text("Hello Bob!")
@@ -711,7 +711,7 @@ def test_import_export_keys(acfactory, tmp_path) -> None:
alice_keys_path = tmp_path / "alice_keys"
alice_keys_path.mkdir()
alice.export_self_keys(alice_keys_path)
alice = acfactory.resetup_account(alice)
alice = acf.resetup_account(alice)
alice.import_self_keys(alice_keys_path)
snapshot.chat.accept()
@@ -746,9 +746,14 @@ def test_early_failure(tmp_path) -> None:
with pytest.raises(JsonRpcError, match="invalid_dir"):
rpc.start()
# Requests issued after the server exited must fail immediately
# instead of waiting forever for the finished reader loop.
with pytest.raises(JsonRpcError, match="RPC server closed"):
rpc.get_system_info()
def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
def test_mdn_doesnt_break_autocrypt(acf) -> None:
alice, bob = acf.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
@@ -778,10 +783,10 @@ def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
@pytest.mark.parametrize("n_accounts", [3, 2])
def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
def test_download_limit_chat_assignment(acf, tmp_path, n_accounts):
download_limit = 300000
alice, *others = acfactory.get_online_accounts(n_accounts)
alice, *others = acf.get_online_accounts(n_accounts)
bob = others[0]
alice_group = alice.create_group("test group")
@@ -817,10 +822,10 @@ def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
assert snapshot.chat == bob_group
def test_download_small_msg_first(acfactory, tmp_path):
def test_download_small_msg_first(acf, tmp_path):
download_limit = 70000
alice, bob0 = acfactory.get_online_accounts(2)
alice, bob0 = acf.get_online_accounts(2)
bob1 = bob0.clone()
bob1.set_config("download_limit", str(download_limit))
@@ -841,14 +846,14 @@ def test_download_small_msg_first(acfactory, tmp_path):
@pytest.mark.parametrize("delete_chat", [False, True])
def test_delete_available_msg(acfactory, tmp_path, direct_imap, delete_chat):
def test_delete_available_msg(acf, tmp_path, direct_imap, delete_chat):
"""
Tests `DownloadState.AVAILABLE` message deletion on the receiver side.
Also tests pre- and post-message deletion on the sender side.
"""
# Min. UI setting as of v2.35
download_limit = 163840
alice, bob = acfactory.get_online_accounts(2)
alice, bob = acf.get_online_accounts(2)
bob.set_config("download_limit", str(download_limit))
# Avoid immediate deletion from the server
alice.set_config("bcc_self", "1")
@@ -891,8 +896,8 @@ def test_delete_available_msg(acfactory, tmp_path, direct_imap, delete_chat):
break
def test_delete_fully_downloaded_msg(acfactory, tmp_path, direct_imap):
alice, bob = acfactory.get_online_accounts(2)
def test_delete_fully_downloaded_msg(acf, tmp_path, direct_imap):
alice, bob = acf.get_online_accounts(2)
# Avoid immediate deletion from the server
bob.set_config("bcc_self", "1")
@@ -927,8 +932,8 @@ def test_delete_fully_downloaded_msg(acfactory, tmp_path, direct_imap):
break
def test_imap_autodelete_fully_downloaded_msg(acfactory, tmp_path, direct_imap):
alice, bob = acfactory.get_online_accounts(2)
def test_imap_autodelete_fully_downloaded_msg(acf, tmp_path, direct_imap):
alice, bob = acf.get_online_accounts(2)
chat_alice = alice.create_chat(bob)
path = tmp_path / "large"
@@ -956,12 +961,12 @@ def test_imap_autodelete_fully_downloaded_msg(acfactory, tmp_path, direct_imap):
break
def test_markseen_contact_request(acfactory):
def test_markseen_contact_request(acf):
"""
Test that seen status is synchronized for contact request messages
even though read receipt is not sent.
"""
alice, bob = acfactory.get_online_accounts(2)
alice, bob = acf.get_online_accounts(2)
# Bob sets up a second device.
bob2 = bob.clone()
@@ -980,11 +985,11 @@ def test_markseen_contact_request(acfactory):
@pytest.mark.parametrize("team_profile", [True, False])
def test_no_markseen_in_team_profile(team_profile, acfactory):
def test_no_markseen_in_team_profile(team_profile, acf):
"""
Test that seen status is synchronized iff `team_profile` isn't set.
"""
alice, bob = acfactory.get_online_accounts(2)
alice, bob = acf.get_online_accounts(2)
if team_profile:
bob.set_config("team_profile", "1")
@@ -1025,11 +1030,11 @@ def test_no_markseen_in_team_profile(team_profile, acfactory):
assert message2.get_snapshot().state == MessageState.IN_SEEN
def test_read_receipt(acfactory):
def test_read_receipt(acf):
"""
Test sending a read receipt and ensure it is attributed to the correct contact.
"""
alice, bob = acfactory.get_online_accounts(2)
alice, bob = acf.get_online_accounts(2)
alice_chat_bob = alice.create_chat(bob)
alice_contact_bob = alice.create_contact(bob)
@@ -1048,15 +1053,15 @@ def test_read_receipt(acfactory):
assert read_receipt_cnt == 1
def test_get_http_response(acfactory):
alice = acfactory.new_configured_account()
def test_get_http_response(acf):
alice = acf.new_configured_account()
http_response = alice._rpc.get_http_response(alice.id, "https://example.org")
assert http_response["mimetype"] == "text/html"
assert b"<title>Example Domain</title>" in base64.b64decode((http_response["blob"] + "==").encode())
def test_configured_imap_certificate_checks(acfactory):
alice = acfactory.new_configured_account()
def test_configured_imap_certificate_checks(acf):
alice = acf.new_configured_account()
# Certificate checks should be configured (not None)
assert "cert_strict" in alice.get_info().used_transport_settings
@@ -1075,8 +1080,8 @@ def test_configured_imap_certificate_checks(acfactory):
assert "cert_old_automatic" not in alice.get_info().used_transport_settings
def test_no_old_msg_is_fresh(acfactory):
ac1, ac2 = acfactory.get_online_accounts(2)
def test_no_old_msg_is_fresh(acf):
ac1, ac2 = acf.get_online_accounts(2)
ac1_clone = ac1.clone()
ac1_clone.start_io()
@@ -1103,9 +1108,9 @@ def test_no_old_msg_is_fresh(acfactory):
assert len(list(ac1.get_fresh_messages())) == 0
def test_rename_synchronization(acfactory):
def test_rename_synchronization(acf):
"""Test synchronization of contact renaming."""
alice, bob = acfactory.get_online_accounts(2)
alice, bob = acf.get_online_accounts(2)
alice2 = alice.clone()
alice2.bring_online()
@@ -1120,9 +1125,9 @@ def test_rename_synchronization(acfactory):
assert alice2_msg.sender.get_snapshot().display_name == "Bobby"
def test_rename_group(acfactory):
def test_rename_group(acf):
"""Test renaming the group."""
alice, bob = acfactory.get_online_accounts(2)
alice, bob = acf.get_online_accounts(2)
alice_group = alice.create_group("Test group")
alice_contact_bob = alice.create_contact(bob)
@@ -1151,8 +1156,8 @@ def test_get_all_accounts_deadlock(rpc):
@pytest.mark.parametrize("all_devices_online", [True, False])
def test_leave_broadcast(acfactory, all_devices_online):
alice, bob = acfactory.get_online_accounts(2)
def test_leave_broadcast(acf, all_devices_online):
alice, bob = acf.get_online_accounts(2)
bob2 = bob.clone()
@@ -1252,8 +1257,8 @@ def test_leave_broadcast(acfactory, all_devices_online):
check_account(bob2, bob2.create_contact(alice), inviter_side=False)
def test_leave_and_delete_group(acfactory, log):
alice, bob = acfactory.get_online_accounts(2)
def test_leave_and_delete_group(acf, log):
alice, bob = acf.get_online_accounts(2)
log.section("Alice creates a group")
alice_chat = alice.create_group("Group")
@@ -1276,12 +1281,12 @@ def test_leave_and_delete_group(acfactory, log):
alice.wait_for_event(EventType.CHAT_MODIFIED)
def test_immediate_autodelete(acfactory, direct_imap, log):
def test_immediate_autodelete(acf, direct_imap, log):
"""
`bcc_self` is off by default,
so that messages are supposed to be immediately autodeleted
"""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1, ac2 = acf.get_online_accounts(2)
assert ac1.get_config("bcc_self") == "0"
log.section("ac1: create chat with ac2")
@@ -1312,8 +1317,8 @@ def test_immediate_autodelete(acfactory, direct_imap, log):
assert ev.msg_id == sent_msg.id
def test_background_fetch(acfactory, dc):
ac1, ac2 = acfactory.get_online_accounts(2)
def test_background_fetch(acf, dc):
ac1, ac2 = acf.get_online_accounts(2)
ac1.stop_io()
ac1_chat = ac1.create_chat(ac2)
@@ -1349,8 +1354,8 @@ def test_background_fetch(acfactory, dc):
break
def test_message_exists(acfactory):
ac1, ac2 = acfactory.get_online_accounts(2)
def test_message_exists(acf):
ac1, ac2 = acf.get_online_accounts(2)
chat = ac1.create_chat(ac2)
message1 = chat.send_text("Hello!")
message2 = chat.send_text("Hello again!")
@@ -1368,7 +1373,7 @@ def test_message_exists(acfactory):
assert not message2.exists()
def test_synchronize_member_list_on_group_rejoin(acfactory, log):
def test_synchronize_member_list_on_group_rejoin(acf, log):
"""
Test that user recreates group member list when it joins the group again.
ac1 creates a group with two other accounts: ac2 and ac3
@@ -1376,7 +1381,7 @@ def test_synchronize_member_list_on_group_rejoin(acfactory, log):
ac2 did not see that ac3 is removed, so it should rebuild member list from scratch.
"""
log.section("setting up accounts, accepted with each other")
ac1, ac2, ac3 = accounts = acfactory.get_online_accounts(3)
ac1, ac2, ac3 = accounts = acf.get_online_accounts(3)
log.section("ac1: creating group chat with 2 other members")
chat = ac1.create_group("title1")
@@ -1412,17 +1417,17 @@ def test_synchronize_member_list_on_group_rejoin(acfactory, log):
assert msg.get_snapshot().chat.num_contacts() == 2
def test_large_message(acfactory, data) -> None:
def test_large_message(acf, rpcdata) -> None:
"""
Test sending large message without download limit set,
so it is sent with pre-message but downloaded without user interaction.
"""
alice, bob = acfactory.get_online_accounts(2)
alice, bob = acf.get_online_accounts(2)
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.send_message(
"Hello World, this message is bigger than 5 bytes",
file=data.get_path("image/screenshot.jpg"),
file=rpcdata.get_path("image/screenshot.jpg"),
)
msg = bob.wait_for_incoming_msg()

View File

@@ -80,8 +80,8 @@ def read_database_schema(dbfile):
return ";\n".join(row[0] for row in rows)
def test_documented_schema_matches_database(acfactory):
account = acfactory.get_unconfigured_account()
def test_documented_schema_matches_database(acf):
account = acf.get_unconfigured_account()
real = parse_schema(read_database_schema(account.get_info()["database_dir"]))
documented = parse_schema(DOC_PATH.read_text())

View File

@@ -1,5 +1,5 @@
def test_vcard(acfactory) -> None:
alice, bob, fiona = acfactory.get_online_accounts(3)
def test_vcard(acf) -> None:
alice, bob, fiona = acf.get_online_accounts(3)
bob.create_chat(alice)
alice_contact_bob = alice.create_contact(bob, "Bob")

View File

@@ -1,9 +1,9 @@
def test_webxdc(acfactory, data) -> None:
alice, bob = acfactory.get_online_accounts(2)
def test_webxdc(acf, rpcdata) -> None:
alice, bob = acf.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_message(text="Let's play chess!", file=data.get_path("webxdc/chess.xdc"))
alice_chat_bob.send_message(text="Let's play chess!", file=rpcdata.get_path("webxdc/chess.xdc"))
event = bob.wait_for_incoming_msg_event()
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
@@ -43,12 +43,12 @@ def test_webxdc(acfactory, data) -> None:
]
def test_webxdc_insert_lots_of_updates(acfactory, data) -> None:
alice, bob = acfactory.get_online_accounts(2)
def test_webxdc_insert_lots_of_updates(acf, rpcdata) -> None:
alice, bob = acf.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
message = alice_chat_bob.send_message(text="Let's play chess!", file=data.get_path("webxdc/chess.xdc"))
message = alice_chat_bob.send_message(text="Let's play chess!", file=rpcdata.get_path("webxdc/chess.xdc"))
for i in range(2000):
message.send_webxdc_status_update({"payload": str(i)}, "description")

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "2.59.0"
version = "2.60.0-dev"
description = "DeltaChat JSON-RPC server"
edition = "2024"
readme = "README.md"

View File

@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "2.59.0"
"version": "2.60.0-dev"
}

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "2.59.0"
version = "2.60.0-dev"
license = "MPL-2.0"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"

View File

@@ -37,6 +37,10 @@ and an own build machine.
- `android-rpc-server.sh` compiles binaries of `deltachat-rpc-server` using Android NDK.
- `future-sizes.sh` prints the sizes of the largest Futures.
This can be helpful because Async Rust can lead to huge futures,
increasing RAM usage and compilation times.
## Triggering runs on the build machine locally (fast!)
There is experimental support for triggering a remote Python or Rust test run

39
scripts/future-sizes.sh Executable file
View File

@@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -euo pipefail
# Report the largest async-fn futures using rustc's unstable -Zprint-type-sizes.
#
# Usage: scripts/future-sizes.sh [TOP_N] (default: 30)
#
# Uses a dedicated target dir so the normal build cache is untouched
# and repeated runs only recompile the core crate itself.
TOP_N="${1:-30}"
export CARGO_TARGET_DIR="target/type-sizes"
export RUSTFLAGS="-Zprint-type-sizes"
export RUSTC_BOOTSTRAP=1
# Warm the dependency cache so their output doesn't pollute the report later.
cargo check --locked --release -p deltachat >/dev/null
# Recompile only the core crate (release) capture its type-size report.
cargo clean --locked --release -p deltachat
report="$CARGO_TARGET_DIR/type-sizes.txt"
if ! cargo check --locked --release -p deltachat >"$report"; then
tail -20 "$report"
exit 1
fi
sizes=$(rg '^print-type-size type: `\{async fn body of (.*)\(\)\}`: ([0-9]+) bytes' \
-or '$2 $1' "$report" | sort -rn | uniq)
if [ -z "$sizes" ]; then
echo "error: no type-size output found, see $report" >&2
exit 1
fi
echo "Largest async fn futures in deltachat (top $TOP_N, bytes):"
head -"$TOP_N" <<<"$sizes"
echo
echo "Full report (all types, with per-field breakdown): $report"

View File

@@ -44,6 +44,24 @@ pub struct AppSource {
pub download_url: String,
}
/// Sanitize version string for safe display/use by UIs.
///
/// Replaces untypical characters by `-`
/// and truncates to at most 16 characters.
/// This is to avoid to inject long text, unexpected content, formatting, homoglyphs.
///
/// Note, that UI should still take care to not linkify versions numbers -
/// they may still look like phone numbers or IP-addresses.
fn sanitize_version_string(version_string: &str) -> String {
version_string
.trim()
.to_lowercase()
.replace(|c: char| !c.is_ascii_alphanumeric() && c != '.', "-")
.chars()
.take(16)
.collect()
}
/// Get version information of a specific client and source.
///
/// Iterates over all accounts and all transports,
@@ -79,11 +97,16 @@ pub async fn get_app_version(
.find(|c| c.client_id == client_id)
.and_then(|c| c.sources.into_iter().find(|s| s.source_id == source_id));
if let Some(candidate) = candidate
if let Some(mut candidate) = candidate
&& best
.as_ref()
.is_none_or(|b| candidate.version_integer > b.version_integer)
{
candidate.version_string = sanitize_version_string(&candidate.version_string);
if candidate.version_string.is_empty() {
warn!(context, "version_string missing.");
continue;
}
best = Some(candidate);
}
}
@@ -97,6 +120,30 @@ mod tests {
use super::*;
use std::path::PathBuf;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sanitize_version_string() {
assert_eq!(sanitize_version_string(""), "");
assert_eq!(sanitize_version_string("\n"), "");
assert_eq!(sanitize_version_string("7٣৬¾①و藏"), "7------");
assert_eq!(sanitize_version_string("2.57.0"), "2.57.0");
assert_eq!(
sanitize_version_string("2.57.0 whatever"),
"2.57.0-whatever"
);
assert_eq!(sanitize_version_string("2.57.0-RC1"), "2.57.0-rc1");
assert_eq!(sanitize_version_string(" 23.0 "), "23.0");
assert_eq!(
sanitize_version_string("666.999 tap https://evil.com"),
"666.999-tap-http"
);
assert_eq!(sanitize_version_string("2.0<br>bla"), "2.0-br-bla");
assert_eq!(
sanitize_version_string("1.0\nand now let me tell the following: bar baz foo"),
"1.0-and-now-let-"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_app_version_info_deserialize() -> Result<()> {
let json = r##"{
@@ -203,7 +250,7 @@ mod tests {
{
"sourceId": "baz",
"versionInteger": 1337,
"versionString": "13.37",
"versionString": " 13.37 ",
"downloadUrl": "https://dl.org/1337.acc"
}
]
@@ -224,7 +271,7 @@ mod tests {
let version = get_app_version(&accounts, "foo", "baz").await?.unwrap();
assert_eq!(version.version_integer, 1337);
assert_eq!(version.version_string, "13.37");
assert_eq!(version.version_string, "13.37"); // spaces are removed by sanitize_version_string()
assert_eq!(version.download_url, "https://dl.org/1337.acc");
let version = get_app_version(&accounts, "non-", "existant").await?;