Files
smart-support/backend/tests/e2e/test_openapi_import.py
Yaojia Wang f0699436c5 refactor: engineering improvements -- API versioning, structured logging, Alembic, error standardization, test coverage
- API versioning: all REST endpoints prefixed with /api/v1/
- Structured logging: replaced stdlib logging with structlog (console/JSON modes)
- Alembic migrations: versioned DB schema with initial migration
- Error standardization: global exception handlers for consistent envelope format
- Interrupt cleanup: asyncio background task for expired interrupt removal
- Integration tests: +30 tests (analytics, replay, openapi, error, session APIs)
- Frontend tests: +57 tests (all components, pages, useWebSocket hook)
- Backend: 557 tests, 89.75% coverage | Frontend: 80 tests, 16 test files
2026-04-06 23:19:29 +02:00

202 lines
6.8 KiB
Python

"""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