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:
kshitijk4poor
2026-09-20 16:51:02 +05:30
committed by kshitij
parent 439f2b1cf6
commit f49fa66eb6
2 changed files with 0 additions and 172 deletions

View File

@@ -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
# ---------------------------------------------------------------------------

View File

@@ -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"
]