from __future__ import annotations from dataclasses import replace from types import SimpleNamespace import pytest import app.mcp.tools as mcp_tools_module from app.config import get_settings from app.security.auth import AuthBackend class _DummyCoreService: def check_availability( self, start: str, end: str, calendar_ids: list[str] | None, ) -> SimpleNamespace: return SimpleNamespace( start=start, end=end, available=True, busy_slots=[], checked_calendars=calendar_ids or ["primary"], ) def scan_mailbox(self, max_results: int) -> SimpleNamespace: return SimpleNamespace( scanned=max_results, linkedin=0, advertising=0, veille_techno=0, skipped=0, failed=0, ) def available_meeting_intervals( self, start: str, end: str, calendar_ids: list[str] | None, ) -> SimpleNamespace: return SimpleNamespace( start=start, end=end, timezone="Europe/Paris", meeting_intervals=[ SimpleNamespace( start="2026-03-10T08:30:00+01:00", end="2026-03-10T09:00:00+01:00", ) ], checked_calendars=calendar_ids or ["primary"], ) class _DummyCtx: def __init__(self, headers: dict[str, str]) -> None: self.request_context = SimpleNamespace( request=SimpleNamespace(headers=headers) ) def test_mcp_check_availability_requires_auth(monkeypatch) -> None: auth_settings = replace( get_settings(), auth_mode="api_key", agent_api_key="mcp-key", auth_jwt_secret="", ) monkeypatch.setattr(mcp_tools_module, "auth_backend", AuthBackend(auth_settings)) monkeypatch.setattr(mcp_tools_module, "core_service", _DummyCoreService()) with pytest.raises(PermissionError): mcp_tools_module.check_availability( start="2026-03-10T09:00:00+01:00", end="2026-03-10T10:00:00+01:00", calendar_ids=["primary"], ctx=_DummyCtx(headers={}), ) def test_mcp_check_availability_with_api_key(monkeypatch) -> None: auth_settings = replace( get_settings(), auth_mode="api_key", agent_api_key="mcp-key", auth_jwt_secret="", ) monkeypatch.setattr(mcp_tools_module, "auth_backend", AuthBackend(auth_settings)) monkeypatch.setattr(mcp_tools_module, "core_service", _DummyCoreService()) payload = mcp_tools_module.check_availability( start="2026-03-10T09:00:00+01:00", end="2026-03-10T10:00:00+01:00", calendar_ids=["primary"], ctx=_DummyCtx(headers={"x-api-key": "mcp-key"}), ) assert payload["available"] is True assert payload["checked_calendars"] == ["primary"] def test_mcp_scan_mailbox_requires_mail_scan_scope(monkeypatch) -> None: auth_settings = replace( get_settings(), auth_mode="api_key", agent_api_key="mcp-key", auth_jwt_secret="", ) monkeypatch.setattr(mcp_tools_module, "auth_backend", AuthBackend(auth_settings)) monkeypatch.setattr(mcp_tools_module, "core_service", _DummyCoreService()) payload = mcp_tools_module.scan_mailbox( max_results=10, ctx=_DummyCtx(headers={"x-api-key": "mcp-key"}), ) assert payload["scanned"] == 10 def test_mcp_available_meeting_intervals_requires_auth(monkeypatch) -> None: auth_settings = replace( get_settings(), auth_mode="api_key", agent_api_key="mcp-key", auth_jwt_secret="", ) monkeypatch.setattr(mcp_tools_module, "auth_backend", AuthBackend(auth_settings)) monkeypatch.setattr(mcp_tools_module, "core_service", _DummyCoreService()) with pytest.raises(PermissionError): mcp_tools_module.available_meeting_intervals( start="2026-03-10T08:00:00+01:00", end="2026-03-10T10:00:00+01:00", calendar_ids=["primary"], ctx=_DummyCtx(headers={}), ) def test_mcp_available_meeting_intervals_with_api_key(monkeypatch) -> None: auth_settings = replace( get_settings(), auth_mode="api_key", agent_api_key="mcp-key", auth_jwt_secret="", ) monkeypatch.setattr(mcp_tools_module, "auth_backend", AuthBackend(auth_settings)) monkeypatch.setattr(mcp_tools_module, "core_service", _DummyCoreService()) payload = mcp_tools_module.available_meeting_intervals( start="2026-03-10T08:00:00+01:00", end="2026-03-10T10:00:00+01:00", calendar_ids=["primary"], ctx=_DummyCtx(headers={"x-api-key": "mcp-key"}), ) assert payload["timezone"] == "Europe/Paris" assert payload["meeting_intervals"] == [ { "start": "2026-03-10T08:30:00+01:00", "end": "2026-03-10T09:00:00+01:00", } ] def test_mcp_auth_mode_oauth_uses_bearer_token(monkeypatch) -> None: mcp_settings = replace( get_settings(), auth_mode="api_key", agent_api_key="api-key-not-used-for-mcp", auth_jwt_secret="", mcp_auth_mode="oauth", mcp_oauth_introspection_url="https://issuer.example/introspect", mcp_oauth_issuer="https://issuer.example", mcp_oauth_audience="personal-agent-mcp", ) backend = mcp_tools_module._build_mcp_auth_backend(mcp_settings) monkeypatch.setattr( backend, "_introspect_oauth_token", lambda _: { "active": True, "sub": "oauth-agent", "iss": "https://issuer.example", "aud": "personal-agent-mcp", "scope": "availability:read", }, ) monkeypatch.setattr(mcp_tools_module, "auth_backend", backend) monkeypatch.setattr(mcp_tools_module, "core_service", _DummyCoreService()) payload = mcp_tools_module.check_availability( start="2026-03-10T09:00:00+01:00", end="2026-03-10T10:00:00+01:00", calendar_ids=["primary"], ctx=_DummyCtx(headers={"authorization": "Bearer oauth-token"}), ) assert payload["available"] is True assert payload["checked_calendars"] == ["primary"] def test_mcp_available_meeting_intervals_oauth_requires_new_scope(monkeypatch) -> None: mcp_settings = replace( get_settings(), auth_mode="api_key", agent_api_key="api-key-not-used-for-mcp", auth_jwt_secret="", mcp_auth_mode="oauth", mcp_oauth_introspection_url="https://issuer.example/introspect", mcp_oauth_issuer="https://issuer.example", mcp_oauth_audience="personal-agent-mcp", ) backend = mcp_tools_module._build_mcp_auth_backend(mcp_settings) monkeypatch.setattr( backend, "_introspect_oauth_token", lambda _: { "active": True, "sub": "oauth-agent", "iss": "https://issuer.example", "aud": "personal-agent-mcp", "scope": "availability:read", }, ) monkeypatch.setattr(mcp_tools_module, "auth_backend", backend) monkeypatch.setattr(mcp_tools_module, "core_service", _DummyCoreService()) with pytest.raises(PermissionError, match="available_meeting_intervals:read"): mcp_tools_module.available_meeting_intervals( start="2026-03-10T08:00:00+01:00", end="2026-03-10T10:00:00+01:00", calendar_ids=["primary"], ctx=_DummyCtx(headers={"authorization": "Bearer oauth-token"}), )