Ctrl K
ComunidadGuías…

fia-service/tests/test_poison_handling.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""
Poison-message accounting: the offset is only committed once retries are
exhausted, and the exhausted message must reach the DLQ before that
commit rather than vanishing.
"""

import os
import sys
from unittest.mock import MagicMock

import pytest

sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))


class StubService:
    """FIA's retry bookkeeping. The real service is not constructed here:
    its __init__ opens Postgres and Kafka connections."""

    def __init__(self, dlq, max_retries=3):
        self._dlq = dlq
        self._max_retries = max_retries
        self._retry_counts = {}
        self._failed = 0
        self._dropped_poison = 0
        self._dlq_published = 0


@pytest.fixture
def record_failure():
    # Unbound method off the class, so the test always exercises the
    # shipped implementation.
    from src.main import FIAService

    return FIAService._record_failure


EVENT = {"transaction_id": "txn-1", "decision": "DECLINE"}


class TestPoisonHandling:
    def test_keeps_the_offset_uncommitted_while_retries_remain(self, record_failure):
        dlq = MagicMock()
        svc = StubService(dlq)

        assert record_failure(svc, "txn-1", "generation", RuntimeError("x"), EVENT) is False
        assert record_failure(svc, "txn-1", "generation", RuntimeError("x"), EVENT) is False
        dlq.publish.assert_not_called()

    def test_publishes_to_the_dlq_and_commits_once_retries_are_exhausted(self, record_failure):
        dlq = MagicMock()
        dlq.publish.return_value = True
        svc = StubService(dlq)

        for _ in range(2):
            record_failure(svc, "txn-1", "generation", RuntimeError("x"), EVENT)
        committed = record_failure(svc, "txn-1", "generation", RuntimeError("x"), EVENT)

        assert committed is True
        dlq.publish.assert_called_once()
        assert dlq.publish.call_args[0][0] == EVENT
        assert svc._dlq_published == 1
        assert svc._dropped_poison == 1

    def test_still_commits_when_the_dlq_itself_is_unavailable(self, record_failure):
        dlq = MagicMock()
        dlq.publish.return_value = False
        svc = StubService(dlq)

        for _ in range(2):
            record_failure(svc, "txn-1", "generation", RuntimeError("x"), EVENT)
        assert record_failure(svc, "txn-1", "generation", RuntimeError("x"), EVENT) is True
        assert svc._dlq_published == 0

    def test_clears_the_retry_counter_so_the_id_is_not_leaked(self, record_failure):
        dlq = MagicMock()
        svc = StubService(dlq)
        for _ in range(3):
            record_failure(svc, "txn-1", "generation", RuntimeError("x"), EVENT)
        assert "txn-1" not in svc._retry_counts

    def test_tracks_transactions_independently(self, record_failure):
        dlq = MagicMock()
        svc = StubService(dlq)
        record_failure(svc, "txn-1", "generation", RuntimeError("x"), EVENT)
        record_failure(svc, "txn-2", "generation", RuntimeError("x"), EVENT)
        assert svc._retry_counts == {"txn-1": 1, "txn-2": 1}