"""Integration tests for end-to-end MCP authentication behavior.""" from __future__ import annotations from unittest.mock import patch import pytest from fastapi.testclient import TestClient from aegis_gitea_mcp.config import reset_settings from aegis_gitea_mcp.oauth import reset_oauth_validator @pytest.fixture(autouse=True) def reset_state() -> None: """Reset global state between tests.""" reset_settings() reset_oauth_validator() yield reset_settings() reset_oauth_validator() @pytest.fixture def full_env(monkeypatch: pytest.MonkeyPatch) -> None: """Set OAuth-enabled environment for integration tests.""" monkeypatch.setenv("GITEA_URL", "https://gitea.example.com") monkeypatch.setenv("OAUTH_MODE", "true") monkeypatch.setenv("GITEA_OAUTH_CLIENT_ID", "test-client-id") monkeypatch.setenv("GITEA_OAUTH_CLIENT_SECRET", "test-client-secret") monkeypatch.setenv("ENVIRONMENT", "test") monkeypatch.setenv("MCP_HOST", "127.0.0.1") monkeypatch.setenv("MCP_PORT", "8080") monkeypatch.setenv("LOG_LEVEL", "INFO") monkeypatch.setenv("STARTUP_VALIDATE_GITEA", "false") @pytest.fixture def client(full_env: None, monkeypatch: pytest.MonkeyPatch) -> TestClient: """Create test client with deterministic OAuth behavior.""" async def _validate(_self, token: str | None, _ip: str, _ua: str): if token == "valid-read-token": return True, None, {"login": "alice", "scopes": ["read:repository"]} return False, "Invalid or expired OAuth token.", None monkeypatch.setattr( "aegis_gitea_mcp.oauth.GiteaOAuthValidator.validate_oauth_token", _validate, ) from aegis_gitea_mcp.server import app return TestClient(app) def test_no_token_returns_401_with_www_authenticate(client: TestClient) -> None: """Missing bearer token is rejected with OAuth challenge metadata.""" response = client.post( "/mcp/tool/call", json={"tool": "list_repositories", "arguments": {}}, ) assert response.status_code == 401 assert "WWW-Authenticate" in response.headers assert "resource_metadata=" in response.headers["WWW-Authenticate"] def test_invalid_token_returns_401(client: TestClient) -> None: """Invalid OAuth token is rejected.""" response = client.post( "/mcp/tool/call", headers={"Authorization": "Bearer invalid-token"}, json={"tool": "list_repositories", "arguments": {}}, ) assert response.status_code == 401 def test_valid_token_executes_tool(client: TestClient) -> None: """Valid OAuth token allows tool execution.""" with patch("aegis_gitea_mcp.gitea_client.GiteaClient.list_repositories") as mock_list_repos: mock_list_repos.return_value = [{"id": 1, "name": "repo-one", "owner": {"login": "alice"}}] response = client.post( "/mcp/tool/call", headers={"Authorization": "Bearer valid-read-token"}, json={"tool": "list_repositories", "arguments": {}}, ) assert response.status_code == 200 payload = response.json() assert payload["success"] is True assert "result" in payload def test_write_scope_enforcement_returns_403(client: TestClient) -> None: """Write tool calls are denied when token lacks write scope.""" response = client.post( "/mcp/tool/call", headers={"Authorization": "Bearer valid-read-token"}, json={ "tool": "create_issue", "arguments": {"owner": "acme", "repo": "demo", "title": "Needs write scope"}, }, ) assert response.status_code == 403 assert "required scope: write:repository" in response.json()["detail"].lower() def test_error_responses_include_helpful_messages(client: TestClient) -> None: """Auth failures include actionable guidance.""" response = client.post( "/mcp/tool/call", json={"tool": "list_repositories", "arguments": {}}, ) assert response.status_code == 401 data = response.json() assert "Provide Authorization" in data["message"]