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>
This commit is contained in:
27
pronote_sync/channels/protocol.py
Normal file
27
pronote_sync/channels/protocol.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""Protocole abstrait définissant le contrat des canaux de sortie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
from pronote_sync.models.xmpp import XmppMessage
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class Channel(Protocol):
|
||||
"""Contrat structurel d'un canal de sortie du pipeline.
|
||||
|
||||
Un canal de sortie reçoit un message final :class:`XmppMessage` et tente de
|
||||
l'envoyer vers la destination qu'il représente (CalDAV, XMPP, etc.).
|
||||
|
||||
:ivar send: Envoie un message sur le canal.
|
||||
"""
|
||||
|
||||
def send(self, message: XmppMessage) -> bool:
|
||||
"""Envoie un message sur le canal.
|
||||
|
||||
:param message: Message final à transmettre.
|
||||
:return: ``True`` si l'envoi a réussi, ``False`` sinon.
|
||||
:rtype: bool
|
||||
"""
|
||||
...
|
||||
202
tests/unit/test_channel_protocol.py
Normal file
202
tests/unit/test_channel_protocol.py
Normal file
@@ -0,0 +1,202 @@
|
||||
"""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}"
|
||||
)
|
||||
Reference in New Issue
Block a user