feat: global notification policy severity to channels (notif-22)
Singleton notification_policy table, NOTIFY_MIN_SEVERITY/CHANNELS env, central dispatch gate, Settings policy UI, migration 007. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
"""Global notification policy (notif-22)."""
|
||||
|
||||
from app.config import get_settings
|
||||
from app.models.notification_policy import NotificationPolicy
|
||||
from app.services.notification_policy import get_effective_notification_policy, upsert_notification_policy
|
||||
from app.services.notification_severity import severity_meets_minimum
|
||||
|
||||
|
||||
def test_severity_meets_minimum():
|
||||
assert severity_meets_minimum("warning", "warning")
|
||||
assert severity_meets_minimum("high", "warning")
|
||||
assert not severity_meets_minimum("info", "warning")
|
||||
|
||||
|
||||
def test_put_policy_persists_db(jwt_headers, client, db_session, monkeypatch):
|
||||
monkeypatch.setenv("NOTIFY_MIN_SEVERITY", "high")
|
||||
monkeypatch.setenv("NOTIFY_CHANNELS", "telegram")
|
||||
get_settings.cache_clear()
|
||||
|
||||
response = client.put(
|
||||
"/api/v1/settings/notifications/policy",
|
||||
headers=jwt_headers,
|
||||
json={
|
||||
"min_severity": "warning",
|
||||
"use_telegram": True,
|
||||
"use_webhook": True,
|
||||
"use_email": False,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["min_severity"] == "warning"
|
||||
assert body["use_webhook"] is True
|
||||
assert body["source"] == "db"
|
||||
|
||||
row = db_session.get(NotificationPolicy, 1)
|
||||
assert row is not None
|
||||
assert row.use_webhook is True
|
||||
|
||||
cfg = get_effective_notification_policy(db_session)
|
||||
assert cfg.min_severity == "warning"
|
||||
|
||||
|
||||
def test_upsert_policy_syncs_channel_min_severity(db_session, monkeypatch):
|
||||
monkeypatch.setenv("NOTIFY_MIN_SEVERITY", "high")
|
||||
get_settings.cache_clear()
|
||||
|
||||
from app.models.notification_channel import NotificationChannel
|
||||
|
||||
db_session.add(
|
||||
NotificationChannel(channel="telegram", enabled=False, min_severity="high")
|
||||
)
|
||||
db_session.commit()
|
||||
|
||||
upsert_notification_policy(
|
||||
db_session,
|
||||
min_severity="critical",
|
||||
use_telegram=True,
|
||||
use_webhook=False,
|
||||
use_email=False,
|
||||
)
|
||||
row = db_session.get(NotificationChannel, "telegram")
|
||||
assert row.min_severity == "critical"
|
||||
@@ -0,0 +1,62 @@
|
||||
"""notify_dispatch respects global policy."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import patch
|
||||
|
||||
from app.models import Event
|
||||
from app.services import notify_dispatch
|
||||
from app.services.notification_policy import NotificationPolicyConfig
|
||||
|
||||
|
||||
def test_notify_event_skipped_below_policy_severity():
|
||||
event = Event(
|
||||
event_id="00000000-0000-4000-8000-000000000401",
|
||||
host_id=1,
|
||||
occurred_at=datetime(2026, 5, 29, 15, 0, tzinfo=timezone.utc),
|
||||
category="auth",
|
||||
type="rdp.login.success",
|
||||
severity="info",
|
||||
title="ok",
|
||||
summary="skip",
|
||||
payload={},
|
||||
)
|
||||
policy = NotificationPolicyConfig(
|
||||
min_severity="warning",
|
||||
use_telegram=True,
|
||||
use_webhook=False,
|
||||
use_email=False,
|
||||
source="db",
|
||||
)
|
||||
with patch.object(notify_dispatch, "get_effective_notification_policy", return_value=policy):
|
||||
with patch.object(notify_dispatch, "telegram_notify") as mock_tg:
|
||||
notify_dispatch.notify_event(event)
|
||||
mock_tg.notify_event.assert_not_called()
|
||||
|
||||
|
||||
def test_notify_event_calls_selected_channels():
|
||||
event = Event(
|
||||
event_id="00000000-0000-4000-8000-000000000402",
|
||||
host_id=1,
|
||||
occurred_at=datetime(2026, 5, 29, 15, 0, tzinfo=timezone.utc),
|
||||
category="auth",
|
||||
type="rdp.login.failed",
|
||||
severity="warning",
|
||||
title="fail",
|
||||
summary="send",
|
||||
payload={},
|
||||
)
|
||||
policy = NotificationPolicyConfig(
|
||||
min_severity="warning",
|
||||
use_telegram=True,
|
||||
use_webhook=True,
|
||||
use_email=False,
|
||||
source="db",
|
||||
)
|
||||
with patch.object(notify_dispatch, "get_effective_notification_policy", return_value=policy):
|
||||
with patch.object(notify_dispatch, "telegram_notify") as mock_tg:
|
||||
with patch.object(notify_dispatch, "webhook_notify") as mock_wh:
|
||||
with patch.object(notify_dispatch, "email_notify") as mock_em:
|
||||
notify_dispatch.notify_event(event)
|
||||
mock_tg.notify_event.assert_called_once()
|
||||
mock_wh.notify_event.assert_called_once()
|
||||
mock_em.notify_event.assert_not_called()
|
||||
@@ -22,6 +22,8 @@ def test_get_notification_settings_masks_secrets(jwt_headers, client, monkeypatc
|
||||
assert body["telegram"]["min_severity"] == "warning"
|
||||
assert body["telegram"]["source"] == "env"
|
||||
assert body["telegram"]["bot_token_hint"].endswith("ghij")
|
||||
assert "policy" in body
|
||||
assert body["policy"]["min_severity"] == "warning"
|
||||
assert "webhook" in body
|
||||
assert body["webhook"]["enabled"] is False
|
||||
assert "email" in body
|
||||
@@ -38,7 +40,6 @@ def test_put_email_settings_persists_db(jwt_headers, client, db_session, monkeyp
|
||||
headers=jwt_headers,
|
||||
json={
|
||||
"enabled": True,
|
||||
"min_severity": "warning",
|
||||
"smtp_host": "smtp.example.com",
|
||||
"smtp_port": 587,
|
||||
"smtp_user": "monitor",
|
||||
@@ -95,7 +96,6 @@ def test_put_webhook_settings_persists_db(jwt_headers, client, db_session, monke
|
||||
headers=jwt_headers,
|
||||
json={
|
||||
"enabled": True,
|
||||
"min_severity": "warning",
|
||||
"url": "https://example.com/hooks/sac",
|
||||
"secret_header": "X-SAC-Token",
|
||||
"secret": "supersecret",
|
||||
@@ -145,7 +145,6 @@ def test_put_telegram_settings_persists_db(jwt_headers, client, db_session, monk
|
||||
headers=jwt_headers,
|
||||
json={
|
||||
"enabled": True,
|
||||
"min_severity": "warning",
|
||||
"bot_token": "1234567890:TESTTOKEN",
|
||||
"chat_id": "999001",
|
||||
},
|
||||
@@ -154,7 +153,6 @@ def test_put_telegram_settings_persists_db(jwt_headers, client, db_session, monk
|
||||
body = response.json()
|
||||
assert body["source"] == "db"
|
||||
assert body["enabled"] is True
|
||||
assert body["min_severity"] == "warning"
|
||||
|
||||
row = db_session.get(NotificationChannel, "telegram")
|
||||
assert row is not None
|
||||
@@ -163,7 +161,6 @@ def test_put_telegram_settings_persists_db(jwt_headers, client, db_session, monk
|
||||
|
||||
cfg = get_effective_telegram_config(db_session)
|
||||
assert cfg.source == "db"
|
||||
assert cfg.min_severity == "warning"
|
||||
|
||||
|
||||
def test_post_telegram_test_success(jwt_headers, client, db_session, monkeypatch):
|
||||
|
||||
Reference in New Issue
Block a user