"""E2E tests for OpenAPI import flow (flow 5). Flow 5: paste OpenAPI spec URL -> import job -> classify endpoints -> review classifications -> approve -> tool generation. """ from __future__ import annotations from unittest.mock import AsyncMock, patch import pytest from starlette.testclient import TestClient from app.openapi.models import ClassificationResult, EndpointInfo from app.openapi.review_api import _job_store from tests.e2e.conftest import create_e2e_app pytestmark = pytest.mark.e2e def _fake_endpoint( path: str = "/orders/{id}", method: str = "GET", operation_id: str = "getOrder", summary: str = "Get order details", ) -> EndpointInfo: return EndpointInfo( path=path, method=method, operation_id=operation_id, summary=summary, description="", parameters=(), request_body_schema=None, response_schema=None, ) def _fake_classification( endpoint: EndpointInfo | None = None, access_type: str = "read", needs_interrupt: bool = False, agent_group: str = "order_lookup", ) -> ClassificationResult: return ClassificationResult( endpoint=endpoint or _fake_endpoint(), access_type=access_type, customer_params=["order_id"], agent_group=agent_group, confidence=0.95, needs_interrupt=needs_interrupt, ) class TestFlow5OpenAPIImport: """Flow 5: full OpenAPI import lifecycle.""" def test_import_job_lifecycle(self) -> None: """Start import -> check status -> review classifications -> approve.""" app = create_e2e_app() with TestClient(app) as client: # Step 1: Start import job resp = client.post( "/api/v1/openapi/import", json={"url": "https://api.example.com/openapi.json"}, ) assert resp.status_code == 202 body = resp.json() assert body["status"] == "pending" job_id = body["job_id"] # Step 2: Check job status (still pending since background task hasn't run) resp = client.get(f"/api/v1/openapi/jobs/{job_id}") assert resp.status_code == 200 assert resp.json()["job_id"] == job_id def test_import_job_with_classifications(self) -> None: """Simulate completed import and review classified endpoints.""" app = create_e2e_app() # Seed a completed job directly ep_read = _fake_endpoint("/orders/{id}", "GET", "getOrder", "Get order") ep_write = _fake_endpoint("/orders/{id}/cancel", "POST", "cancelOrder", "Cancel order") clf_read = _fake_classification(ep_read, "read", False, "order_lookup") clf_write = _fake_classification(ep_write, "write", True, "order_actions") job_id = "test-job-001" _job_store[job_id] = { "job_id": job_id, "status": "done", "spec_url": "https://api.example.com/openapi.json", "total_endpoints": 2, "classified_count": 2, "error_message": None, "classifications": [clf_read, clf_write], } with TestClient(app) as client: # Step 1: Get classifications resp = client.get(f"/api/v1/openapi/jobs/{job_id}/classifications") assert resp.status_code == 200 classifications = resp.json() assert len(classifications) == 2 # Verify read endpoint read_clf = classifications[0] assert read_clf["access_type"] == "read" assert read_clf["needs_interrupt"] is False assert read_clf["endpoint"]["path"] == "/orders/{id}" # Verify write endpoint write_clf = classifications[1] assert write_clf["access_type"] == "write" assert write_clf["needs_interrupt"] is True assert write_clf["endpoint"]["path"] == "/orders/{id}/cancel" # Step 2: Update a classification resp = client.put( f"/api/v1/openapi/jobs/{job_id}/classifications/0", json={ "access_type": "write", "needs_interrupt": True, "agent_group": "order_actions", }, ) assert resp.status_code == 200 updated = resp.json() assert updated["access_type"] == "write" assert updated["needs_interrupt"] is True assert updated["agent_group"] == "order_actions" # Step 3: Approve the job resp = client.post(f"/api/v1/openapi/jobs/{job_id}/approve") assert resp.status_code == 200 assert resp.json()["status"] == "approved" def test_import_nonexistent_job_returns_404(self) -> None: app = create_e2e_app() with TestClient(app) as client: resp = client.get("/api/v1/openapi/jobs/nonexistent") assert resp.status_code == 404 def test_import_invalid_url_returns_422(self) -> None: app = create_e2e_app() with TestClient(app) as client: resp = client.post("/api/v1/openapi/import", json={"url": "not-a-url"}) assert resp.status_code == 422 def test_classification_index_out_of_range(self) -> None: app = create_e2e_app() job_id = "test-job-range" _job_store[job_id] = { "job_id": job_id, "status": "done", "spec_url": "https://example.com/spec.json", "total_endpoints": 1, "classified_count": 1, "error_message": None, "classifications": [_fake_classification()], } with TestClient(app) as client: resp = client.put( f"/api/v1/openapi/jobs/{job_id}/classifications/99", json={ "access_type": "read", "needs_interrupt": False, "agent_group": "order_lookup", }, ) assert resp.status_code == 404 def test_update_classification_invalid_agent_group(self) -> None: app = create_e2e_app() job_id = "test-job-invalid" _job_store[job_id] = { "job_id": job_id, "status": "done", "spec_url": "https://example.com/spec.json", "total_endpoints": 1, "classified_count": 1, "error_message": None, "classifications": [_fake_classification()], } with TestClient(app) as client: resp = client.put( f"/api/v1/openapi/jobs/{job_id}/classifications/0", json={ "access_type": "read", "needs_interrupt": False, "agent_group": "invalid group!", # spaces and special chars }, ) assert resp.status_code == 422