feat: initial commit for merica-pipeline delivery

This commit is contained in:
MERICA Admin
2026-10-08 19:33:15 +07:00
commit 42fedab752
46 changed files with 5484 additions and 0 deletions
+248
View File
@@ -0,0 +1,248 @@
"""
Unit tests for MericaAlertHook: multi-channel alerting, governance enrichment,
and anti-alert fatigue throttling engine.
"""
import pytest
from unittest.mock import patch, MagicMock
from helpers.merica_alert_hook import MericaAlertHook, _ALERT_SUPPRESSION_CACHE
@pytest.fixture(autouse=True)
def clean_alert_cache():
"""Clear suppression cache before and after every test."""
MericaAlertHook.clear_suppression_cache()
yield
MericaAlertHook.clear_suppression_cache()
class TestMericaAlertHookChannels:
"""Test individual dispatch channels for Telegram, Slack, Discord, SITOMAT."""
@patch("urllib.request.urlopen")
def test_send_webhook_success(self, mock_urlopen):
mock_resp = MagicMock()
mock_resp.status = 200
mock_urlopen.return_value.__enter__.return_value = mock_resp
result = MericaAlertHook.send_webhook("http://example.com/webhook", {"test": "data"})
assert result is True
mock_urlopen.assert_called_once()
@patch("urllib.request.urlopen")
def test_send_webhook_failure(self, mock_urlopen):
mock_urlopen.side_effect = Exception("Connection refused")
result = MericaAlertHook.send_webhook("http://example.com/webhook", {"test": "data"})
assert result is False
@patch("urllib.request.urlopen")
def test_send_telegram_alert(self, mock_urlopen):
mock_resp = MagicMock()
mock_resp.status = 200
mock_urlopen.return_value.__enter__.return_value = mock_resp
result = MericaAlertHook.send_telegram_alert(
bot_token="bot123456:ABC-DEF",
chat_id="-100987654321",
message="<b>Test Alert</b>"
)
assert result is True
args, kwargs = mock_urlopen.call_args
req = args[0]
assert "api.telegram.org/botbot123456:ABC-DEF/sendMessage" in req.full_url
assert b"-100987654321" in req.data
@patch("urllib.request.urlopen")
def test_send_slack_alert(self, mock_urlopen):
mock_resp = MagicMock()
mock_resp.status = 200
mock_urlopen.return_value.__enter__.return_value = mock_resp
result = MericaAlertHook.send_slack_alert(
webhook_url="https://hooks.slack.com/services/T00/B00/X00",
payload={"text": "Test Slack"}
)
assert result is True
args, kwargs = mock_urlopen.call_args
req = args[0]
assert "hooks.slack.com" in req.full_url
@patch("urllib.request.urlopen")
def test_send_discord_alert(self, mock_urlopen):
mock_resp = MagicMock()
mock_resp.status = 204
mock_urlopen.return_value.__enter__.return_value = mock_resp
result = MericaAlertHook.send_discord_alert(
webhook_url="https://discord.com/api/webhooks/123/abc",
payload={"content": "Test Discord"}
)
assert result is True
@patch("urllib.request.urlopen")
def test_send_sitomat_alert(self, mock_urlopen):
mock_resp = MagicMock()
mock_resp.status = 200
mock_urlopen.return_value.__enter__.return_value = mock_resp
result = MericaAlertHook.send_sitomat_alert(
endpoint_url="http://sitomat:8080/api/v1/lakehouse/webhook",
incident_payload={"incidentType": "TASK_FAILURE"},
api_key="secret-api-key"
)
assert result is True
args, kwargs = mock_urlopen.call_args
req = args[0]
assert req.headers.get("X-api-key") == "secret-api-key" or req.headers.get("Authorization") == "Bearer secret-api-key"
class TestAntiAlertFatigue:
"""Test suppression and throttling of duplicate/cascading incident alerts."""
def test_anti_alert_fatigue_throttling(self):
cache_key = "dag_permits:task_silver:TASK_FAILURE"
# Panggilan 1: Belum pernah terlihat, harus lolos (suppressed = False)
is_suppressed, count = MericaAlertHook.check_and_record_suppression(cache_key, window_seconds=300)
assert is_suppressed is False
assert count == 1
# Panggilan 2: Terjadi dalam 300s window, harus dibatasi (suppressed = True)
is_suppressed, count = MericaAlertHook.check_and_record_suppression(cache_key, window_seconds=300)
assert is_suppressed is True
assert count == 2
# Panggilan 3: Masih dalam window
is_suppressed, count = MericaAlertHook.check_and_record_suppression(cache_key, window_seconds=300)
assert is_suppressed is True
assert count == 3
# Reset cache manual
MericaAlertHook.clear_suppression_cache()
is_suppressed, count = MericaAlertHook.check_and_record_suppression(cache_key, window_seconds=300)
assert is_suppressed is False
assert count == 1
class TestGovernanceEnrichment:
"""Test governance metadata extraction and severity routing."""
def test_context_extraction_tier1_p0(self):
mock_dag = MagicMock()
mock_dag.dag_id = "gold_analytics_pipeline"
mock_dag.params = {
"domain": "TataLingkunganPerizinan",
"owner": "dlh_data_engineering_group",
"tier": "Tier.Tier1"
}
mock_ti = MagicMock()
mock_ti.task_id = "dbt_gold_mart"
mock_ti.try_number = 2
mock_ti.max_tries = 3
mock_ti.duration = 42.5
mock_ti.log_url = "http://localhost:8088/log/123"
context = {
"dag": mock_dag,
"task_instance": mock_ti,
"exception": Exception("Table locked by concurrent vacuum process")
}
data = MericaAlertHook.extract_context(context)
assert data["dag_id"] == "gold_analytics_pipeline"
assert data["task_id"] == "dbt_gold_mart"
assert data["try_number"] == 2
assert data["domain"] == "TataLingkunganPerizinan"
assert data["tier"] == "Tier.Tier1"
assert "P0 (CRITICAL)" in data["severity"]
assert data["color_hex"] == "#E01E5A"
assert "Table locked" in data["error_msg"]
def test_context_extraction_tier2_p1(self):
mock_dag = MagicMock()
mock_dag.dag_id = "silver_cleaning_pipeline"
mock_dag.params = {"tier": "Tier.Tier2"}
mock_ti = MagicMock()
mock_ti.task_id = "cleanse_task"
mock_ti.try_number = 1
mock_ti.duration = 10.0
context = {
"dag": mock_dag,
"task_instance": mock_ti,
"exception": Exception("Syntax error")
}
data = MericaAlertHook.extract_context(context)
assert "P1 (WARNING)" in data["severity"]
assert data["color_hex"] == "#ECB22E"
class TestAirflowLifecycleCallbacks:
"""Test failure, SLA miss, and quality breach lifecycle handlers."""
@patch("helpers.merica_alert_hook.MericaAlertHook.send_telegram_alert")
@patch("helpers.merica_alert_hook.MericaAlertHook.send_slack_alert")
def test_on_failure_callback_dispatches_channels(self, mock_slack, mock_tg, monkeypatch):
monkeypatch.setenv("ALERT_TELEGRAM_BOT_TOKEN", "mock_bot_token")
monkeypatch.setenv("ALERT_TELEGRAM_CHAT_ID", "mock_chat_id")
monkeypatch.setenv("ALERT_SLACK_WEBHOOK_URL", "https://hooks.slack.com/mock")
mock_slack.return_value = True
mock_tg.return_value = True
mock_dag = MagicMock()
mock_dag.dag_id = "permits_medallion"
mock_dag.params = {"tier": "Tier.Tier1", "domain": "Perizinan", "owner": "DE"}
mock_ti = MagicMock()
mock_ti.task_id = "quality_gate_silver"
mock_ti.duration = 15.0
context = {
"dag": mock_dag,
"task_instance": mock_ti,
"exception": Exception("Check constraint violated: status must be in set")
}
results = MericaAlertHook.on_failure_callback(context)
assert results.get("telegram") is True
assert results.get("slack") is True
mock_tg.assert_called_once()
mock_slack.assert_called_once()
@patch("helpers.merica_alert_hook.MericaAlertHook.dispatch_alert")
def test_sla_miss_callback(self, mock_dispatch):
mock_dispatch.return_value = {"telegram": True}
mock_dag = MagicMock()
mock_dag.dag_id = "sla_test_dag"
res = MericaAlertHook.sla_miss_callback(
dag=mock_dag,
task_list=["task_1"],
blocking_task_list=["task_1"],
slas=[30],
blocking_tis=[]
)
assert res == {"telegram": True}
mock_dispatch.assert_called_once()
call_args = mock_dispatch.call_args[0]
assert call_args[0] == "SLA_MISS"
assert call_args[1]["dag_id"] == "sla_test_dag"
@patch("helpers.merica_alert_hook.MericaAlertHook.dispatch_alert")
def test_on_quality_gate_breach(self, mock_dispatch):
mock_dispatch.return_value = {"slack": True}
context = {
"dag": MagicMock(dag_id="spku_iot_pipeline", params={"tier": "Tier.Tier2"}),
"task_instance": MagicMock(task_id="ge_pm25_check")
}
failed_rules = [{"rule": "expect_column_values_to_be_between", "column": "pm25"}]
res = MericaAlertHook.on_quality_gate_breach(context, failed_rules, 75.0)
assert res == {"slack": True}
call_args = mock_dispatch.call_args[0]
assert call_args[0] == "QUALITY_GATE_BREACH"
assert "75.0%" in call_args[1]["error_msg"]
assert "pm25" in call_args[1]["error_msg"]