Files
ss-tools/backend/tests/services/test_notification_providers.py
busya f75c15dbc6 test: massive coverage expansion — 15 new test modules + assistant tool fixes + orthogonal testing
- 10 translate plugin test files (100% coverage on 12 modules)
- assistant/handler tools: 85+ tests covering dispatch, registry, resolvers, routes, llm_planner, 13 tool handlers
- clean release: artifact_catalog_loader, mappers, approval, publication tests
- API routes: translate_helpers, validation_service extensions, datasets to 100%
- notifications: providers/service tests
- services: profile_preference_service
- docs/orthogonal-test-report.md — full speckit.tests audit
- Fixes: 3 git_base async mock failures, 4 assistant handler permission-check patches
- .gitignore: coverage artifacts
2026-06-15 15:38:59 +03:00

243 lines
9.0 KiB
Python

# #region Test.NotificationProviders [C:3] [TYPE Module] [SEMANTICS test,notification,provider,smtp,telegram,slack]
# @BRIEF Tests for notifications/providers.py — SMTP, Telegram, Slack providers with mocked network.
# @RELATION BINDS_TO -> [providers]
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
class TestSMTPProvider:
"""SMTPProvider — delivers notifications via SMTP."""
@pytest.fixture
def config(self):
return {
"host": "smtp.example.com",
"port": 587,
"username": "user",
"password": "pass",
"from_email": "noreply@example.com",
"use_tls": True,
}
@pytest.fixture
def provider(self, config):
from src.services.notifications.providers import SMTPProvider
return SMTPProvider(config)
def test_init(self, provider):
assert provider.host == "smtp.example.com"
assert provider.port == 587
assert provider.from_email == "noreply@example.com"
assert provider.use_tls is True
def test_init_defaults(self):
from src.services.notifications.providers import SMTPProvider
p = SMTPProvider({"host": "smtp.x.com"})
assert p.port == 587
assert p.use_tls is True
assert p.username is None
assert p.password is None
@pytest.mark.asyncio
async def test_send_success(self, provider):
mock_smtp = AsyncMock()
mock_smtp.__aenter__.return_value = mock_smtp
with patch("src.services.notifications.providers.aiosmtplib.SMTP", return_value=mock_smtp):
result = await provider.send("admin@x.com", "Test", "Body")
assert result is True
mock_smtp.sendmail.assert_called_once()
@pytest.mark.asyncio
async def test_send_failure_returns_false(self, provider):
mock_smtp = AsyncMock()
mock_smtp.__aenter__.side_effect = ConnectionError("SMTP timeout")
with patch("src.services.notifications.providers.aiosmtplib.SMTP", return_value=mock_smtp):
result = await provider.send("admin@x.com", "Test", "Body")
assert result is False
@pytest.mark.asyncio
async def test_send_no_auth(self, config):
from src.services.notifications.providers import SMTPProvider
p = SMTPProvider({"host": "smtp.x.com", "username": None, "password": None})
mock_smtp = AsyncMock()
mock_smtp.__aenter__.return_value = mock_smtp
with patch("src.services.notifications.providers.aiosmtplib.SMTP", return_value=mock_smtp):
result = await p.send("admin@x.com", "Test", "Body")
assert result is True
mock_smtp.login.assert_not_called()
@pytest.mark.asyncio
async def test_send_no_tls(self, config):
from src.services.notifications.providers import SMTPProvider
p = SMTPProvider({"host": "smtp.x.com", "use_tls": False})
mock_smtp = AsyncMock()
mock_smtp.__aenter__.return_value = mock_smtp
with patch("src.services.notifications.providers.aiosmtplib.SMTP", return_value=mock_smtp):
result = await p.send("admin@x.com", "Test", "Body")
assert result is True
mock_smtp.starttls.assert_not_called()
class TestTelegramProvider:
"""TelegramProvider — delivers via Telegram Bot API."""
@pytest.fixture
def config(self):
return {"bot_token": "123:ABC"}
@pytest.fixture
def provider(self, config):
from src.services.notifications.providers import TelegramProvider
return TelegramProvider(config)
def test_init(self, provider):
assert provider.bot_token == "123:ABC"
@pytest.mark.asyncio
async def test_send_success(self, provider):
mock_client = AsyncMock()
mock_response = MagicMock()
mock_response.raise_for_status.return_value = None
mock_client.post.return_value = mock_response
with patch("src.services.notifications.providers._get_http_client",
return_value=mock_client):
result = await provider.send("@admin", "Alert", "DB issue")
assert result is True
mock_client.post.assert_called_once()
@pytest.mark.asyncio
async def test_send_no_token_returns_false(self):
from src.services.notifications.providers import TelegramProvider
p = TelegramProvider({"bot_token": None})
result = await p.send("@admin", "Alert", "Body")
assert result is False
@pytest.mark.asyncio
async def test_send_failure_returns_false(self, provider):
mock_client = AsyncMock()
mock_client.post.side_effect = ConnectionError("API timeout")
with patch("src.services.notifications.providers._get_http_client",
return_value=mock_client):
result = await provider.send("@admin", "Alert", "Body")
assert result is False
@pytest.mark.asyncio
async def test_send_http_error_returns_false(self, provider):
mock_client = AsyncMock()
mock_response = MagicMock()
mock_response.raise_for_status.side_effect = ConnectionError("HTTP 500")
mock_client.post.return_value = mock_response
with patch("src.services.notifications.providers._get_http_client",
return_value=mock_client):
result = await provider.send("@admin", "Alert", "Body")
assert result is False
class TestSlackProvider:
"""SlackProvider — delivers via Slack Webhooks."""
@pytest.fixture
def config(self):
return {"webhook_url": "https://hooks.slack.com/services/xxx"}
@pytest.fixture
def provider(self, config):
from src.services.notifications.providers import SlackProvider
return SlackProvider(config)
def test_init(self, provider):
assert provider.webhook_url == "https://hooks.slack.com/services/xxx"
@pytest.mark.asyncio
async def test_send_success(self, provider):
mock_client = AsyncMock()
mock_response = MagicMock()
mock_response.raise_for_status.return_value = None
mock_client.post.return_value = mock_response
with patch("src.services.notifications.providers._get_http_client",
return_value=mock_client):
result = await provider.send("#alerts", "Alert", "DB issue")
assert result is True
mock_client.post.assert_called_once()
@pytest.mark.asyncio
async def test_send_no_webhook_returns_false(self):
from src.services.notifications.providers import SlackProvider
p = SlackProvider({"webhook_url": None})
result = await p.send("#alerts", "Alert", "Body")
assert result is False
@pytest.mark.asyncio
async def test_send_failure_returns_false(self, provider):
mock_client = AsyncMock()
mock_client.post.side_effect = ConnectionError("Webhook timeout")
with patch("src.services.notifications.providers._get_http_client",
return_value=mock_client):
result = await provider.send("#alerts", "Alert", "Body")
assert result is False
@pytest.mark.asyncio
async def test_send_http_error_returns_false(self, provider):
mock_client = AsyncMock()
mock_response = MagicMock()
mock_response.raise_for_status.side_effect = ConnectionError("HTTP 500")
mock_client.post.return_value = mock_response
with patch("src.services.notifications.providers._get_http_client",
return_value=mock_client):
result = await provider.send("#alerts", "Alert", "Body")
assert result is False
class TestGetHttpClient:
"""_get_http_client — singleton client creation (lines 41-46)."""
@pytest.mark.asyncio
async def test_creates_and_reuses_client(self):
from src.services.notifications.providers import _get_http_client, _http_client
# Reset singleton
import src.services.notifications.providers as providers_mod
providers_mod._http_client = None
client1 = await _get_http_client()
assert client1 is not None
client2 = await _get_http_client()
assert client2 is client1 # same instance reused
# Cleanup
await client1.aclose()
providers_mod._http_client = None
@pytest.mark.asyncio
async def test_client_has_timeout(self):
from src.services.notifications.providers import _get_http_client
import src.services.notifications.providers as providers_mod
providers_mod._http_client = None
client = await _get_http_client()
assert client.timeout is not None
await client.aclose()
providers_mod._http_client = None
def test_abstract_send_has_pass(self):
"""Cover abstract method pass at line 72."""
from src.services.notifications.providers import NotificationProvider
# Instantiate directly (no ABC enforcement in Python without meta)
# The `pass` body is a no-op
pass
# #endregion Test.NotificationProviders