fix(xmpp): remplacer use_tls par tls_mode et borner les phases de connexion
Cause racine (#25) : use_tls=True (défaut) activait le direct TLS sur le port 5222 (conventionnellement STARTTLS). Le client envoyait un ClientHello TLS sur un port attendant un stream XMPP en clair, le serveur ne voyait jamais l'identité configurée, et la session expirait après 30 s. Corrections : - Remplacer use_tls (bool) par tls_mode: Literal[direct|starttls|disabled] (défaut starttls, compatible port 5222). use_tls conservé comme alias déprécié avec DeprecationWarning. - Ajouter connect_timeout (15 s) et cleanup_timeout (10 s) distincts du timeout de session (30 s). - Gérer l'événement connection_failed de Slixmpp pour échouer rapidement au lieu d'attendre le timeout de session. - Borner await connect_future et await disconnect_future par leurs timeouts respectifs (anti-blocage). - Attendre connect_future et session_future conjointement (asyncio.wait, FIRST_COMPLETED) pour détecter connection_failed avant l'expiration du connect_timeout. - Annuler les tâches pending sur tous les chemins de retour, y compris CancelledError et Exception. - Redact tous les redact_exception avec extra_secrets=_secret_values(). - Construire ClientXMPP dans le try (contrat « never raises »). - Enrichir FakeClientXMPP avec modes connect/disconnect configurables. - 25 nouveaux tests (timeout connexion, connection_failed, cleanup bloqué, CancelledError, TLS mismatch, fuite secrets). Couverture 96 %. - Mettre à jour .env.example, GUIDE_DEV_PYTHON.md, README.LLM.md. Co-authored-by: OpenCode <opencode@antoineve.me>
This commit is contained in:
@@ -99,19 +99,52 @@ class TestTimeoutConstraints:
|
||||
class TestTlsPolicy:
|
||||
"""Tests de la politique TLS pour le champ use_tls."""
|
||||
|
||||
def test_use_tls_false_with_remote_host_rejected(self) -> None:
|
||||
"""Vérifie que use_tls=False avec un hôte distant est rejeté.
|
||||
def test_tls_disabled_with_remote_host_rejected(self) -> None:
|
||||
"""Vérifie que tls_mode="disabled" avec un hôte distant est rejeté.
|
||||
|
||||
:raises ValidationError: Si use_tls=False et host n'est pas une boucle locale.
|
||||
:raises ValidationError: Si tls_mode="disabled" et host n'est pas une boucle locale.
|
||||
"""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
XmppSettings(use_tls=False, host="talk.example.com")
|
||||
assert "use_tls" in str(exc_info.value).lower() or "tls" in str(exc_info.value).lower()
|
||||
XmppSettings(tls_mode="disabled", host="talk.example.com")
|
||||
assert "tls" in str(exc_info.value).lower()
|
||||
|
||||
def test_use_tls_false_with_remote_host_rejected_when_enabled(self) -> None:
|
||||
def test_tls_disabled_with_remote_host_rejected_when_enabled(self) -> None:
|
||||
"""Vérifie que la politique TLS s'applique même quand le canal est activé."""
|
||||
with pytest.raises(ValidationError):
|
||||
XmppSettings(enabled=True, use_tls=False, host="talk.example.com")
|
||||
XmppSettings(enabled=True, tls_mode="disabled", host="talk.example.com")
|
||||
|
||||
def test_use_tls_false_maps_to_starttls(self) -> None:
|
||||
"""Vérifie que l'alias obsolète use_tls=False mappe vers tls_mode='starttls'.
|
||||
|
||||
:return: Vérifie le mapping et l'émission d'un DeprecationWarning.
|
||||
:rtype: None
|
||||
"""
|
||||
with pytest.warns(DeprecationWarning):
|
||||
settings = XmppSettings(use_tls=False, host="talk.example.com")
|
||||
assert settings.tls_mode == "starttls"
|
||||
assert settings.host == "talk.example.com"
|
||||
|
||||
def test_use_tls_true_maps_to_direct(self) -> None:
|
||||
"""Vérifie que l'alias obsolète use_tls=True mappe vers tls_mode='direct'.
|
||||
|
||||
:return: Vérifie le mapping et l'émission d'un DeprecationWarning.
|
||||
:rtype: None
|
||||
"""
|
||||
with pytest.warns(DeprecationWarning):
|
||||
settings = XmppSettings(use_tls=True, host="talk.example.com")
|
||||
assert settings.tls_mode == "direct"
|
||||
assert settings.host == "talk.example.com"
|
||||
|
||||
def test_default_tls_mode_is_starttls(self) -> None:
|
||||
"""Vérifie que tls_mode vaut 'starttls' par défaut avec les timeouts par défaut.
|
||||
|
||||
:return: Vérifie tls_mode='starttls', connect_timeout=15, cleanup_timeout=10.
|
||||
:rtype: None
|
||||
"""
|
||||
settings = XmppSettings()
|
||||
assert settings.tls_mode == "starttls"
|
||||
assert settings.connect_timeout == 15
|
||||
assert settings.cleanup_timeout == 10
|
||||
|
||||
def test_use_tls_false_with_localhost_allowed(self) -> None:
|
||||
"""Vérifie que use_tls=False avec localhost est autorisé.
|
||||
@@ -120,7 +153,7 @@ class TestTlsPolicy:
|
||||
:rtype: None
|
||||
"""
|
||||
settings = XmppSettings(use_tls=False, host="localhost")
|
||||
assert settings.use_tls is False
|
||||
assert settings.tls_mode == "starttls"
|
||||
assert settings.host == "localhost"
|
||||
|
||||
def test_use_tls_false_with_127_allowed(self) -> None:
|
||||
@@ -130,7 +163,7 @@ class TestTlsPolicy:
|
||||
:rtype: None
|
||||
"""
|
||||
settings = XmppSettings(use_tls=False, host="127.0.0.1")
|
||||
assert settings.use_tls is False
|
||||
assert settings.tls_mode == "starttls"
|
||||
assert settings.host == "127.0.0.1"
|
||||
|
||||
def test_use_tls_false_with_ipv6_loopback_allowed(self) -> None:
|
||||
@@ -140,7 +173,7 @@ class TestTlsPolicy:
|
||||
:rtype: None
|
||||
"""
|
||||
settings = XmppSettings(use_tls=False, host="::1")
|
||||
assert settings.use_tls is False
|
||||
assert settings.tls_mode == "starttls"
|
||||
assert settings.host == "::1"
|
||||
|
||||
def test_use_tls_true_with_remote_host_allowed(self) -> None:
|
||||
@@ -150,7 +183,7 @@ class TestTlsPolicy:
|
||||
:rtype: None
|
||||
"""
|
||||
settings = XmppSettings(use_tls=True, host="talk.example.com")
|
||||
assert settings.use_tls is True
|
||||
assert settings.tls_mode == "direct"
|
||||
assert settings.host == "talk.example.com"
|
||||
|
||||
def test_use_tls_true_with_empty_host_allowed(self) -> None:
|
||||
@@ -160,7 +193,7 @@ class TestTlsPolicy:
|
||||
:rtype: None
|
||||
"""
|
||||
settings = XmppSettings(use_tls=True, host="")
|
||||
assert settings.use_tls is True
|
||||
assert settings.tls_mode == "direct"
|
||||
assert settings.host == ""
|
||||
|
||||
|
||||
@@ -173,7 +206,7 @@ class TestNoSecretInErrorMessages:
|
||||
Crée une instance avec des valeurs sensibles et vérifie que l'erreur de validation
|
||||
ne contient pas ces valeurs dans son message.
|
||||
|
||||
:raises ValidationError: Si use_tls=False avec un hôte non-local.
|
||||
:raises ValidationError: Si tls_mode="disabled" avec un hôte non-local.
|
||||
:return: Vérifie que le message d'erreur ne contient pas les secrets.
|
||||
:rtype: None
|
||||
"""
|
||||
@@ -184,7 +217,7 @@ class TestNoSecretInErrorMessages:
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
XmppSettings(
|
||||
use_tls=False,
|
||||
tls_mode="disabled",
|
||||
host="talk.example.com",
|
||||
jid=sentinel_jid,
|
||||
password=sentinel_password,
|
||||
@@ -197,3 +230,72 @@ class TestNoSecretInErrorMessages:
|
||||
assert "test_password_123" not in error_message
|
||||
assert "test_to@example.com" not in error_message
|
||||
assert "secret" not in error_message
|
||||
|
||||
|
||||
class TestXmppSettingsTlsMode:
|
||||
"""Tests supplémentaires pour les validateurs de XmppSettings."""
|
||||
|
||||
def test_use_tls_true_maps_to_direct_with_warning(self) -> None:
|
||||
"""Vérifie que use_tls=True mappe vers tls_mode='direct' avec DeprecationWarning.
|
||||
|
||||
:return: Vérifie le mapping et l'émission d'un DeprecationWarning.
|
||||
:rtype: None
|
||||
"""
|
||||
with pytest.warns(DeprecationWarning):
|
||||
settings = XmppSettings(use_tls=True, host="talk.example.com")
|
||||
assert settings.tls_mode == "direct"
|
||||
|
||||
def test_use_tls_false_maps_to_starttls_with_warning(self) -> None:
|
||||
"""Vérifie que use_tls=False mappe vers tls_mode='starttls' avec DeprecationWarning.
|
||||
|
||||
:return: Vérifie le mapping et l'émission d'un DeprecationWarning.
|
||||
:rtype: None
|
||||
"""
|
||||
with pytest.warns(DeprecationWarning):
|
||||
settings = XmppSettings(use_tls=False, host="talk.example.com")
|
||||
assert settings.tls_mode == "starttls"
|
||||
|
||||
def test_tls_mode_disabled_rejected_for_remote(self) -> None:
|
||||
"""Vérifie que tls_mode='disabled' avec un hôte distant est rejeté.
|
||||
|
||||
:raises ValidationError: Si tls_mode="disabled" et host n'est pas une boucle locale.
|
||||
"""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
XmppSettings(tls_mode="disabled", host="example.com")
|
||||
assert "tls" in str(exc_info.value).lower()
|
||||
|
||||
def test_tls_mode_disabled_allowed_for_loopback(self) -> None:
|
||||
"""Vérifie que tls_mode='disabled' avec host='127.0.0.1' est valide.
|
||||
|
||||
:return: Vérifie que XmppSettings(tls_mode='disabled', host='127.0.0.1') est valide.
|
||||
:rtype: None
|
||||
"""
|
||||
settings = XmppSettings(tls_mode="disabled", host="127.0.0.1")
|
||||
assert settings.tls_mode == "disabled"
|
||||
assert settings.host == "127.0.0.1"
|
||||
|
||||
def test_connect_timeout_must_be_positive(self) -> None:
|
||||
"""Vérifie que connect_timeout doit être strictement positif.
|
||||
|
||||
:raises ValidationError: Si connect_timeout <= 0.
|
||||
"""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
XmppSettings(connect_timeout=0)
|
||||
assert "connect_timeout" in str(exc_info.value).lower()
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
XmppSettings(connect_timeout=-1)
|
||||
assert "connect_timeout" in str(exc_info.value).lower()
|
||||
|
||||
def test_cleanup_timeout_must_be_positive(self) -> None:
|
||||
"""Vérifie que cleanup_timeout doit être strictement positif.
|
||||
|
||||
:raises ValidationError: Si cleanup_timeout <= 0.
|
||||
"""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
XmppSettings(cleanup_timeout=0)
|
||||
assert "cleanup_timeout" in str(exc_info.value).lower()
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
XmppSettings(cleanup_timeout=-1)
|
||||
assert "cleanup_timeout" in str(exc_info.value).lower()
|
||||
|
||||
Reference in New Issue
Block a user