fix(M5): corrections d'audit — transport HTTP, statuts d'erreur, cache atomique
Corrige les 5 points de l'audit FIXME_M5 : 1. (Bloquant) Sépare transport HTTP et parsing : utilise requests.get() avec timeout explicite et en-têtes conditionnels, puis transmet le contenu à feedparser.parse() — supprime le paramètre inexistant request_timeout qui faisait échouer toute récupération réelle. 2. Rejette les statuts HTTP 4xx/5xx via raise_for_status() avant le parsing. 3. Préserve les validateurs de cache (etag, last_modified) d'entrée sur les chemins d'échec (exception, bozo) au lieu de les écraser à None. 4. Sauvegarde atomique de BlogRSSState : écrit dans un .tmp puis Path.replace() pour éviter la corruption sur interruption. 5. Déduplication normale silencieuse : les GUID déjà connus sont ignorés sans warning ; seuls les doublons intra-flux génèrent un avertissement. Tests : 49 tests (32 client + 17 state) dont 11 nouveaux couvrant transport HTTP réel, statuts 401/404/500, préservation des validateurs, en-têtes conditionnels, doublons intra-flux et sauvegarde atomique. Guide : §5 bis.7.1 aligné avec le nouveau pattern transport/parsing. Co-authored-by: opencode/coder <coder@agents.invalid> Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
This commit is contained in:
@@ -15,6 +15,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -308,4 +309,42 @@ def test_get_known_guids_returns_frozenset(tmp_path: Path) -> None:
|
||||
assert type(result) is frozenset
|
||||
|
||||
|
||||
def test_atomic_save_preserves_on_error(tmp_path: Path) -> None:
|
||||
"""Vérifie que l'état original est préservé en cas d'erreur lors de la sauvegarde atomique.
|
||||
|
||||
Si une erreur survient pendant le remplacement atomique du fichier,
|
||||
le fichier original doit rester intact et le fichier temporaire doit être nettoyé.
|
||||
|
||||
:param tmp_path: Fixture pytest pour un répertoire temporaire.
|
||||
:return: None
|
||||
"""
|
||||
state_file = tmp_path / "state.json"
|
||||
|
||||
# Créer un état initial avec des GUID
|
||||
state = BlogRSSState(state_file)
|
||||
state.add_guids(["original-guid-1", "original-guid-2"])
|
||||
|
||||
# Lire le contenu original
|
||||
original_content = state_file.read_text(encoding="utf-8")
|
||||
|
||||
# Mock Path.replace pour simuler une erreur pendant le remplacement atomique
|
||||
with patch.object(Path, "replace") as mock_replace:
|
||||
mock_replace.side_effect = OSError("Simulated atomic replace failure")
|
||||
|
||||
# Essayer d'ajouter de nouveaux GUID, ce qui déclenchera _save()
|
||||
state.add_guids(["new-guid"])
|
||||
|
||||
# Vérifier que le fichier original est toujours intact
|
||||
assert state_file.read_text(encoding="utf-8") == original_content
|
||||
|
||||
# Vérifier que le fichier temporaire a été nettoyé
|
||||
tmp_file = state_file.with_suffix(".tmp")
|
||||
assert not tmp_file.exists()
|
||||
|
||||
# Vérifier que l'état en mémoire n'a pas été modifié (car la sauvegarde a échoué)
|
||||
# Note: En réalité, l'état en mémoire est modifié mais pas persistant
|
||||
# C'est le fichier qui doit rester intact
|
||||
assert state.get_known_guids() == frozenset({"original-guid-1", "original-guid-2", "new-guid"})
|
||||
|
||||
|
||||
# Ensure trailing newline
|
||||
|
||||
Reference in New Issue
Block a user