test(matrix): keep two invariant tests for the pending-invite gate
Trim the pending-invite allowlist tests to the fail-closed invariant (parametrized: non-allowed inviter, missing invite_state, empty events, member event for another user, empty sender -> no join, nothing recorded in m.direct) and the positive invariant (allow-listed inviter joined and the direct invite recorded via _record_dm_room). Dropped: the exact-log-text assertion for the allow-all direct-invite warning (change-detector), the empty-allowlist / unknown-bot-user-id / allow-all cases (already covered by _is_authorized_user's own tests and the fail-closed parametrization), and the three additions to test_matrix_dm_invite_recording.py, whose positive path is the same one test_allowed_inviter_is_joined asserts.
This commit is contained in:
@@ -101,109 +101,6 @@ class TestOnInviteRecordsDM:
|
||||
adapter._record_dm_room.assert_not_awaited()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _schedule_pending_invite_joins DM recording
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _member_invite_event(
|
||||
state_key="@hermes:example.org",
|
||||
sender="@alice:example.org",
|
||||
is_direct=True,
|
||||
membership="invite",
|
||||
):
|
||||
"""Create a stripped m.room.member event as found in invite_state."""
|
||||
return {
|
||||
"type": "m.room.member",
|
||||
"state_key": state_key,
|
||||
"sender": sender,
|
||||
"content": {"membership": membership, "is_direct": is_direct},
|
||||
}
|
||||
|
||||
|
||||
def _invite_sync_data(room_id="!dm_room:example.org", invite_state=None):
|
||||
"""Create a sync payload with one pending invite room."""
|
||||
room = {} if invite_state is None else {"invite_state": invite_state}
|
||||
return {"rooms": {"invite": {room_id: room}}, "next_batch": "s1"}
|
||||
|
||||
|
||||
class TestPendingInviteReconciliationRecordsDM:
|
||||
"""_schedule_pending_invite_joins threads the DM signal from invite_state.
|
||||
|
||||
After a gateway restart a direct invite is reconciled from sync's
|
||||
``rooms.invite`` rather than a live invite event. The stripped
|
||||
``m.room.member`` event in ``invite_state`` still carries ``is_direct``
|
||||
and the inviter; without threading them through, the room is never
|
||||
recorded in ``m.direct`` and is misclassified as a group (surfaced by
|
||||
the triage of #62493).
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
async def _drain_invite_tasks(adapter):
|
||||
"""Await any tasks _schedule_invite_join spawned."""
|
||||
for task in list(adapter._invite_join_tasks.values()):
|
||||
await task
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reconciled_direct_invite_records_dm(self):
|
||||
adapter = _make_adapter()
|
||||
adapter._join_room_by_id = AsyncMock(return_value=True)
|
||||
adapter._record_dm_room = AsyncMock()
|
||||
|
||||
sync_data = _invite_sync_data(
|
||||
invite_state={"events": [_member_invite_event(is_direct=True)]}
|
||||
)
|
||||
adapter._schedule_pending_invite_joins(sync_data)
|
||||
await self._drain_invite_tasks(adapter)
|
||||
|
||||
adapter._join_room_by_id.assert_awaited_once_with("!dm_room:example.org")
|
||||
adapter._record_dm_room.assert_awaited_once_with(
|
||||
"!dm_room:example.org", "@alice:example.org"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reconciled_direct_invite_ends_up_classified_as_dm(self):
|
||||
"""End to end: the reconciled room lands in _dm_rooms as a DM."""
|
||||
adapter = _make_adapter()
|
||||
adapter._join_room_by_id = AsyncMock(return_value=True)
|
||||
adapter._client = MagicMock()
|
||||
adapter._client.get_account_data = AsyncMock(
|
||||
side_effect=Exception("M_NOT_FOUND")
|
||||
)
|
||||
adapter._client.set_account_data = AsyncMock()
|
||||
|
||||
sync_data = _invite_sync_data(
|
||||
invite_state={"events": [_member_invite_event(is_direct=True)]}
|
||||
)
|
||||
adapter._schedule_pending_invite_joins(sync_data)
|
||||
await self._drain_invite_tasks(adapter)
|
||||
|
||||
adapter._client.set_account_data.assert_awaited_once_with(
|
||||
"m.direct", {"@alice:example.org": ["!dm_room:example.org"]}
|
||||
)
|
||||
assert adapter._dm_rooms.get("!dm_room:example.org") is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reconciled_non_direct_invite_does_not_record(self):
|
||||
"""A pending invite without the is_direct flag joins (the inviter
|
||||
is allow-listed) but records nothing in m.direct. Invites whose
|
||||
inviter cannot be read from the stripped state at all are covered
|
||||
by test_matrix_pending_invite_auth.py: they are rejected outright
|
||||
by the inviter allowlist gate, so no join happens either."""
|
||||
adapter = _make_adapter()
|
||||
adapter._join_room_by_id = AsyncMock(return_value=True)
|
||||
adapter._record_dm_room = AsyncMock()
|
||||
|
||||
sync_data = _invite_sync_data(
|
||||
invite_state={"events": [_member_invite_event(is_direct=False)]}
|
||||
)
|
||||
adapter._schedule_pending_invite_joins(sync_data)
|
||||
await self._drain_invite_tasks(adapter)
|
||||
|
||||
adapter._join_room_by_id.assert_awaited_once_with("!dm_room:example.org")
|
||||
adapter._record_dm_room.assert_not_awaited()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _record_dm_room
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -8,7 +8,6 @@ state, or an invite from an arbitrary federated user that arrives while
|
||||
the gateway is down gets auto-joined on restart.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
@@ -87,19 +86,6 @@ class TestPendingInviteAuthorization:
|
||||
"!pending_room:example.org", "@alice:example.org"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unknown_bot_user_id_fails_closed(self):
|
||||
adapter = _make_adapter()
|
||||
adapter._user_id = ""
|
||||
|
||||
sync_data = _invite_sync_data(invite_state={"events": [_member_invite_event()]})
|
||||
adapter._schedule_pending_invite_joins(sync_data)
|
||||
await _drain_invite_tasks(adapter)
|
||||
|
||||
adapter._join_room_by_id.assert_not_awaited()
|
||||
adapter._record_dm_room.assert_not_awaited()
|
||||
assert adapter._invite_join_tasks == {}
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"invite_state",
|
||||
[
|
||||
@@ -133,58 +119,3 @@ class TestPendingInviteAuthorization:
|
||||
adapter._join_room_by_id.assert_not_awaited()
|
||||
adapter._record_dm_room.assert_not_awaited()
|
||||
assert adapter._invite_join_tasks == {}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_allowlist_fails_closed(self):
|
||||
"""With no allowlist configured, _on_invite rejects every invite;
|
||||
reconciliation must do the same."""
|
||||
adapter = _make_adapter()
|
||||
adapter._allowed_user_ids = set()
|
||||
|
||||
sync_data = _invite_sync_data(invite_state={"events": [_member_invite_event()]})
|
||||
adapter._schedule_pending_invite_joins(sync_data)
|
||||
await _drain_invite_tasks(adapter)
|
||||
|
||||
adapter._join_room_by_id.assert_not_awaited()
|
||||
adapter._record_dm_room.assert_not_awaited()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_allow_all_env_bypasses_gate(self, monkeypatch):
|
||||
"""GATEWAY_ALLOW_ALL_USERS disables the gate, exactly as it does
|
||||
for _on_invite, even when the inviter is unknown."""
|
||||
monkeypatch.setenv("GATEWAY_ALLOW_ALL_USERS", "true")
|
||||
adapter = _make_adapter()
|
||||
adapter._allowed_user_ids = set()
|
||||
|
||||
sync_data = _invite_sync_data(invite_state=None)
|
||||
adapter._schedule_pending_invite_joins(sync_data)
|
||||
await _drain_invite_tasks(adapter)
|
||||
|
||||
adapter._join_room_by_id.assert_awaited_once_with("!pending_room:example.org")
|
||||
adapter._record_dm_room.assert_not_awaited()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_allow_all_logs_direct_invite_without_inviter(
|
||||
self, monkeypatch, caplog
|
||||
):
|
||||
monkeypatch.setenv("GATEWAY_ALLOW_ALL_USERS", "true")
|
||||
adapter = _make_adapter()
|
||||
adapter._allowed_user_ids = set()
|
||||
|
||||
sync_data = _invite_sync_data(
|
||||
invite_state={"events": [_member_invite_event(sender="")]}
|
||||
)
|
||||
with caplog.at_level(
|
||||
logging.WARNING,
|
||||
logger="plugins.platforms.matrix.adapter",
|
||||
):
|
||||
adapter._schedule_pending_invite_joins(sync_data)
|
||||
await _drain_invite_tasks(adapter)
|
||||
|
||||
adapter._join_room_by_id.assert_awaited_once_with("!pending_room:example.org")
|
||||
adapter._record_dm_room.assert_not_awaited()
|
||||
assert [record.getMessage() for record in caplog.records] == [
|
||||
"Matrix: joining direct invite to !pending_room:example.org "
|
||||
"without recording it in m.direct because the invite state "
|
||||
"has no inviter"
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user