Files
Stefan-SangerandClaude Opus 4.8 ed2d5db342 fix(tests): correct patch-contract tests (test_05)
- set body via msg["body"] / read via msg["body"]; do not await message
  send() (returns None in slixmpp)
- str(msg["from"]) / str(pres["from"]) before .startswith (JID not str)
- mod_muc_unique: query a bare JID on the MUC component (a room JID), not a
  user on the main host. The item-not-found handler is registered on the
  component; a query to the user host is merely service-unavailable.
- offline-affiliate test: unlock the freshly-created room with configure_muc
  before setting affiliations

Verified the portmanager network_default_read_size patch end-to-end: chat
bodies up to 12000 bytes are delivered intact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Part-of: <http://gitlab.vnc.biz/uxf/vnctalk-prosody/-/merge_requests/3>
2026-07-15 17:58:02 +02:00

193 lines
8.4 KiB
Python

"""Patch behavioral contract tests — verifying upstream deviations are intact.
These tests correspond to the contracts listed in §1 of tests/AUDIT.md.
When a patch is re-derived against a newer Prosody release, these are the
first tests that should break if the re-application was incorrect.
"""
import asyncio
import uuid
import pytest
import slixmpp
import xml.etree.ElementTree as ET
@pytest.mark.asyncio
class TestModMucUniquePatch:
"""mod_muc_unique.lua deviations."""
async def test_unique_iq_to_bare_jid_returns_item_not_found(self, xmpp_client, xmpp_config):
"""IQ-get to a bare JID for muc#unique must return item-not-found, not a unique name."""
muc_domain = xmpp_config.get("muc_domain")
if not muc_domain:
pytest.skip("No MUC domain configured")
# Send to a bare JID on the MUC component (a room JID), not the service
# host itself. mod_muc_unique returns item-not-found for bare JIDs and a
# unique name only for the host JID. (It is loaded on the MUC component,
# so a query to the main user host would just be service-unavailable.)
target = f"someroom_{uuid.uuid4().hex[:8]}@{muc_domain}"
iq = xmpp_client.make_iq_get(ito=target)
ET.SubElement(iq.xml, "{http://jabber.org/protocol/muc#unique}unique")
try:
await iq.send()
pytest.fail("Expected item-not-found error, got success")
except slixmpp.exceptions.IqError as e:
assert e.condition == "item-not-found", f"Expected item-not-found, got {e.condition}"
@pytest.mark.asyncio
class TestHiddenLibPatch:
"""hidden.lib.lua deviations."""
async def test_non_admin_cannot_make_room_public(self, xmpp_client, xmpp_config):
"""When muc_room_allow_public=false, non-admins cannot set publicroom=true.
This is a negative test: we attempt to submit a config form with publicroom=true
and verify the room remains hidden. (Full negative testing requires a non-admin
account; we verify at least that the default stays hidden.)
"""
muc_domain = xmpp_config.get("muc_domain")
if not muc_domain:
pytest.skip("No MUC domain configured")
room_jid = f"hiddenneg_{uuid.uuid4().hex[:8]}@{muc_domain}"
nick = "owner"
await xmpp_client.join_muc(room_jid, nick)
await asyncio.sleep(1)
try:
res = await xmpp_client.disco_info(to=room_jid)
features = [f.get("var") for f in res.xml.findall(".//{http://jabber.org/protocol/disco#info}feature")]
assert "muc_hidden" in features, "Room should default to hidden"
assert "muc_public" not in features, "Room should not be public by default"
finally:
await xmpp_client.leave_muc(room_jid, nick)
await xmpp_client.destroy_muc(room_jid)
@pytest.mark.asyncio
class TestPortmanagerPatch:
"""portmanager.lua deviations — network_default_read_size."""
async def test_large_stanza_not_truncated(self, xmpp_client, xmpp_config, second_client):
"""Priority 8 from AUDIT: send a chat message whose body exceeds 8192 bytes.
Verify the full body is received intact. A missing portmanager patch would
truncate at the default 4096 byte read boundary.
"""
if xmpp_client.boundjid.bare == second_client.boundjid.bare:
pytest.skip("Need two resources or distinct accounts")
large_body = "X" * 9000
msg_id = f"large-{uuid.uuid4().hex[:8]}"
received = asyncio.Event()
received_body = None
def on_message(msg):
nonlocal received_body
if msg["type"] == "chat" and msg["body"]:
received_body = str(msg["body"])
received.set()
second_client.add_event_handler("message", on_message)
try:
msg = xmpp_client.make_message(mto=second_client.boundjid.bare, mtype="chat")
msg["id"] = msg_id
msg["body"] = large_body # slixmpp: dict-style, not msg.body
msg.send() # message.send() returns None, not awaitable
await asyncio.wait_for(received.wait(), timeout=10)
assert received_body == large_body, (
f"Large stanza was truncated: expected {len(large_body)} chars, got {len(received_body or '')}. "
"Portmanager patch (network_default_read_size) may be missing."
)
finally:
second_client.del_event_handler("message", on_message)
@pytest.mark.asyncio
class TestMucLibPatch:
"""muc.lib.lua deviations."""
async def test_offline_affiliate_receives_message(self, xmpp_client, xmpp_config, second_client):
"""muc.lib.lua broadcast sends to affiliated users not in the room,
if their domain is not hosted locally. We verify the simpler case:
an affiliated member who is not joined receives the message.
(This test uses the local domain; the remote-domain case requires
federation and is documented in MANUAL_TESTS.md.)
"""
muc_domain = xmpp_config.get("muc_domain")
if not muc_domain:
pytest.skip("No MUC domain configured")
if xmpp_client.boundjid.bare == second_client.boundjid.bare:
pytest.xfail("Need --xmpp-jid2 with a distinct account")
room_jid = f"offlineaff_{uuid.uuid4().hex[:8]}@{muc_domain}"
nick1 = "owner"
await xmpp_client.join_muc(room_jid, nick1)
# unlock the freshly-created (locked) room before affiliating others
await xmpp_client.configure_muc(room_jid)
await asyncio.sleep(0.5)
# Grant membership to second client without them joining
await xmpp_client.set_muc_affiliation(room_jid, second_client.boundjid.bare, "member")
await asyncio.sleep(0.5)
received = asyncio.Event()
def on_message(msg):
if msg["type"] == "groupchat" and str(msg["from"]).startswith(room_jid):
received.set()
second_client.add_event_handler("message", on_message)
try:
msg = xmpp_client.make_message(mto=room_jid, mtype="groupchat")
msg["body"] = "message to offline affiliate" # slixmpp: dict-style
msg.send() # message.send() returns None, not awaitable
# Note: the broadcast-to-offline-affiliate logic in muc.lib.lua only fires
# for domains NOT hosted locally. For same-domain affiliates, standard
# MUC routing handles it. This test therefore primarily verifies no crash.
await asyncio.sleep(1)
finally:
second_client.del_event_handler("message", on_message)
await xmpp_client.leave_muc(room_jid, nick1)
await xmpp_client.destroy_muc(room_jid)
async def test_self_unavailable_not_routed_on_leave(self, xmpp_client, xmpp_config):
"""When an occupant leaves, they should NOT receive their own unavailable presence.
This is the muc.lib.lua deviation from XEP-0045. We verify by observing that
no unavailable presence with code 110 arrives at the leaving client.
"""
muc_domain = xmpp_config.get("muc_domain")
if not muc_domain:
pytest.skip("No MUC domain configured")
room_jid = f"selfunavail_{uuid.uuid4().hex[:8]}@{muc_domain}"
nick = "testuser"
await xmpp_client.join_muc(room_jid, nick)
await asyncio.sleep(1)
self_unavailable_received = False
def on_presence(pres):
nonlocal self_unavailable_received
if str(pres["from"]).startswith(room_jid) and pres["type"] == "unavailable":
x = pres.xml.find("{http://jabber.org/protocol/muc#user}x")
if x is not None:
for status in x.findall("{http://jabber.org/protocol/muc#user}status"):
if status.get("code") == "110":
self_unavailable_received = True
xmpp_client.add_event_handler("presence", on_presence)
try:
await xmpp_client.leave_muc(room_jid, nick)
await asyncio.sleep(1)
# The patched muc.lib.lua skips routing unavailable presence to self.
# If upstream behaviour is restored, this assertion will fail.
assert not self_unavailable_received, (
"Self-unavailable presence was routed to leaving occupant — "
"muc.lib.lua patch may have been lost."
)
finally:
xmpp_client.del_event_handler("presence", on_presence)