Initial commit
Some checks failed
CI/CD Pipeline / Code Quality & Linting (push) Has been cancelled
CI/CD Pipeline / Policy Validation (push) Has been cancelled
CI/CD Pipeline / Test Suite (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-coverage) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-extract) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-firm-connectors) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-forms) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-hmrc) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-ingestion) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-kg) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-normalize-map) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-ocr) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rag-indexer) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rag-retriever) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-reason) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rpa) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (ui-review) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-coverage) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-extract) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-kg) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-rag-retriever) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (ui-review) (push) Has been cancelled
CI/CD Pipeline / Generate SBOM (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Notifications (push) Has been cancelled
Some checks failed
CI/CD Pipeline / Code Quality & Linting (push) Has been cancelled
CI/CD Pipeline / Policy Validation (push) Has been cancelled
CI/CD Pipeline / Test Suite (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-coverage) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-extract) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-firm-connectors) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-forms) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-hmrc) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-ingestion) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-kg) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-normalize-map) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-ocr) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rag-indexer) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rag-retriever) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-reason) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rpa) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (ui-review) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-coverage) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-extract) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-kg) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-rag-retriever) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (ui-review) (push) Has been cancelled
CI/CD Pipeline / Generate SBOM (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Notifications (push) Has been cancelled
This commit is contained in:
271
tests/unit/test_nats_bus.py
Normal file
271
tests/unit/test_nats_bus.py
Normal file
@@ -0,0 +1,271 @@
|
||||
"""Tests for NATS event bus implementation."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from libs.events.base import EventPayload
|
||||
from libs.events.nats_bus import NATSEventBus
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def event_payload():
|
||||
"""Create a test event payload."""
|
||||
return EventPayload(
|
||||
data={"test": "data", "value": 123},
|
||||
actor="test-user",
|
||||
tenant_id="test-tenant",
|
||||
trace_id="test-trace-123",
|
||||
schema_version="1.0",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def nats_bus():
|
||||
"""Create a NATS event bus instance."""
|
||||
return NATSEventBus(
|
||||
servers="nats://localhost:4222",
|
||||
stream_name="TEST_STREAM",
|
||||
consumer_group="test-group",
|
||||
)
|
||||
|
||||
|
||||
class TestNATSEventBus:
|
||||
"""Test cases for NATS event bus."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_initialization(self, nats_bus):
|
||||
"""Test NATS event bus initialization."""
|
||||
assert nats_bus.servers == ["nats://localhost:4222"]
|
||||
assert nats_bus.stream_name == "TEST_STREAM"
|
||||
assert nats_bus.consumer_group == "test-group"
|
||||
assert not nats_bus.running
|
||||
assert nats_bus.nc is None
|
||||
assert nats_bus.js is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_initialization_with_multiple_servers(self):
|
||||
"""Test NATS event bus initialization with multiple servers."""
|
||||
servers = ["nats://server1:4222", "nats://server2:4222"]
|
||||
bus = NATSEventBus(servers=servers)
|
||||
assert bus.servers == servers
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("libs.events.nats_bus.nats.connect")
|
||||
async def test_start(self, mock_connect, nats_bus):
|
||||
"""Test starting the NATS event bus."""
|
||||
# Mock NATS connection and JetStream
|
||||
mock_nc = AsyncMock()
|
||||
mock_js = AsyncMock()
|
||||
mock_nc.jetstream.return_value = mock_js
|
||||
mock_connect.return_value = mock_nc
|
||||
|
||||
# Mock stream info to simulate existing stream
|
||||
mock_js.stream_info.return_value = {"name": "TEST_STREAM"}
|
||||
|
||||
await nats_bus.start()
|
||||
|
||||
assert nats_bus.running
|
||||
assert nats_bus.nc == mock_nc
|
||||
assert nats_bus.js == mock_js
|
||||
mock_connect.assert_called_once_with(servers=["nats://localhost:4222"])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("libs.events.nats_bus.nats.connect")
|
||||
async def test_start_creates_stream_if_not_exists(self, mock_connect, nats_bus):
|
||||
"""Test that start creates stream if it doesn't exist."""
|
||||
# Mock NATS connection and JetStream
|
||||
mock_nc = AsyncMock()
|
||||
mock_js = AsyncMock()
|
||||
mock_nc.jetstream.return_value = mock_js
|
||||
mock_connect.return_value = mock_nc
|
||||
|
||||
# Mock stream_info to raise NotFoundError, then add_stream
|
||||
from nats.js.errors import NotFoundError
|
||||
mock_js.stream_info.side_effect = NotFoundError
|
||||
mock_js.add_stream = AsyncMock()
|
||||
|
||||
await nats_bus.start()
|
||||
|
||||
mock_js.add_stream.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_already_running(self, nats_bus):
|
||||
"""Test that start does nothing if already running."""
|
||||
nats_bus.running = True
|
||||
original_nc = nats_bus.nc
|
||||
|
||||
await nats_bus.start()
|
||||
|
||||
assert nats_bus.nc == original_nc
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop(self, nats_bus):
|
||||
"""Test stopping the NATS event bus."""
|
||||
# Setup mock objects
|
||||
mock_nc = AsyncMock()
|
||||
mock_subscription = AsyncMock()
|
||||
mock_task = AsyncMock()
|
||||
|
||||
nats_bus.running = True
|
||||
nats_bus.nc = mock_nc
|
||||
nats_bus.subscriptions = {"test-topic": mock_subscription}
|
||||
nats_bus.consumer_tasks = [mock_task]
|
||||
|
||||
await nats_bus.stop()
|
||||
|
||||
assert not nats_bus.running
|
||||
mock_task.cancel.assert_called_once()
|
||||
mock_subscription.unsubscribe.assert_called_once()
|
||||
mock_nc.close.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_not_running(self, nats_bus):
|
||||
"""Test that stop does nothing if not running."""
|
||||
assert not nats_bus.running
|
||||
await nats_bus.stop()
|
||||
assert not nats_bus.running
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_publish(self, nats_bus, event_payload):
|
||||
"""Test publishing an event."""
|
||||
# Setup mock JetStream
|
||||
mock_js = AsyncMock()
|
||||
mock_ack = MagicMock()
|
||||
mock_ack.seq = 123
|
||||
mock_js.publish.return_value = mock_ack
|
||||
nats_bus.js = mock_js
|
||||
|
||||
result = await nats_bus.publish("test-topic", event_payload)
|
||||
|
||||
assert result is True
|
||||
mock_js.publish.assert_called_once()
|
||||
call_args = mock_js.publish.call_args
|
||||
assert call_args[1]["subject"] == "TEST_STREAM.test-topic"
|
||||
assert call_args[1]["payload"] == event_payload.to_json().encode()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_publish_not_started(self, nats_bus, event_payload):
|
||||
"""Test publishing when event bus is not started."""
|
||||
with pytest.raises(RuntimeError, match="Event bus not started"):
|
||||
await nats_bus.publish("test-topic", event_payload)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_publish_failure(self, nats_bus, event_payload):
|
||||
"""Test publishing failure."""
|
||||
# Setup mock JetStream that raises exception
|
||||
mock_js = AsyncMock()
|
||||
mock_js.publish.side_effect = Exception("Publish failed")
|
||||
nats_bus.js = mock_js
|
||||
|
||||
result = await nats_bus.publish("test-topic", event_payload)
|
||||
|
||||
assert result is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_subscribe(self, nats_bus):
|
||||
"""Test subscribing to a topic."""
|
||||
# Setup mock JetStream
|
||||
mock_js = AsyncMock()
|
||||
mock_subscription = AsyncMock()
|
||||
mock_js.pull_subscribe.return_value = mock_subscription
|
||||
nats_bus.js = mock_js
|
||||
|
||||
# Mock handler
|
||||
async def test_handler(topic: str, payload: EventPayload) -> None:
|
||||
pass
|
||||
|
||||
with patch("asyncio.create_task") as mock_create_task:
|
||||
await nats_bus.subscribe("test-topic", test_handler)
|
||||
|
||||
assert "test-topic" in nats_bus.handlers
|
||||
assert test_handler in nats_bus.handlers["test-topic"]
|
||||
assert "test-topic" in nats_bus.subscriptions
|
||||
mock_js.pull_subscribe.assert_called_once()
|
||||
mock_create_task.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_subscribe_not_started(self, nats_bus):
|
||||
"""Test subscribing when event bus is not started."""
|
||||
async def test_handler(topic: str, payload: EventPayload) -> None:
|
||||
pass
|
||||
|
||||
with pytest.raises(RuntimeError, match="Event bus not started"):
|
||||
await nats_bus.subscribe("test-topic", test_handler)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_subscribe_multiple_handlers(self, nats_bus):
|
||||
"""Test subscribing multiple handlers to the same topic."""
|
||||
# Setup mock JetStream
|
||||
mock_js = AsyncMock()
|
||||
mock_subscription = AsyncMock()
|
||||
mock_js.pull_subscribe.return_value = mock_subscription
|
||||
nats_bus.js = mock_js
|
||||
|
||||
# Mock handlers
|
||||
async def handler1(topic: str, payload: EventPayload) -> None:
|
||||
pass
|
||||
|
||||
async def handler2(topic: str, payload: EventPayload) -> None:
|
||||
pass
|
||||
|
||||
with patch("asyncio.create_task"):
|
||||
await nats_bus.subscribe("test-topic", handler1)
|
||||
await nats_bus.subscribe("test-topic", handler2)
|
||||
|
||||
assert len(nats_bus.handlers["test-topic"]) == 2
|
||||
assert handler1 in nats_bus.handlers["test-topic"]
|
||||
assert handler2 in nats_bus.handlers["test-topic"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_consume_messages(self, nats_bus, event_payload):
|
||||
"""Test consuming messages from NATS."""
|
||||
# Setup mock subscription and message
|
||||
mock_subscription = AsyncMock()
|
||||
mock_message = MagicMock()
|
||||
mock_message.data.decode.return_value = event_payload.to_json()
|
||||
mock_message.ack = AsyncMock()
|
||||
|
||||
mock_subscription.fetch.return_value = [mock_message]
|
||||
nats_bus.running = True
|
||||
|
||||
# Mock handler
|
||||
handler_called = False
|
||||
received_topic = None
|
||||
received_payload = None
|
||||
|
||||
async def test_handler(topic: str, payload: EventPayload) -> None:
|
||||
nonlocal handler_called, received_topic, received_payload
|
||||
handler_called = True
|
||||
received_topic = topic
|
||||
received_payload = payload
|
||||
|
||||
nats_bus.handlers["test-topic"] = [test_handler]
|
||||
|
||||
# Run one iteration of message consumption
|
||||
with patch.object(nats_bus, "running", side_effect=[True, False]):
|
||||
await nats_bus._consume_messages("test-topic", mock_subscription)
|
||||
|
||||
assert handler_called
|
||||
assert received_topic == "test-topic"
|
||||
assert received_payload.event_id == event_payload.event_id
|
||||
mock_message.ack.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_factory_integration(self):
|
||||
"""Test that the factory can create a NATS event bus."""
|
||||
from libs.events.factory import create_event_bus
|
||||
|
||||
bus = create_event_bus(
|
||||
"nats",
|
||||
servers="nats://localhost:4222",
|
||||
stream_name="TEST_STREAM",
|
||||
consumer_group="test-group",
|
||||
)
|
||||
|
||||
assert isinstance(bus, NATSEventBus)
|
||||
assert bus.servers == ["nats://localhost:4222"]
|
||||
assert bus.stream_name == "TEST_STREAM"
|
||||
assert bus.consumer_group == "test-group"
|
||||
Reference in New Issue
Block a user