diff --git a/shondesh/channels/webhook.py b/shondesh/channels/webhook.py index 6284980..71923bc 100644 --- a/shondesh/channels/webhook.py +++ b/shondesh/channels/webhook.py @@ -39,12 +39,13 @@ async def send(self, data: dict) -> bool: async with aiohttp.ClientSession() as session: for attempt in range(self.config.get("retry_count", 1)): try: - async with session.request( + response = await session.request( self.config.get("method", "POST"), self.config["url"], json=payload, headers=headers, - ) as response: + ) + async with response: if response.status < 400: return True except Exception as e: diff --git a/shondesh/formatters/slack_message_formatter.py b/shondesh/formatters/slack_message_formatter.py index 08c4dea..0c04528 100644 --- a/shondesh/formatters/slack_message_formatter.py +++ b/shondesh/formatters/slack_message_formatter.py @@ -17,3 +17,5 @@ def format(self, message: dict) -> list: value.replace("`", "\\`").replace("*", "\\*").replace("_", "\\_") ) attachment_fields.append({"title": key, "value": str(value), "short": True}) + + return attachment_fields diff --git a/tests/shondesh/channels/test_email.py b/tests/shondesh/channels/test_email.py new file mode 100644 index 0000000..f3077da --- /dev/null +++ b/tests/shondesh/channels/test_email.py @@ -0,0 +1,64 @@ +import pytest +from shondesh.channels.email import Email + + +@pytest.fixture +def email_config(): + return { + "from_address": "sender@example.com", + "recipients": ["recipient@example.com"], + "subject_template": "Test Subject", + "password": "testpassword", + "smtp_server": "smtp.example.com", + "smtp_port": 587, + "username": "user@example.com", + } + + +@pytest.fixture +def email_channel(email_config): + return Email(config=email_config) + + +def test_email_properties(email_channel, email_config): + assert email_channel.config == email_config + + +@pytest.mark.asyncio +async def test_send_success(mocker, email_channel): + mock_smtp = mocker.patch("smtplib.SMTP", autospec=True) + data = {"key": "value"} + result = await email_channel.send(data) + assert result + mock_smtp.assert_called_with( + email_channel.config["smtp_server"], email_channel.config["smtp_port"] + ) + instance = mock_smtp.return_value + instance.starttls.assert_called_once() + instance.login.assert_called_once_with( + email_channel.config["username"], email_channel.config["password"] + ) + instance.sendmail.assert_called_once() + instance.quit.assert_called_once() + + +@pytest.mark.asyncio +async def test_send_env_password(mocker, email_channel, email_config, monkeypatch): + email_channel.config["password"] = "${EMAIL_PASSWORD}" + monkeypatch.setenv("EMAIL_PASSWORD", "envpassword") + mock_smtp = mocker.patch("smtplib.SMTP", autospec=True) + data = {"key": "value"} + result = await email_channel.send(data) + assert result + instance = mock_smtp.return_value + instance.login.assert_called_once_with( + email_channel.config["username"], "envpassword" + ) + + +@pytest.mark.asyncio +async def test_send_failure(mocker, email_channel): + mocker.patch("smtplib.SMTP", side_effect=Exception("SMTP error")) + data = {"key": "value"} + result = await email_channel.send(data) + assert result is False diff --git a/tests/shondesh/channels/test_slack.py b/tests/shondesh/channels/test_slack.py new file mode 100644 index 0000000..d6606c3 --- /dev/null +++ b/tests/shondesh/channels/test_slack.py @@ -0,0 +1,53 @@ +import pytest +from shondesh.channels.slack import Slack +from shondesh.utils.constants import Severity + + +@pytest.fixture +def slack_config(): + return { + "channel": "#alerts", + "username": "AlertBot", + "icon_emoji": ":robot_face:", + "webhook_url": "https://hooks.slack.com/services/test", + "mention_users": ["@user1", "@user2"], + } + + +@pytest.fixture +def slack_channel(slack_config): + return Slack(config=slack_config) + + +@pytest.mark.asyncio +async def test_send_success(mocker, slack_channel): + mocker.patch.object( + slack_channel, "send", mocker.AsyncMock(return_value={"ok": True}) + ) + + data = {"severity": Severity.INFO, "message": "Test"} + result = await slack_channel.send(data) + assert result.get("ok") is True + slack_channel.send.assert_awaited_once_with(data) + + +@pytest.mark.asyncio +async def test_send_failure_status(mocker, slack_channel): + mocker.patch.object( + slack_channel, "send", mocker.AsyncMock(return_value={"ok": False}) + ) + + data = {"severity": Severity.INFO, "message": "Test"} + result = await slack_channel.send(data) + assert result.get("ok") is False + + +@pytest.mark.asyncio +async def test_send_exception(mocker, slack_channel): + mock_session = mocker.MagicMock() + mock_session.__aenter__.side_effect = Exception("Connection error") + mocker.patch("aiohttp.ClientSession", return_value=mock_session) + + data = {"severity": Severity.INFO, "message": "Test"} + result = await slack_channel.send(data) + assert result is False diff --git a/tests/shondesh/channels/test_telegram.py b/tests/shondesh/channels/test_telegram.py index 0655727..afddff1 100644 --- a/tests/shondesh/channels/test_telegram.py +++ b/tests/shondesh/channels/test_telegram.py @@ -13,6 +13,34 @@ def telegram_channel(): return Telegram(config) +def test_telegram_initialization(telegram_channel): + assert telegram_channel is not None + assert isinstance(telegram_channel, Telegram) + assert telegram_channel.config is not None + assert "token" in telegram_channel.config + assert "chat_id" in telegram_channel.config + assert "webhook_url" in telegram_channel.config + + +def test_telegram_initialization_missing_config(): + with pytest.raises(ValueError): + Telegram(config={}) # Should raise an error due to missing token and chat_id + + +def test_raises_value_error_when_chat_id_is_missing(): + config = {"webhook_url": "https://example.com/telegram"} + with pytest.raises( + ValueError, match="Telegram chat ID is required in the configuration." + ): + Telegram(config=config) + + +def test_does_not_raise_error_when_chat_id_is_present(): + config = {"webhook_url": "https://example.com/telegram", "chat_id": "12345"} + channel = Telegram(config=config) + assert channel.config["chat_id"] == "12345" + + def test_telegram_properties(telegram_channel): assert telegram_channel is not None assert telegram_channel.config is not None @@ -27,3 +55,22 @@ async def test_telegram_send_message(mocker, telegram_channel): response = await telegram_channel.send(message) assert response is not None assert response.get("ok") is True + + +@pytest.mark.asyncio +async def test_telegram_send_message_failure(mocker, telegram_channel): + message = {"msg": "Test message", "severity": "info"} + mocker.patch.object(telegram_channel, "send", return_value={"ok": False}) + response = await telegram_channel.send(message) + assert response is not None + assert response.get("ok") is False + + +@pytest.mark.asyncio +async def test_telegram_send_message_exception(mocker, telegram_channel): + message = {"msg": "Test message", "severity": "info"} + mocker.patch.object( + telegram_channel, "send", side_effect=Exception("Network error") + ) + with pytest.raises(Exception): + await telegram_channel.send(message) diff --git a/tests/shondesh/channels/test_webhook.py b/tests/shondesh/channels/test_webhook.py new file mode 100644 index 0000000..fd65d7b --- /dev/null +++ b/tests/shondesh/channels/test_webhook.py @@ -0,0 +1,80 @@ +import pytest +from shondesh.channels.webhook import Webhook + + +@pytest.fixture +def webhook_config(): + return { + "url": "https://example.com/webhook", + "method": "POST", + "headers": {"Authorization": "Bearer testtoken"}, + "retry_count": 1, + } + + +@pytest.fixture +def webhook_channel(webhook_config): + return Webhook(config=webhook_config) + + +@pytest.mark.asyncio +async def test_send_success(mocker, webhook_channel): + mock_response = mocker.MagicMock() + mock_response.status = 200 + mock_request = mocker.AsyncMock(return_value=mock_response) + mock_session = mocker.MagicMock() + mock_session.__aenter__.return_value = mock_session + mock_session.request = mock_request + mocker.patch("aiohttp.ClientSession", return_value=mock_session) + + data = {"message": "Test"} + result = await webhook_channel.send(data) + assert result is True + mock_request.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_send_failure_status(mocker, webhook_channel): + mock_response = mocker.MagicMock() + mock_response.status = 500 + mock_request = mocker.AsyncMock(return_value=mock_response) + mock_session = mocker.MagicMock() + mock_session.__aenter__.return_value = mock_session + mock_session.request = mock_request + mocker.patch("aiohttp.ClientSession", return_value=mock_session) + + data = {"message": "Test"} + result = await webhook_channel.send(data) + assert result is False + + +@pytest.mark.asyncio +async def test_send_exception(mocker, webhook_channel): + mock_session = mocker.MagicMock() + mock_session.__aenter__.side_effect = Exception("Connection error") + mocker.patch("aiohttp.ClientSession", return_value=mock_session) + + data = {"message": "Test"} + result = await webhook_channel.send(data) + assert result is False + + +@pytest.mark.asyncio +async def test_send_env_header(monkeypatch, webhook_config, mocker): + webhook_config["headers"] = {"Authorization": "${WEBHOOK_TOKEN}"} + monkeypatch.setenv("WEBHOOK_TOKEN", "envtoken") + channel = Webhook(config=webhook_config) + + mock_response = mocker.MagicMock() + mock_response.status = 200 + mock_request = mocker.AsyncMock(return_value=mock_response) + mock_session = mocker.MagicMock() + mock_session.__aenter__.return_value = mock_session + mock_session.request = mock_request + mocker.patch("aiohttp.ClientSession", return_value=mock_session) + + data = {"message": "Test"} + result = await channel.send(data) + assert result is True + _, kwargs = mock_request.call_args + assert kwargs["headers"]["Authorization"] == "envtoken" diff --git a/tests/shondesh/formatters/__init__.py b/tests/shondesh/formatters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/shondesh/formatters/test_dict_table_formatter.py b/tests/shondesh/formatters/test_dict_table_formatter.py new file mode 100644 index 0000000..966388e --- /dev/null +++ b/tests/shondesh/formatters/test_dict_table_formatter.py @@ -0,0 +1,17 @@ +from shondesh.formatters.dict_table_formatter import DictTableFormatter + + +def test_dict_table_formatter(): + formatter = DictTableFormatter() + data = {"Name": "Alice", "Age": 30, "Country": "Wonderland"} + expected = ( + "Key | Value \n" + "--------+-----------\n" + "Name | Alice \n" + "Age | 30 \n" + "Country | Wonderland" + ) + result = formatter.format(data) + assert [line.rstrip() for line in result.splitlines()] == [ + line.rstrip() for line in expected.splitlines() + ] diff --git a/tests/shondesh/formatters/test_slack_message_formatter.py b/tests/shondesh/formatters/test_slack_message_formatter.py new file mode 100644 index 0000000..086ec76 --- /dev/null +++ b/tests/shondesh/formatters/test_slack_message_formatter.py @@ -0,0 +1,37 @@ +import pytest +from shondesh.formatters.slack_message_formatter import ( + SlackMessageFormatter as SlackFormatter, +) + + +@pytest.fixture +def slack_data(): + return { + "message": "Test message", + "severity": "info", + "extra": {"foo": "bar"}, + "timestamp": "2023-10-01T12:00:00Z", + } + + +def test_slack_formatter_basic(slack_data): + formatter = SlackFormatter() + formatted = formatter.format(slack_data) + assert isinstance(formatted, list) + assert formatted + assert any(field["title"] == "message" for key, field in enumerate(formatted)) + + +def test_slack_formatter_severity(slack_data): + formatter = SlackFormatter() + slack_data["severity"] = "critical" + formatted = formatter.format(slack_data) + assert "danger" in str(formatted).lower() or "critical" in str(formatted).lower() + + +def test_slack_formatter_extra_fields(slack_data): + formatter = SlackFormatter() + formatted = formatter.format(slack_data) + assert formatted + assert any(field["title"] == "extra" for key, field in enumerate(formatted)) + assert any(field["value"] == "{'foo': 'bar'}" for field in formatted) diff --git a/tests/shondesh/utils/__init__.py b/tests/shondesh/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/shondesh/utils/test_constants.py b/tests/shondesh/utils/test_constants.py new file mode 100644 index 0000000..e422777 --- /dev/null +++ b/tests/shondesh/utils/test_constants.py @@ -0,0 +1,25 @@ +import pytest +from shondesh.utils.constants import Severity + + +def returns_correct_value_for_info(): + assert Severity.INFO.value == "info" + + +def returns_correct_value_for_warning(): + assert Severity.WARNING.value == "warning" + + +def returns_correct_value_for_critical(): + assert Severity.CRITICAL.value == "critical" + + +def has_all_expected_severity_levels(): + expected_levels = {"info", "warning", "critical"} + actual_levels = {severity.value for severity in Severity} + assert actual_levels == expected_levels + + +def raises_attribute_error_for_invalid_severity(): + with pytest.raises(AttributeError): + _ = Severity.INVALID