Files
college-infos/tests/unit/test_channel_protocol.py
Antoine Van Elstraete 7d765476de feat: define Channel Protocol for output channels (M10-U2)
Add @runtime_checkable Channel Protocol with send(XmppMessage) -> bool
as the structural contract for all output channels (XMPP, future CalDAV, etc).

6 unit tests covering protocol structure, conforming/non-conforming classes,
method signature introspection, and bool return type.

Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-07 21:04:01 +02:00

203 lines
7.5 KiB
Python

"""Tests unitaires pour le Protocol Channel.
Ce module valide la spécification du Protocol ``Channel`` qui sera ajouté
à ``pronote_sync.channels.protocol``. Ces tests doivent être ROUGES tant que
le Protocol n'est pas implémenté.
"""
from __future__ import annotations
from datetime import date
import pytest
from pronote_sync.models.xmpp import XmppMessage
# Import du Protocol à valider (doit échouer tant qu'il n'existe pas)
try:
from pronote_sync.channels.protocol import Channel
CHANNEL_MODULE_EXISTS = True
except ImportError:
CHANNEL_MODULE_EXISTS = False
class TestChannelProtocol:
"""Tests pour le Protocol Channel."""
def test_channel_is_protocol(self) -> None:
"""Vérifie que Channel est un Protocol.
:return: None
:raises AssertionError: Si Channel n'est pas un Protocol.
"""
if not CHANNEL_MODULE_EXISTS:
pytest.fail(
"Le module pronote_sync.channels.protocol n'existe pas encore. "
"Ceci est attendu pour l'instant."
)
assert hasattr(Channel, "_is_protocol"), "Channel doit être un sous-type de typing.Protocol"
def test_channel_has_send_method(self) -> None:
"""Vérifie que le Protocol Channel définit une méthode send.
:return: None
:raises AssertionError: Si la méthode send n'est pas dans l'interface.
"""
if not CHANNEL_MODULE_EXISTS:
pytest.fail(
"Le module pronote_sync.channels.protocol n'existe pas encore. "
"Ceci est attendu pour l'instant."
)
assert hasattr(Channel, "send"), "Channel doit définir une méthode 'send'"
send_method = Channel.send
assert callable(send_method), "La méthode 'send' doit être callable"
def test_conforming_class_satisfies_protocol(self) -> None:
"""Vérifie qu'une classe conforme satisfait le Protocol Channel.
:return: None
:raises AssertionError: Si la classe conforme n'est pas acceptée.
"""
if not CHANNEL_MODULE_EXISTS:
pytest.fail(
"Le module pronote_sync.channels.protocol n'existe pas encore. "
"Ceci est attendu pour l'instant."
)
# Classe minimale conforme au Protocol
class DummyChannel:
"""Implémentation minimale conforme au Protocol Channel."""
def send(self, message: XmppMessage) -> bool:
"""Envoie un message XMPP.
:param message: Message à envoyer.
:return: True si l'envoi a réussi.
:rtype: bool
"""
return True
# Création d'une instance de message pour le test
test_message = XmppMessage(target_date=date(2025, 9, 7), synthesis=None, external_info=None)
# Instanciation et vérification
dummy_instance = DummyChannel()
assert dummy_instance.send(test_message) is True, "La méthode send doit retourner True"
# Vérification que l'instance satisfait le Protocol
if hasattr(Channel, "__protocol_attrs__"):
# Vérification runtime avec @runtime_checkable
assert isinstance(dummy_instance, Channel), (
"Une classe conforme doit satisfaire le Protocol Channel"
)
def test_non_conforming_class_does_not_satisfy_protocol(self) -> None:
"""Vérifie qu'une classe non conforme ne satisfait pas le Protocol Channel.
:return: None
:raises AssertionError: Si la classe non conforme est acceptée.
"""
if not CHANNEL_MODULE_EXISTS:
pytest.fail(
"Le module pronote_sync.channels.protocol n'existe pas encore. "
"Ceci est attendu pour l'instant."
)
# Classe minimale non conforme (sans méthode send)
class NonConformingChannel:
"""Implémentation minimale non conforme au Protocol Channel."""
pass
# Vérification que la classe ne satisfait pas le Protocol
non_conforming_instance = NonConformingChannel()
if hasattr(Channel, "__protocol_attrs__"):
# Vérification runtime avec @runtime_checkable
assert not isinstance(non_conforming_instance, Channel), (
"Une classe non conforme ne doit pas satisfaire le Protocol Channel"
)
def test_channel_send_returns_bool(self) -> None:
"""Vérifie que la méthode send retourne un booléen.
:return: None
:raises AssertionError: Si le retour n'est pas de type bool.
"""
if not CHANNEL_MODULE_EXISTS:
pytest.fail(
"Le module pronote_sync.channels.protocol n'existe pas encore. "
"Ceci est attendu pour l'instant."
)
# Implémentation minimale retournant True
class BoolReturningChannel:
"""Implémentation minimale retournant un booléen."""
def send(self, message: XmppMessage) -> bool:
"""Envoie un message XMPP.
:param message: Message à envoyer.
:return: True
:rtype: bool
"""
return True
# Création d'une instance de message pour le test
test_message = XmppMessage(target_date=date(2025, 9, 7), synthesis=None, external_info=None)
# Test du retour
channel = BoolReturningChannel()
result = channel.send(test_message)
assert isinstance(result, bool), "La méthode send doit retourner un booléen"
assert result is True, "La méthode send doit retourner True dans cette implémentation"
def test_channel_send_signature(self) -> None:
"""Vérifie la signature déclarée de la méthode Channel.send.
:return: None
:raises AssertionError: Si la signature ne correspond pas aux attentes.
"""
if not CHANNEL_MODULE_EXISTS:
pytest.fail(
"Le module pronote_sync.channels.protocol n'existe pas encore. "
"Ceci est attendu pour l'instant."
)
import inspect
# Vérification de l'existence et de la nature callable de la méthode
assert hasattr(Channel, "send"), "Channel doit définir une méthode 'send'"
send_method = Channel.send
assert callable(send_method), "La méthode 'send' doit être callable"
# Introspection de la signature
sig = inspect.signature(send_method)
params = list(sig.parameters.values())
# Vérification du nombre de paramètres (1 paramètre + self)
# On exclut 'self' pour vérifier le paramètre 'message'
param_count = len(params)
assert param_count == 2, (
f"La méthode send doit avoir exactement 2 paramètres (self + message), "
f"trouvé {param_count}"
)
# Vérification du nom du paramètre (on ignore 'self')
param_names = [p.name for p in params if p.name != "self"]
assert len(param_names) == 1, "Doit avoir exactement un paramètre autre que self"
param_name = param_names[0]
assert param_name == "message", (
f"Le paramètre doit s'appeler 'message', trouvé '{param_name}'"
)
# Vérification du type de retour
return_annotation = sig.return_annotation
# Le type de retour peut être soit la chaîne 'bool' soit le type bool (forward reference)
assert return_annotation in (bool, "bool"), (
f"Le type de retour doit être 'bool' ou bool, trouvé {return_annotation}"
)