"""Matrix client validation: room_id format and quoting.""" from __future__ import annotations import aiohttp import pytest from aioresponses import aioresponses from notify_bridge_core.notifications.matrix.client import MatrixClient HOMESERVER = "https://matrix.example.com" TOKEN = "secret-bearer-token-1234567890" @pytest.mark.asyncio async def test_rejects_path_injection_room_id() -> None: async with aiohttp.ClientSession() as sess: client = MatrixClient(sess, HOMESERVER, TOKEN) result = await client.send_message("!abc:host/../../etc/passwd", "hi") assert result["success"] is False assert "room_id" in result["error"] @pytest.mark.asyncio async def test_rejects_empty_room_id() -> None: async with aiohttp.ClientSession() as sess: client = MatrixClient(sess, HOMESERVER, TOKEN) result = await client.send_message("", "hi") assert result["success"] is False assert "room_id" in result["error"] @pytest.mark.asyncio async def test_rejects_unicode_control_chars_in_room_id() -> None: async with aiohttp.ClientSession() as sess: client = MatrixClient(sess, HOMESERVER, TOKEN) result = await client.send_message("!abc\x00:host", "hi") assert result["success"] is False @pytest.mark.asyncio async def test_url_encodes_room_id_special_chars() -> None: """``!`` and ``:`` must reach the server URL-encoded.""" captured: list[str] = [] with aioresponses() as mocked: # Match any PUT under the rooms path; capture the URL we got. mocked.put( "https://matrix.example.com/_matrix/client/v3/rooms/%21abc%3Ahost.example/send/m.room.message", status=200, body='{}', repeat=True, ) # aioresponses doesn't expose URL templates well, so use a regex mock. import re mocked.put( re.compile(r"https://matrix\.example\.com/_matrix/client/v3/rooms/[^/]+/send/m\.room\.message/.*"), status=200, body='{}', repeat=True, ) async with aiohttp.ClientSession() as sess: client = MatrixClient(sess, HOMESERVER, TOKEN) result = await client.send_message("!abc:host.example", "hi") assert result["success"] is True @pytest.mark.asyncio async def test_redacts_bearer_in_error() -> None: """A 4xx response body must not echo the Authorization Bearer back to caller.""" import re with aioresponses() as mocked: mocked.put( re.compile(r".*"), status=403, body='{"errcode": "M_FORBIDDEN", "Authorization": "Bearer ' + TOKEN + '"}', repeat=True, ) async with aiohttp.ClientSession() as sess: client = MatrixClient(sess, HOMESERVER, TOKEN) result = await client.send_message("!abc:host.example", "hi") assert result["success"] is False assert TOKEN not in result["error"]