test: Add python test test_qr_securejoin_broadcast, and fix some small bugs I found on the way

This commit is contained in:
Hocuri
2025-09-03 22:07:17 +02:00
parent 8eb5fc528f
commit 60e4899b3a
5 changed files with 130 additions and 13 deletions

View File

@@ -439,6 +439,12 @@ class Account:
"""Wait for reaction change event.""" """Wait for reaction change event."""
return self.wait_for_event(EventType.REACTIONS_CHANGED) return self.wait_for_event(EventType.REACTIONS_CHANGED)
def wait_for_imap_inbox_idle(self):
"""Wait until all messages are fetched,
and the IMAP loop enters IDLE mode.
"""
self.wait_for_event(EventType.IMAP_INBOX_IDLE)
def get_fresh_messages_in_arrival_order(self) -> list[Message]: def get_fresh_messages_in_arrival_order(self) -> list[Message]:
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs.""" """Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
warn( warn(

View File

@@ -112,6 +112,98 @@ def test_qr_securejoin(acfactory, protect):
fiona.wait_for_securejoin_joiner_success() fiona.wait_for_securejoin_joiner_success()
@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)
alice2 = alice.clone()
bob2 = bob.clone()
if all_devices_online:
alice2.start_io()
bob2.start_io()
logging.info("Alice creates a broadcast")
alice_chat = alice.create_broadcast("Broadcast channel for everyone!")
logging.info("Bob joins the broadcast")
qr_code = alice_chat.get_qr_code()
bob.secure_join(qr_code)
alice.wait_for_securejoin_inviter_success()
bob.wait_for_securejoin_joiner_success()
snapshot = bob.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == f"Member Me added by {alice.get_config('addr')}."
alice_chat.send_text("Hello everyone!")
snapshot = bob.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "Hello everyone!"
def check_account(ac, contact, inviter_side, please_wait_info_msg=False):
# Check that the chat partner is verified.
contact_snapshot = contact.get_snapshot()
assert contact_snapshot.is_verified
chat = ac.get_chatlist()[0]
chat_msgs = chat.get_messages()
if please_wait_info_msg:
first_msg = chat_msgs.pop(0).get_snapshot()
assert first_msg.text == "Establishing guaranteed end-to-end encryption, please wait…"
assert first_msg.is_info
encrypted_msg = chat_msgs[0].get_snapshot()
assert encrypted_msg.text == "Messages are end-to-end encrypted."
assert encrypted_msg.is_info
member_added_msg = chat_msgs[1].get_snapshot()
if inviter_side:
assert member_added_msg.text == f"Member {contact_snapshot.display_name} added."
else:
assert member_added_msg.text == f"Member Me added by {contact_snapshot.display_name}."
assert member_added_msg.is_info
hello_msg = chat_msgs[2].get_snapshot()
assert hello_msg.text == "Hello everyone!"
assert not hello_msg.is_info
assert len(chat_msgs) == 3
chat_snapshot = chat.get_basic_snapshot() # TODO or get_full_snapshot()
assert chat_snapshot.is_encrypted
# TODO check more things
check_account(alice, alice.create_contact(bob), inviter_side=True)
check_account(bob, bob.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
logging.info("===================== Test Alice's second device =====================")
# Start second Alice device, if it wasn't started already.
alice2.start_io()
alice2.wait_for_securejoin_inviter_success()
alice2.wait_for_imap_inbox_idle()
check_account(alice2, alice2.create_contact(bob), inviter_side=True)
logging.info("===================== Test Bob's second device =====================")
# Start second Bob device, if it wasn't started already.
bob2.start_io()
bob2.wait_for_securejoin_joiner_success()
bob2.wait_for_imap_inbox_idle()
check_account(bob2, bob2.create_contact(alice), inviter_side=False)
# The QR code token is synced, so alice2 must be able to handle join requests.
logging.info("Fiona joins the group via alice2")
alice.stop_io()
fiona.secure_join(qr_code)
alice2.wait_for_securejoin_inviter_success()
fiona.wait_for_securejoin_joiner_success()
# TODO test that Fiona is in the channel correctly
def test_qr_securejoin_contact_request(acfactory) -> None: def test_qr_securejoin_contact_request(acfactory) -> None:
"""Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode.""" """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 = acfactory.get_online_accounts(2)

View File

@@ -6,7 +6,7 @@ envlist =
[testenv] [testenv]
commands = commands =
pytest -n6 {posargs} pytest {posargs}
setenv = setenv =
# Avoid stack overflow when Rust core is built without optimizations. # Avoid stack overflow when Rust core is built without optimizations.
RUST_MIN_STACK=8388608 RUST_MIN_STACK=8388608
@@ -25,6 +25,6 @@ commands =
ruff check src/ examples/ tests/ ruff check src/ examples/ tests/
[pytest] [pytest]
timeout = 300 timeout = 30
log_cli = true log_cli = true
log_level = debug log_level = debug

View File

@@ -601,11 +601,23 @@ impl ChatId {
|| chat.is_device_talk() || chat.is_device_talk()
|| chat.is_self_talk() || chat.is_self_talk()
|| (!chat.can_send(context).await? && !chat.is_contact_request()) || (!chat.can_send(context).await? && !chat.is_contact_request())
// For chattype InBrodacast, the info message is added when the member-added message is received
// by directly calling add_encrypted_msg():
|| chat.typ == Chattype::InBroadcast
|| chat.blocked == Blocked::Yes || chat.blocked == Blocked::Yes
{ {
return Ok(()); return Ok(());
} }
self.add_encrypted_msg(context, timestamp_sort).await?;
Ok(())
}
pub(crate) async fn add_encrypted_msg(
self,
context: &Context,
timestamp_sort: i64,
) -> Result<()> {
let text = stock_str::messages_e2e_encrypted(context).await; let text = stock_str::messages_e2e_encrypted(context).await;
add_info_msg_with_cmd( add_info_msg_with_cmd(
context, context,
@@ -3817,10 +3829,17 @@ pub(crate) async fn create_broadcast_ex(
chat_name: String, chat_name: String,
secret: String, secret: String,
) -> Result<ChatId> { ) -> Result<ChatId> {
let row_id = { let chat_name = sanitize_single_line(&chat_name);
if chat_name.is_empty() {
bail!("Invalid broadcast channel name: {chat_name}.");
}
let timestamp = create_smeared_timestamp(context);
let chat_id = {
let chat_name = &chat_name; let chat_name = &chat_name;
let grpid = &grpid; let grpid = &grpid;
let trans_fn = |t: &mut rusqlite::Transaction| { let trans_fn = |t: &mut rusqlite::Transaction| -> Result<u32> {
// TODO it's not needed to lookup an existing broadcast here
let cnt = t.execute("UPDATE chats SET name=? WHERE grpid=?", (chat_name, grpid))?; let cnt = t.execute("UPDATE chats SET name=? WHERE grpid=?", (chat_name, grpid))?;
ensure!(cnt <= 1, "{cnt} chats exist with grpid {grpid}"); ensure!(cnt <= 1, "{cnt} chats exist with grpid {grpid}");
if cnt == 1 { if cnt == 1 {
@@ -3828,7 +3847,7 @@ pub(crate) async fn create_broadcast_ex(
"SELECT id FROM chats WHERE grpid=? AND type=?", "SELECT id FROM chats WHERE grpid=? AND type=?",
(grpid, Chattype::OutBroadcast), (grpid, Chattype::OutBroadcast),
|row| { |row| {
let id: isize = row.get(0)?; let id: u32 = row.get(0)?;
Ok(id) Ok(id)
}, },
)?); )?);
@@ -3837,23 +3856,20 @@ pub(crate) async fn create_broadcast_ex(
"INSERT INTO chats \ "INSERT INTO chats \
(type, name, grpid, created_timestamp) \ (type, name, grpid, created_timestamp) \
VALUES(?, ?, ?, ?);", VALUES(?, ?, ?, ?);",
( (Chattype::OutBroadcast, &chat_name, &grpid, timestamp),
Chattype::OutBroadcast,
&chat_name,
&grpid,
create_smeared_timestamp(context),
),
)?; )?;
let chat_id = t.last_insert_rowid(); let chat_id = t.last_insert_rowid();
t.execute(SQL_INSERT_BROADCAST_SECRET, (chat_id, &secret))?; t.execute(SQL_INSERT_BROADCAST_SECRET, (chat_id, &secret))?;
Ok(t.last_insert_rowid().try_into()?) Ok(chat_id.try_into()?)
}; };
context.sql.transaction(trans_fn).await? context.sql.transaction(trans_fn).await?
}; };
let chat_id = ChatId::new(u32::try_from(row_id)?); let chat_id = ChatId::new(chat_id);
chat_id.maybe_add_encrypted_msg(context, timestamp).await?;
context.emit_msgs_changed_without_ids(); context.emit_msgs_changed_without_ids();
chatlist_events::emit_chatlist_changed(context); chatlist_events::emit_chatlist_changed(context);
chatlist_events::emit_chatlist_item_changed(context, chat_id);
if sync.into() { if sync.into() {
let id = SyncId::Grpid(grpid); let id = SyncId::Grpid(grpid);

View File

@@ -3571,6 +3571,9 @@ async fn apply_in_broadcast_changes(
info!(context, "No-op broadcast 'Member added' message (TRASH)"); info!(context, "No-op broadcast 'Member added' message (TRASH)");
msg = "".to_string(); msg = "".to_string();
} else { } else {
chat.id
.add_encrypted_msg(context, mime_parser.timestamp_sent)
.await?;
msg = stock_str::msg_add_member_local(context, ContactId::SELF, from_id).await; msg = stock_str::msg_add_member_local(context, ContactId::SELF, from_id).await;
} }