Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)
Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
330 lines
11 KiB
Python
330 lines
11 KiB
Python
"""Unit tests for the PAT Manager alert sender module."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
import respx
|
|
from httpx import Response
|
|
|
|
from scripts.pat_manager.alerter import AlertSender
|
|
from scripts.pat_manager.errors import AlertError
|
|
from scripts.pat_manager.models import (
|
|
AlertChannel,
|
|
AlertResult,
|
|
CheckResult,
|
|
PatEntry,
|
|
PatStatus,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def expiring_result() -> CheckResult:
|
|
"""A CheckResult for a Jira token expiring in 7 days."""
|
|
return CheckResult(
|
|
entry=PatEntry(
|
|
service="Jira",
|
|
token_name="jira-api-access",
|
|
expiry_date="2025-08-01",
|
|
renewal_method="manual",
|
|
),
|
|
status=PatStatus.EXPIRING_SOON,
|
|
days_remaining=7,
|
|
error_message=None,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def expired_result() -> CheckResult:
|
|
"""A CheckResult for an expired Confluence token."""
|
|
return CheckResult(
|
|
entry=PatEntry(
|
|
service="Confluence",
|
|
token_name="confluence-bot",
|
|
expiry_date="2025-06-01",
|
|
renewal_method="manual",
|
|
),
|
|
status=PatStatus.EXPIRED,
|
|
days_remaining=0,
|
|
error_message=None,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def orgmylife_config() -> dict:
|
|
"""Config for OrgMyLife alert channel."""
|
|
return {
|
|
"orgmylife_url": "https://orgmylife.app",
|
|
"orgmylife_api_key": "test-api-key-123",
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def email_config() -> dict:
|
|
"""Config for email alert channel."""
|
|
return {
|
|
"smtp_host": "smtp.example.com",
|
|
"smtp_port": "587",
|
|
"smtp_user": "user@example.com",
|
|
"smtp_pass": "password123",
|
|
"alert_email_to": "admin@example.com",
|
|
}
|
|
|
|
|
|
class TestAlertSenderInit:
|
|
"""Tests for AlertSender initialization."""
|
|
|
|
def test_orgmylife_channel(self, orgmylife_config: dict) -> None:
|
|
sender = AlertSender(AlertChannel.ORGMYLIFE, orgmylife_config)
|
|
assert sender.channel == AlertChannel.ORGMYLIFE
|
|
assert sender.config == orgmylife_config
|
|
|
|
def test_email_channel(self, email_config: dict) -> None:
|
|
sender = AlertSender(AlertChannel.EMAIL, email_config)
|
|
assert sender.channel == AlertChannel.EMAIL
|
|
assert sender.config == email_config
|
|
|
|
|
|
class TestBuildTaskTitle:
|
|
"""Tests for task title formatting."""
|
|
|
|
def test_title_format(self, expiring_result: CheckResult, orgmylife_config: dict) -> None:
|
|
sender = AlertSender(AlertChannel.ORGMYLIFE, orgmylife_config)
|
|
title = sender._build_task_title(expiring_result)
|
|
assert title == "[PAT Renewal] Jira - jira-api-access"
|
|
|
|
def test_title_contains_service_and_token(
|
|
self, expired_result: CheckResult, orgmylife_config: dict
|
|
) -> None:
|
|
sender = AlertSender(AlertChannel.ORGMYLIFE, orgmylife_config)
|
|
title = sender._build_task_title(expired_result)
|
|
assert "Confluence" in title
|
|
assert "confluence-bot" in title
|
|
assert title == "[PAT Renewal] Confluence - confluence-bot"
|
|
|
|
|
|
class TestBuildTaskDescription:
|
|
"""Tests for task description formatting."""
|
|
|
|
def test_expiring_description(
|
|
self, expiring_result: CheckResult, orgmylife_config: dict
|
|
) -> None:
|
|
sender = AlertSender(AlertChannel.ORGMYLIFE, orgmylife_config)
|
|
desc = sender._build_task_description(expiring_result)
|
|
assert "7 days" in desc
|
|
assert "2025-08-01" in desc
|
|
assert "Manual renewal required" in desc
|
|
assert "Jira" in desc
|
|
assert "jira-api-access" in desc
|
|
|
|
def test_expired_description(
|
|
self, expired_result: CheckResult, orgmylife_config: dict
|
|
) -> None:
|
|
sender = AlertSender(AlertChannel.ORGMYLIFE, orgmylife_config)
|
|
desc = sender._build_task_description(expired_result)
|
|
assert "0 days remaining" in desc
|
|
assert "Token has expired" in desc
|
|
assert "Manual renewal required" in desc
|
|
|
|
|
|
class TestSendAlertOrgMyLife:
|
|
"""Tests for OrgMyLife alert sending."""
|
|
|
|
@pytest.mark.asyncio
|
|
@respx.mock
|
|
async def test_successful_task_creation(
|
|
self, expiring_result: CheckResult, orgmylife_config: dict
|
|
) -> None:
|
|
# Mock: no existing tasks
|
|
respx.get("https://orgmylife.app/api/tasks").mock(
|
|
return_value=Response(200, json=[])
|
|
)
|
|
# Mock: successful task creation
|
|
respx.post("https://orgmylife.app/api/tasks").mock(
|
|
return_value=Response(201, json={"id": 1, "title": "[PAT Renewal] Jira - jira-api-access"})
|
|
)
|
|
|
|
sender = AlertSender(AlertChannel.ORGMYLIFE, orgmylife_config)
|
|
result = await sender.send_alert(expiring_result)
|
|
|
|
assert result.sent is True
|
|
assert result.channel == AlertChannel.ORGMYLIFE
|
|
assert result.error_message is None
|
|
|
|
@pytest.mark.asyncio
|
|
@respx.mock
|
|
async def test_skips_when_existing_task(
|
|
self, expiring_result: CheckResult, orgmylife_config: dict
|
|
) -> None:
|
|
# Mock: existing task found
|
|
respx.get("https://orgmylife.app/api/tasks").mock(
|
|
return_value=Response(
|
|
200,
|
|
json=[{"title": "[PAT Renewal] Jira - jira-api-access", "status": "open"}],
|
|
)
|
|
)
|
|
|
|
sender = AlertSender(AlertChannel.ORGMYLIFE, orgmylife_config)
|
|
result = await sender.send_alert(expiring_result)
|
|
|
|
assert result.sent is False
|
|
assert result.channel == AlertChannel.ORGMYLIFE
|
|
assert result.error_message is None
|
|
|
|
@pytest.mark.asyncio
|
|
@respx.mock
|
|
async def test_raises_alert_error_on_failure(
|
|
self, expiring_result: CheckResult, orgmylife_config: dict
|
|
) -> None:
|
|
# Mock: no existing tasks
|
|
respx.get("https://orgmylife.app/api/tasks").mock(
|
|
return_value=Response(200, json=[])
|
|
)
|
|
# Mock: task creation fails
|
|
respx.post("https://orgmylife.app/api/tasks").mock(
|
|
return_value=Response(500, json={"error": "Internal Server Error"})
|
|
)
|
|
|
|
sender = AlertSender(AlertChannel.ORGMYLIFE, orgmylife_config)
|
|
with pytest.raises(AlertError) as exc_info:
|
|
await sender.send_alert(expiring_result)
|
|
|
|
assert "orgmylife" in exc_info.value.channel
|
|
assert "500" in exc_info.value.reason
|
|
|
|
@pytest.mark.asyncio
|
|
@respx.mock
|
|
async def test_api_key_header_sent(
|
|
self, expiring_result: CheckResult, orgmylife_config: dict
|
|
) -> None:
|
|
# Mock: no existing tasks
|
|
get_route = respx.get("https://orgmylife.app/api/tasks").mock(
|
|
return_value=Response(200, json=[])
|
|
)
|
|
# Mock: successful task creation
|
|
post_route = respx.post("https://orgmylife.app/api/tasks").mock(
|
|
return_value=Response(201, json={"id": 1})
|
|
)
|
|
|
|
sender = AlertSender(AlertChannel.ORGMYLIFE, orgmylife_config)
|
|
await sender.send_alert(expiring_result)
|
|
|
|
# Verify API key was sent
|
|
assert get_route.calls[0].request.headers["X-API-Key"] == "test-api-key-123"
|
|
assert post_route.calls[0].request.headers["X-API-Key"] == "test-api-key-123"
|
|
|
|
|
|
class TestCheckExistingTask:
|
|
"""Tests for idempotent alerting via check_existing_task."""
|
|
|
|
@pytest.mark.asyncio
|
|
@respx.mock
|
|
async def test_returns_true_when_task_exists(self, orgmylife_config: dict) -> None:
|
|
respx.get("https://orgmylife.app/api/tasks").mock(
|
|
return_value=Response(
|
|
200,
|
|
json=[{"title": "[PAT Renewal] Jira - jira-api-access", "status": "open"}],
|
|
)
|
|
)
|
|
|
|
sender = AlertSender(AlertChannel.ORGMYLIFE, orgmylife_config)
|
|
exists = await sender.check_existing_task("Jira", "jira-api-access", "2025-08-01")
|
|
assert exists is True
|
|
|
|
@pytest.mark.asyncio
|
|
@respx.mock
|
|
async def test_returns_false_when_no_task(self, orgmylife_config: dict) -> None:
|
|
respx.get("https://orgmylife.app/api/tasks").mock(
|
|
return_value=Response(200, json=[])
|
|
)
|
|
|
|
sender = AlertSender(AlertChannel.ORGMYLIFE, orgmylife_config)
|
|
exists = await sender.check_existing_task("Jira", "jira-api-access", "2025-08-01")
|
|
assert exists is False
|
|
|
|
@pytest.mark.asyncio
|
|
@respx.mock
|
|
async def test_returns_false_on_api_error(self, orgmylife_config: dict) -> None:
|
|
respx.get("https://orgmylife.app/api/tasks").mock(
|
|
return_value=Response(500, json={"error": "Server Error"})
|
|
)
|
|
|
|
sender = AlertSender(AlertChannel.ORGMYLIFE, orgmylife_config)
|
|
exists = await sender.check_existing_task("Jira", "jira-api-access", "2025-08-01")
|
|
assert exists is False
|
|
|
|
@pytest.mark.asyncio
|
|
@respx.mock
|
|
async def test_handles_wrapped_response(self, orgmylife_config: dict) -> None:
|
|
"""Test handling of paginated/wrapped API response."""
|
|
respx.get("https://orgmylife.app/api/tasks").mock(
|
|
return_value=Response(
|
|
200,
|
|
json={
|
|
"tasks": [
|
|
{"title": "[PAT Renewal] Jira - jira-api-access", "status": "open"}
|
|
]
|
|
},
|
|
)
|
|
)
|
|
|
|
sender = AlertSender(AlertChannel.ORGMYLIFE, orgmylife_config)
|
|
exists = await sender.check_existing_task("Jira", "jira-api-access", "2025-08-01")
|
|
assert exists is True
|
|
|
|
|
|
class TestSendAlertEmail:
|
|
"""Tests for email alert sending."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_raises_on_missing_smtp_config(
|
|
self, expiring_result: CheckResult
|
|
) -> None:
|
|
sender = AlertSender(AlertChannel.EMAIL, {})
|
|
with pytest.raises(AlertError) as exc_info:
|
|
await sender.send_alert(expiring_result)
|
|
assert "email" in exc_info.value.channel
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_raises_on_missing_email_to(
|
|
self, expiring_result: CheckResult
|
|
) -> None:
|
|
config = {"smtp_host": "smtp.example.com"}
|
|
sender = AlertSender(AlertChannel.EMAIL, config)
|
|
with pytest.raises(AlertError) as exc_info:
|
|
await sender.send_alert(expiring_result)
|
|
assert "email" in exc_info.value.channel
|
|
|
|
|
|
class TestDaysRemainingInAlert:
|
|
"""Tests for days remaining in alert messages."""
|
|
|
|
def test_expired_shows_zero_days(
|
|
self, expired_result: CheckResult, orgmylife_config: dict
|
|
) -> None:
|
|
sender = AlertSender(AlertChannel.ORGMYLIFE, orgmylife_config)
|
|
desc = sender._build_task_description(expired_result)
|
|
assert "0 days remaining" in desc
|
|
|
|
def test_expiring_shows_correct_days(
|
|
self, expiring_result: CheckResult, orgmylife_config: dict
|
|
) -> None:
|
|
sender = AlertSender(AlertChannel.ORGMYLIFE, orgmylife_config)
|
|
desc = sender._build_task_description(expiring_result)
|
|
assert "7 days" in desc
|
|
|
|
def test_none_days_remaining_treated_as_zero(self, orgmylife_config: dict) -> None:
|
|
result = CheckResult(
|
|
entry=PatEntry(
|
|
service="OrgMyLife",
|
|
token_name="test-token",
|
|
expiry_date=None,
|
|
renewal_method="manual",
|
|
),
|
|
status=PatStatus.EXPIRED,
|
|
days_remaining=None,
|
|
error_message=None,
|
|
)
|
|
sender = AlertSender(AlertChannel.ORGMYLIFE, orgmylife_config)
|
|
desc = sender._build_task_description(result)
|
|
assert "0 days remaining" in desc
|