"""Integration tests for /api/v1/openapi/ endpoints. Tests the full API layer for the OpenAPI import review workflow, including job creation, status retrieval, classification updates, and approval triggering. """ from __future__ import annotations from unittest.mock import MagicMock, patch import pytest from httpx import ASGITransport, AsyncClient pytestmark = pytest.mark.integration def _build_app(): """Build a minimal FastAPI app with the openapi router and mocked deps.""" from fastapi import FastAPI, HTTPException from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from app.api_utils import envelope from app.openapi.review_api import router as openapi_router test_app = FastAPI() test_app.include_router(openapi_router) @test_app.exception_handler(HTTPException) async def _http_exc(request, exc): return JSONResponse( status_code=exc.status_code, content=envelope(None, success=False, error=exc.detail), ) @test_app.exception_handler(RequestValidationError) async def _validation_exc(request, exc): return JSONResponse( status_code=422, content=envelope(None, success=False, error=str(exc)), ) test_app.state.settings = MagicMock(admin_api_key="") return test_app @pytest.fixture(autouse=True) def _clear_job_store(): """Clear the in-memory job store between tests.""" from app.openapi.review_api import _job_store _job_store.clear() yield _job_store.clear() class TestImportEndpoint: """Tests for POST /api/v1/openapi/import.""" async def test_import_returns_202_with_job_id(self) -> None: """Starting an import returns 202 with a job_id.""" test_app = _build_app() async with AsyncClient( transport=ASGITransport(app=test_app), base_url="http://test" ) as client: resp = await client.post( "/api/v1/openapi/import", json={"url": "https://example.com/api/spec.json"}, ) assert resp.status_code == 202 body = resp.json() assert "job_id" in body assert body["status"] == "pending" assert body["spec_url"] == "https://example.com/api/spec.json" async def test_import_invalid_url_returns_422(self) -> None: """POST with invalid URL (no http/https) returns 422.""" test_app = _build_app() async with AsyncClient( transport=ASGITransport(app=test_app), base_url="http://test" ) as client: resp = await client.post( "/api/v1/openapi/import", json={"url": "ftp://example.com/spec.json"}, ) assert resp.status_code == 422 body = resp.json() assert body["success"] is False class TestJobStatusEndpoint: """Tests for GET /api/v1/openapi/jobs/{job_id}.""" async def test_get_existing_job_returns_status(self) -> None: """Retrieving an existing job returns its status.""" from app.openapi.review_api import _job_store _job_store["test-job-1"] = { "job_id": "test-job-1", "status": "done", "spec_url": "https://example.com/spec.json", "total_endpoints": 5, "classified_count": 5, "error_message": None, "classifications": [], } test_app = _build_app() async with AsyncClient( transport=ASGITransport(app=test_app), base_url="http://test" ) as client: resp = await client.get("/api/v1/openapi/jobs/test-job-1") assert resp.status_code == 200 body = resp.json() assert body["job_id"] == "test-job-1" assert body["status"] == "done" assert body["total_endpoints"] == 5 async def test_get_unknown_job_returns_404(self) -> None: """Retrieving a non-existent job returns 404 error envelope.""" test_app = _build_app() async with AsyncClient( transport=ASGITransport(app=test_app), base_url="http://test" ) as client: resp = await client.get("/api/v1/openapi/jobs/unknown-id-999") assert resp.status_code == 404 body = resp.json() assert body["success"] is False assert "not found" in body["error"].lower() class TestApproveEndpoint: """Tests for POST /api/v1/openapi/jobs/{job_id}/approve.""" async def test_approve_with_no_classifications_returns_400(self) -> None: """Approving a job with no classifications returns 400.""" from app.openapi.review_api import _job_store _job_store["empty-job"] = { "job_id": "empty-job", "status": "done", "spec_url": "https://example.com/spec.json", "total_endpoints": 0, "classified_count": 0, "error_message": None, "classifications": [], } test_app = _build_app() async with AsyncClient( transport=ASGITransport(app=test_app), base_url="http://test" ) as client: resp = await client.post("/api/v1/openapi/jobs/empty-job/approve") assert resp.status_code == 400 body = resp.json() assert body["success"] is False assert "no classifications" in body["error"].lower()