test: add M10 integration tests and coverage to 99% (M10-U7)
Integration tests validating the 3 M10 acceptance criteria end-to-end with mocked slixmpp: 1. XmppChannel.send sends formatted direct message 2. XMPP error → PipelineWarning, no unhandled exception 3. No secret in XMPP logs (sentinel-based verification) Additional unit tests covering daemon-thread branch of SyncXmppChannel, deferred failed_auth, session timeout, disconnected handler, and _format_message edge cases. Channels coverage: 99.18%. All M10 checklist items marked complete in TODO.md. Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid> Co-authored-by: opencode/coder <coder@agents.invalid>
This commit is contained in:
@@ -16,6 +16,8 @@ créer de nouvelle event loop inutilement. Le comportement est :
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import threading
|
||||
from datetime import date
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
@@ -189,6 +191,92 @@ class TestSyncXmppChannelAsyncioRun:
|
||||
# Vérifier que le résultat est False
|
||||
assert result is False
|
||||
|
||||
def test_sync_adapter_send_with_running_loop_uses_daemon_thread(
|
||||
self, xmpp_settings: XmppSettings, xmpp_message: XmppMessage
|
||||
) -> None:
|
||||
"""Test que send bascule dans un thread démon quand une event loop est active.
|
||||
|
||||
Simule un appel depuis une boucle déjà en cours d'exécution : l'envoi
|
||||
passe alors par le thread daemon joint avec un timeout, et retourne
|
||||
True en cas de succès.
|
||||
|
||||
:param xmpp_settings: Paramètres XMPP valides.
|
||||
:param xmpp_message: Message XMPP minimal.
|
||||
"""
|
||||
|
||||
async def _inner() -> None:
|
||||
with patch("pronote_sync.channels.xmpp.XmppChannel") as MockChannel:
|
||||
mock_instance = MockChannel.return_value
|
||||
mock_instance.send_async = AsyncMock(return_value=True)
|
||||
mock_instance.settings = xmpp_settings
|
||||
|
||||
adapter = SyncXmppChannel(xmpp_settings)
|
||||
result = adapter.send(xmpp_message)
|
||||
|
||||
assert result is True
|
||||
|
||||
asyncio.run(_inner())
|
||||
|
||||
def test_sync_adapter_send_with_running_loop_error_returns_false(
|
||||
self, xmpp_settings: XmppSettings, xmpp_message: XmppMessage
|
||||
) -> None:
|
||||
"""Test que send retourne False quand le thread démon lève une exception.
|
||||
|
||||
Une exception survenue dans le thread daemon est capturée par la cible
|
||||
du thread puis journalisée sans secret ; send retourne False sans lever.
|
||||
|
||||
:param xmpp_settings: Paramètres XMPP valides.
|
||||
:param xmpp_message: Message XMPP minimal.
|
||||
"""
|
||||
|
||||
async def _inner() -> None:
|
||||
with patch("pronote_sync.channels.xmpp.XmppChannel") as MockChannel:
|
||||
mock_instance = MockChannel.return_value
|
||||
mock_instance.send_async = AsyncMock(
|
||||
side_effect=RuntimeError("Connexion impossible")
|
||||
)
|
||||
mock_instance.settings = xmpp_settings
|
||||
|
||||
adapter = SyncXmppChannel(xmpp_settings)
|
||||
result = adapter.send(xmpp_message)
|
||||
|
||||
assert result is False
|
||||
|
||||
asyncio.run(_inner())
|
||||
|
||||
def test_sync_adapter_send_with_running_loop_timeout_returns_false(
|
||||
self, xmpp_settings: XmppSettings, xmpp_message: XmppMessage
|
||||
) -> None:
|
||||
"""Test que send retourne False quand le thread démon dépasse le timeout.
|
||||
|
||||
Le thread démon reste bloqué au-delà du délai de jointure : send doit
|
||||
journaliser un avertissement et retourner False sans lever.
|
||||
|
||||
:param xmpp_settings: Paramètres XMPP valides.
|
||||
:param xmpp_message: Message XMPP minimal.
|
||||
"""
|
||||
blocker = threading.Event()
|
||||
|
||||
async def _blocking_send(message: XmppMessage) -> bool:
|
||||
while not blocker.is_set():
|
||||
await asyncio.sleep(0.01)
|
||||
return True
|
||||
|
||||
async def _inner() -> None:
|
||||
settings = xmpp_settings.model_copy(update={"timeout": 1})
|
||||
with patch("pronote_sync.channels.xmpp.XmppChannel") as MockChannel:
|
||||
mock_instance = MockChannel.return_value
|
||||
mock_instance.send_async = _blocking_send
|
||||
mock_instance.settings = settings
|
||||
|
||||
adapter = SyncXmppChannel(settings)
|
||||
result = adapter.send(xmpp_message)
|
||||
|
||||
assert result is False
|
||||
|
||||
asyncio.run(_inner())
|
||||
blocker.set()
|
||||
|
||||
|
||||
class TestSyncXmppChannelSecurity:
|
||||
"""Tests de sécurité pour SyncXmppChannel (non-fuite de secrets).
|
||||
|
||||
Reference in New Issue
Block a user