- mod_vnc_lastactivity: the jabber:iq:last handler returns <forbidden> when the query has no 'to'; send it to another existing user's bare JID and assert the result carries a 'seconds' attribute - set body via msg["body"] and do not await message send() (returns None) - use unique uuid room names instead of a fixed duplicated localpart - unlock the freshly-created (locked) room with configure_muc before a second user joins / is affiliated - the unregister IQ is fire-and-forget (no reply); tolerate IqTimeout rather than failing the kick-tracking smoke test Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Part-of: <http://gitlab.vnc.biz/uxf/vnctalk-prosody/-/merge_requests/3>
142 lines
6.7 KiB
Python
142 lines
6.7 KiB
Python
"""Zero-coverage module load detection.
|
|
|
|
These tests send minimal IQ/presence probes to verify that modules
|
|
which have no dedicated test coverage are at least loaded and not
|
|
crashing on entry. A module that fails to load after a Prosody upgrade
|
|
will typically return service-unavailable or item-not-found.
|
|
|
|
For modules without any IQ/presence hook, see MANUAL_TESTS.md §4.
|
|
"""
|
|
import uuid
|
|
import pytest
|
|
import slixmpp
|
|
import xml.etree.ElementTree as ET
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
class TestModuleLoadDetection:
|
|
"""Smoke tests for modules that would otherwise have zero coverage."""
|
|
|
|
async def test_mod_vnc_lastactivity_loaded(self, xmpp_client, second_client):
|
|
"""mod_vnc_lastactivity hooks on jabber:iq:last and jabber:iq:batch.
|
|
A get request should return a result (not service-unavailable).
|
|
|
|
The handler requires a 'to' on the query: a jabber:iq:last get with no
|
|
target returns <forbidden>, so we query another existing user's bare JID.
|
|
"""
|
|
target = second_client.boundjid.bare
|
|
iq = xmpp_client.make_iq_get(ito=target)
|
|
ET.SubElement(iq.xml, "{jabber:iq:last}query")
|
|
try:
|
|
res = await iq.send()
|
|
assert res["type"] == "result", (
|
|
f"mod_vnc_lastactivity returned {res['type']} instead of result — module may not be loaded"
|
|
)
|
|
q = res.xml.find("{jabber:iq:last}query")
|
|
assert q is not None and q.get("seconds") is not None, (
|
|
"jabber:iq:last result missing the 'seconds' attribute"
|
|
)
|
|
except slixmpp.exceptions.IqError as e:
|
|
pytest.fail(f"mod_vnc_lastactivity returned an error ({e.condition}) — module may not be loaded")
|
|
|
|
async def test_mod_vnc_delfile_message_hook_present(self, xmpp_client, xmpp_config, second_client):
|
|
"""mod_vnc_delfile hooks on pre-message with a message-correction trigger.
|
|
We verify it doesn't crash by sending a message with <replace>.
|
|
The module has no IQ handler; we just verify no error bounces back.
|
|
"""
|
|
if xmpp_client.boundjid.bare == second_client.boundjid.bare:
|
|
pytest.skip("Need two clients for message roundtrip")
|
|
|
|
msg = xmpp_client.make_message(mto=second_client.boundjid.bare, mtype="chat")
|
|
msg["body"] = "delfile test" # slixmpp: dict-style, not msg.body
|
|
msg["id"] = "delfile-test-1"
|
|
replace = ET.SubElement(msg.xml, "{urn:xmpp:message-correct:0}replace")
|
|
replace.set("id", "original-msg-id")
|
|
|
|
try:
|
|
msg.send() # message.send() returns None, not awaitable
|
|
# If no exception and no error bounce, the module at least didn't crash.
|
|
# The actual HTTP POST to del_api_url is an async side-effect we can't observe.
|
|
except Exception as e:
|
|
pytest.fail(f"mod_vnc_delfile message hook crashed: {e}")
|
|
|
|
async def test_mod_vnc_remotemucstore_message_hook_present(self, xmpp_client, xmpp_config, second_client):
|
|
"""mod_vnc_remotemucstore hooks on message/bare and message/full.
|
|
We send a normal chat message and verify no crash.
|
|
"""
|
|
if xmpp_client.boundjid.bare == second_client.boundjid.bare:
|
|
pytest.skip("Need two clients for message roundtrip")
|
|
|
|
msg = xmpp_client.make_message(mto=second_client.boundjid.bare, mtype="chat")
|
|
msg["body"] = "remotemucstore test" # slixmpp: dict-style, not msg.body
|
|
try:
|
|
msg.send() # message.send() returns None, not awaitable
|
|
except Exception as e:
|
|
pytest.fail(f"mod_vnc_remotemucstore message hook crashed: {e}")
|
|
|
|
async def test_mod_vnc_remotemucinvite_event_handler_present(self, xmpp_client, xmpp_config, second_client):
|
|
"""mod_vnc_remotemucinvite hooks on vnc-muc-invitation event.
|
|
We trigger a mediated invite and verify no crash.
|
|
"""
|
|
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.skip("Need two distinct accounts")
|
|
|
|
room_jid = f"reminv_{uuid.uuid4().hex[:8]}@{muc_domain}"
|
|
nick = "owner"
|
|
await xmpp_client.join_muc(room_jid, nick)
|
|
# unlock the freshly-created (locked) room so the invitee can join
|
|
await xmpp_client.configure_muc(room_jid)
|
|
await second_client.join_muc(room_jid, "invited")
|
|
|
|
try:
|
|
# Mediated invite
|
|
invite_msg = xmpp_client.make_message(mto=room_jid, mtype="normal")
|
|
x = ET.SubElement(invite_msg.xml, "{http://jabber.org/protocol/muc#user}x")
|
|
invite = ET.SubElement(x, "{http://jabber.org/protocol/muc#user}invite")
|
|
invite.set("to", second_client.boundjid.bare)
|
|
invite_msg.send() # message.send() returns None, not awaitable
|
|
# If no crash, the event handler is at least present.
|
|
except Exception as e:
|
|
pytest.fail(f"mod_vnc_remotemucinvite event hook crashed: {e}")
|
|
finally:
|
|
await xmpp_client.leave_muc(room_jid, nick)
|
|
await second_client.leave_muc(room_jid, "invited")
|
|
await xmpp_client.destroy_muc(room_jid)
|
|
|
|
async def test_mod_vnc_track_kicks_event_handler_present(self, xmpp_client, xmpp_config, second_client):
|
|
"""mod_vnc_track_kicks hooks on vnc-muc-kick event.
|
|
We trigger it via unregister IQ (which fires vnc-muc-kick) and verify no crash.
|
|
"""
|
|
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.skip("Need two distinct accounts")
|
|
|
|
room_jid = f"trackk_{uuid.uuid4().hex[:8]}@{muc_domain}"
|
|
nick1 = "owner"
|
|
nick2 = "member"
|
|
|
|
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 xmpp_client.set_muc_affiliation(room_jid, second_client.boundjid.bare, "member")
|
|
|
|
try:
|
|
iq = second_client.make_iq_set(ito=room_jid)
|
|
ET.SubElement(iq.xml, "{xmpp:vnctalk:unregister}query")
|
|
# handle_unregister_iq fires vnc-muc-kick but sends no IQ reply, so the
|
|
# send times out by design; that still proves the handler ran without crashing.
|
|
try:
|
|
await iq.send(timeout=5)
|
|
except slixmpp.exceptions.IqTimeout:
|
|
pass
|
|
except Exception as e:
|
|
pytest.fail(f"mod_vnc_track_kicks event hook crashed: {e}")
|
|
finally:
|
|
await xmpp_client.leave_muc(room_jid, nick1)
|
|
await xmpp_client.destroy_muc(room_jid)
|