"""Tests for approvals endpoints with operator token authentication. Phase 5 - Step 3: Verifies that POST /approvals/{thread_id} and GET /approvals/pending require operator token when configured. Written FIRST (TDD RED phase). """ from datetime import datetime, timezone from unittest.mock import AsyncMock, MagicMock, patch import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from release_agent.api.approvals import router as approvals_router # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_test_app(operator_token: str = "") -> FastAPI: """Return a FastAPI app with approvals router and configurable operator token.""" app = FastAPI() app.include_router(approvals_router) mock_settings = MagicMock() mock_settings.operator_token.get_secret_value.return_value = operator_token mock_pool = MagicMock() mock_conn = AsyncMock() mock_cursor = AsyncMock() mock_cursor.fetchall = AsyncMock(return_value=[]) mock_cursor.fetchone = AsyncMock(return_value=("pr_completed",)) mock_cursor.__aenter__ = AsyncMock(return_value=mock_cursor) mock_cursor.__aexit__ = AsyncMock(return_value=False) mock_conn.cursor = MagicMock(return_value=mock_cursor) mock_conn.__aenter__ = AsyncMock(return_value=mock_conn) mock_conn.__aexit__ = AsyncMock(return_value=False) mock_pool.connection = MagicMock(return_value=mock_conn) app.state.settings = mock_settings app.state.graphs = { "pr_completed": MagicMock(), "release": MagicMock(), } app.state.tool_clients = MagicMock() app.state.db_pool = mock_pool app.state.background_tasks = set() return app # --------------------------------------------------------------------------- # POST /approvals/{thread_id} with auth # --------------------------------------------------------------------------- class TestPostApprovalWithAuth: def test_valid_token_allows_post_approval(self) -> None: app = _make_test_app(operator_token="secret-token") with patch( "release_agent.api.approvals._resume_graph", new_callable=AsyncMock ) as mock_resume: mock_resume.return_value = {} with TestClient(app) as client: response = client.post( "/approvals/thread-123", json={"decision": "merge"}, headers={"X-Operator-Token": "secret-token"}, ) assert response.status_code == 200 def test_missing_token_rejects_post_approval(self) -> None: app = _make_test_app(operator_token="secret-token") with TestClient(app) as client: response = client.post( "/approvals/thread-123", json={"decision": "merge"}, ) assert response.status_code == 401 def test_wrong_token_rejects_post_approval(self) -> None: app = _make_test_app(operator_token="secret-token") with TestClient(app) as client: response = client.post( "/approvals/thread-123", json={"decision": "merge"}, headers={"X-Operator-Token": "wrong-token"}, ) assert response.status_code == 401 def test_no_auth_required_when_token_not_configured(self) -> None: app = _make_test_app(operator_token="") with patch( "release_agent.api.approvals._resume_graph", new_callable=AsyncMock ) as mock_resume: mock_resume.return_value = {} with TestClient(app) as client: response = client.post( "/approvals/thread-123", json={"decision": "merge"}, ) assert response.status_code == 200 # --------------------------------------------------------------------------- # GET /approvals/pending with auth # --------------------------------------------------------------------------- class TestGetPendingApprovalsWithAuth: def test_valid_token_allows_get_pending(self) -> None: app = _make_test_app(operator_token="secret-token") with TestClient(app) as client: response = client.get( "/approvals/pending", headers={"X-Operator-Token": "secret-token"}, ) assert response.status_code == 200 def test_missing_token_rejects_get_pending(self) -> None: app = _make_test_app(operator_token="secret-token") with TestClient(app) as client: response = client.get("/approvals/pending") assert response.status_code == 401 def test_wrong_token_rejects_get_pending(self) -> None: app = _make_test_app(operator_token="secret-token") with TestClient(app) as client: response = client.get( "/approvals/pending", headers={"X-Operator-Token": "wrong"}, ) assert response.status_code == 401 def test_no_auth_required_when_token_not_configured(self) -> None: app = _make_test_app(operator_token="") with TestClient(app) as client: response = client.get("/approvals/pending") assert response.status_code == 200