Migrate all repos into monorepo context folders

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.
This commit is contained in:
2026-06-30 20:39:52 +02:00
parent 2f2b295531
commit a5f8fb49ab
1717 changed files with 447332 additions and 0 deletions
@@ -0,0 +1,466 @@
"""Integration tests for the PAT Manager orchestrator flow.
Tests the full end-to-end orchestrator with mocked external APIs using respx.
Validates Requirements: 2.5, 2.6, 3.2, 3.4, 3.5, 4.6, 5.4, 5.5, 6.8
"""
from __future__ import annotations
import base64
import json
from datetime import date, timedelta
from pathlib import Path
from unittest.mock import AsyncMock, patch
import httpx
import pytest
import respx
from nacl.public import PrivateKey
from scripts.pat_manager.__main__ import main, REGISTRY_PATH
def _future_date(days: int = 60) -> str:
"""Return an ISO date string `days` in the future."""
return (date.today() + timedelta(days=days)).isoformat()
def _expiring_date(days: int = 7) -> str:
"""Return an ISO date string `days` in the future (within 14-day window)."""
return (date.today() + timedelta(days=days)).isoformat()
def _write_registry(tmp_path: Path, tokens: list[dict]) -> Path:
"""Write a registry JSON file to tmp_path and return its path."""
registry_path = tmp_path / "pat-registry.json"
data = {"version": "1.0", "tokens": tokens}
registry_path.write_text(json.dumps(data, indent=2), encoding="utf-8")
return registry_path
def _generate_keypair():
"""Generate a NaCl keypair for mocking GitHub secret encryption."""
private_key = PrivateKey.generate()
public_key = private_key.public_key
public_key_b64 = base64.b64encode(bytes(public_key)).decode("utf-8")
return private_key, public_key, public_key_b64
def _set_base_env(monkeypatch, tmp_path: Path, registry_path: Path):
"""Set common environment variables for all integration tests."""
monkeypatch.setattr(
"scripts.pat_manager.__main__.REGISTRY_PATH", registry_path
)
monkeypatch.setenv("GITLAB_PAT", "glpat-test-token-123")
monkeypatch.setenv("JIRA_PAT", "jira-test-token-456")
monkeypatch.setenv("CONFLUENCE_PAT", "confluence-test-token-789")
monkeypatch.setenv("ORGMYLIFE_API_KEY", "orgmylife-api-key-abc")
monkeypatch.setenv("GH_TOKEN", "ghp_github_token_xyz")
monkeypatch.setenv("GITLAB_URL", "https://gitlab.example.com")
monkeypatch.setenv("JIRA_URL", "https://jira.example.com")
monkeypatch.setenv("CONFLUENCE_URL", "https://confluence.example.com")
monkeypatch.setenv("ORGMYLIFE_URL", "https://orgmylife.example.com")
monkeypatch.setenv("ALERT_CHANNEL", "orgmylife")
monkeypatch.setenv("GITLAB_TOKEN_ID", "12345")
monkeypatch.setenv("GITHUB_REPOSITORY", "test-owner/test-repo")
# ---------------------------------------------------------------------------
# Test 1: Happy path - all healthy → no alerts, summary only
# Validates: Requirements 5.4, 5.5
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
@respx.mock
async def test_happy_path_all_healthy(tmp_path, monkeypatch, capsys):
"""All PATs are healthy (far-future expiry) → exit 0, no alerts sent."""
far_future = _future_date(60)
tokens = [
{
"service": "GitLab",
"token_name": "ci-token",
"expiry_date": far_future,
"renewal_method": "auto",
},
{
"service": "Jira",
"token_name": "jira-access",
"expiry_date": far_future,
"renewal_method": "manual",
},
{
"service": "OrgMyLife",
"token_name": "oml-key",
"expiry_date": far_future,
"renewal_method": "manual",
},
]
registry_path = _write_registry(tmp_path, tokens)
_set_base_env(monkeypatch, tmp_path, registry_path)
# Mock GitLab check → healthy (expires_at far in future)
respx.get("https://gitlab.example.com/api/v4/personal_access_tokens/self").mock(
return_value=httpx.Response(200, json={"expires_at": far_future})
)
# Mock Jira check → 200 (token works)
respx.get("https://jira.example.com/rest/api/2/myself").mock(
return_value=httpx.Response(200, json={"name": "user"})
)
# Mock OrgMyLife check → 200 (token works)
respx.get("https://orgmylife.example.com/api/tasks").mock(
return_value=httpx.Response(200, json=[])
)
# No alert routes should be called
alert_route = respx.post("https://orgmylife.example.com/api/tasks").mock(
return_value=httpx.Response(201, json={"id": 1})
)
# Mock asyncio.sleep to avoid delays
monkeypatch.setattr("asyncio.sleep", AsyncMock())
exit_code = await main()
assert exit_code == 0
# No alerts should have been sent
assert not alert_route.called
# Summary report should be printed
captured = capsys.readouterr()
assert "PAT Lifecycle Check Summary" in captured.out
assert "ci-token" in captured.out
assert "jira-access" in captured.out
assert "oml-key" in captured.out
# ---------------------------------------------------------------------------
# Test 2: GitLab rotation flow with mocked APIs
# Validates: Requirements 3.2, 3.4
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
@respx.mock
async def test_gitlab_rotation_flow(tmp_path, monkeypatch, capsys):
"""GitLab token expiring soon → rotation + secret update → registry updated."""
expiring = _expiring_date(7)
new_expiry = _future_date(90)
_, _, public_key_b64 = _generate_keypair()
tokens = [
{
"service": "GitLab",
"token_name": "ci-token",
"expiry_date": expiring,
"renewal_method": "auto",
},
]
registry_path = _write_registry(tmp_path, tokens)
_set_base_env(monkeypatch, tmp_path, registry_path)
# Mock GitLab check → expiring soon
respx.get("https://gitlab.example.com/api/v4/personal_access_tokens/self").mock(
return_value=httpx.Response(200, json={"expires_at": expiring})
)
# Mock GitLab rotation → success
respx.post(
"https://gitlab.example.com/api/v4/personal_access_tokens/12345/rotate"
).mock(
return_value=httpx.Response(
200,
json={"token": "glpat-new-rotated-token", "expires_at": new_expiry},
)
)
# Mock GitHub public key fetch
respx.get(
"https://api.github.com/repos/test-owner/test-repo/actions/secrets/public-key"
).mock(
return_value=httpx.Response(
200, json={"key": public_key_b64, "key_id": "key-123"}
)
)
# Mock GitHub secret update → success
respx.put(
"https://api.github.com/repos/test-owner/test-repo/actions/secrets/GITLAB_PAT"
).mock(return_value=httpx.Response(204))
# Mock asyncio.sleep to avoid delays
monkeypatch.setattr("asyncio.sleep", AsyncMock())
exit_code = await main()
assert exit_code == 0
# Verify registry was updated with new expiry date
updated_registry = json.loads(registry_path.read_text(encoding="utf-8"))
assert updated_registry["tokens"][0]["expiry_date"] == new_expiry
# Verify rotation was logged
captured = capsys.readouterr()
assert "Rotated GitLab token" in captured.out
assert new_expiry in captured.out
# ---------------------------------------------------------------------------
# Test 3: Rotation success + secret update failure → alert triggered
# Validates: Requirements 3.5
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
@respx.mock
async def test_rotation_success_secret_update_failure_triggers_alert(
tmp_path, monkeypatch, capsys
):
"""Rotation succeeds but GitHub secret update fails → alert is triggered."""
expiring = _expiring_date(7)
new_expiry = _future_date(90)
_, _, public_key_b64 = _generate_keypair()
tokens = [
{
"service": "GitLab",
"token_name": "ci-token",
"expiry_date": expiring,
"renewal_method": "auto",
},
]
registry_path = _write_registry(tmp_path, tokens)
_set_base_env(monkeypatch, tmp_path, registry_path)
# Mock GitLab check → expiring soon
respx.get("https://gitlab.example.com/api/v4/personal_access_tokens/self").mock(
return_value=httpx.Response(200, json={"expires_at": expiring})
)
# Mock GitLab rotation → success
respx.post(
"https://gitlab.example.com/api/v4/personal_access_tokens/12345/rotate"
).mock(
return_value=httpx.Response(
200,
json={"token": "glpat-new-rotated-token", "expires_at": new_expiry},
)
)
# Mock GitHub public key fetch → success
respx.get(
"https://api.github.com/repos/test-owner/test-repo/actions/secrets/public-key"
).mock(
return_value=httpx.Response(
200, json={"key": public_key_b64, "key_id": "key-123"}
)
)
# Mock GitHub secret update → FAILURE (500)
respx.put(
"https://api.github.com/repos/test-owner/test-repo/actions/secrets/GITLAB_PAT"
).mock(return_value=httpx.Response(500))
# Mock OrgMyLife GET tasks (check existing) → no existing tasks
orgmylife_get_route = respx.get("https://orgmylife.example.com/api/tasks").mock(
return_value=httpx.Response(200, json=[])
)
# Mock OrgMyLife POST tasks (create alert) → success
orgmylife_post_route = respx.post("https://orgmylife.example.com/api/tasks").mock(
return_value=httpx.Response(201, json={"id": 42})
)
# Mock asyncio.sleep to avoid delays
monkeypatch.setattr("asyncio.sleep", AsyncMock())
exit_code = await main()
# Alert was delivered successfully, so exit code is 0
assert exit_code == 0
# Verify alert was triggered (POST to OrgMyLife)
assert orgmylife_post_route.called
# Verify registry was NOT updated (secret update failed)
updated_registry = json.loads(registry_path.read_text(encoding="utf-8"))
assert updated_registry["tokens"][0]["expiry_date"] == expiring
# ---------------------------------------------------------------------------
# Test 4: Alert channel unreachable → non-zero exit code
# Validates: Requirements 4.6
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
@respx.mock
async def test_alert_channel_unreachable_nonzero_exit(tmp_path, monkeypatch, capsys):
"""Alert channel (OrgMyLife) unreachable → exit code 1."""
expiring = _expiring_date(7)
tokens = [
{
"service": "Jira",
"token_name": "jira-access",
"expiry_date": expiring,
"renewal_method": "manual",
},
]
registry_path = _write_registry(tmp_path, tokens)
_set_base_env(monkeypatch, tmp_path, registry_path)
# Mock Jira check → 200 (token works, but expiry within window)
respx.get("https://jira.example.com/rest/api/2/myself").mock(
return_value=httpx.Response(200, json={"name": "user"})
)
# Mock OrgMyLife GET tasks → connection error
respx.get("https://orgmylife.example.com/api/tasks").mock(
side_effect=httpx.ConnectError("Connection refused")
)
# Mock OrgMyLife POST tasks → connection error
respx.post("https://orgmylife.example.com/api/tasks").mock(
side_effect=httpx.ConnectError("Connection refused")
)
# Mock asyncio.sleep to avoid delays
monkeypatch.setattr("asyncio.sleep", AsyncMock())
exit_code = await main()
# Alert delivery failed → exit code 1
assert exit_code == 1
# ---------------------------------------------------------------------------
# Test 5: Service timeout after retries → "check failed" classification
# Validates: Requirements 2.5
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
@respx.mock
async def test_service_timeout_after_retries_check_failed(
tmp_path, monkeypatch, capsys
):
"""Service times out on all retry attempts → classified as 'check failed'."""
tokens = [
{
"service": "Jira",
"token_name": "jira-access",
"expiry_date": _future_date(30),
"renewal_method": "manual",
},
]
registry_path = _write_registry(tmp_path, tokens)
_set_base_env(monkeypatch, tmp_path, registry_path)
# Mock Jira API → timeout on all attempts
respx.get("https://jira.example.com/rest/api/2/myself").mock(
side_effect=httpx.TimeoutException("Request timed out")
)
# Mock asyncio.sleep to avoid delays in retry logic
monkeypatch.setattr("asyncio.sleep", AsyncMock())
exit_code = await main()
# "check failed" doesn't trigger alerts, so exit code is 0
assert exit_code == 0
# Verify summary shows "check failed"
captured = capsys.readouterr()
assert "check failed" in captured.out
assert "jira-access" in captured.out
# ---------------------------------------------------------------------------
# Test 6: Null expiry_date classification scenarios
# Validates: Requirements 2.6, 6.8
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
@respx.mock
async def test_null_expiry_date_api_success_classified_healthy(
tmp_path, monkeypatch, capsys
):
"""Null expiry_date + API success → classified as 'healthy'."""
tokens = [
{
"service": "OrgMyLife",
"token_name": "oml-key",
"expiry_date": None,
"renewal_method": "manual",
},
]
registry_path = _write_registry(tmp_path, tokens)
_set_base_env(monkeypatch, tmp_path, registry_path)
# Mock OrgMyLife check → 200 (token works)
respx.get("https://orgmylife.example.com/api/tasks").mock(
return_value=httpx.Response(200, json=[])
)
# No alert routes should be called
alert_route = respx.post("https://orgmylife.example.com/api/tasks").mock(
return_value=httpx.Response(201, json={"id": 1})
)
# Mock asyncio.sleep to avoid delays
monkeypatch.setattr("asyncio.sleep", AsyncMock())
exit_code = await main()
assert exit_code == 0
assert not alert_route.called
# Verify summary shows "healthy"
captured = capsys.readouterr()
assert "healthy" in captured.out
assert "oml-key" in captured.out
@pytest.mark.asyncio
@respx.mock
async def test_null_expiry_date_api_401_classified_expired(
tmp_path, monkeypatch, capsys
):
"""Null expiry_date + API returns 401 → classified as 'expired'."""
tokens = [
{
"service": "OrgMyLife",
"token_name": "oml-key",
"expiry_date": None,
"renewal_method": "manual",
},
]
registry_path = _write_registry(tmp_path, tokens)
_set_base_env(monkeypatch, tmp_path, registry_path)
# Mock OrgMyLife check → 401 (token expired/revoked)
respx.get("https://orgmylife.example.com/api/tasks").mock(
return_value=httpx.Response(401)
)
# Mock OrgMyLife alert task creation → success
respx.post("https://orgmylife.example.com/api/tasks").mock(
return_value=httpx.Response(201, json={"id": 99})
)
# Mock asyncio.sleep to avoid delays
monkeypatch.setattr("asyncio.sleep", AsyncMock())
exit_code = await main()
assert exit_code == 0
# Verify summary shows "expired"
captured = capsys.readouterr()
assert "expired" in captured.out
assert "oml-key" in captured.out