"""Tests for OpenAPI endpoint parser module. RED phase: written before implementation. """ from __future__ import annotations import pytest pytestmark = pytest.mark.unit _MINIMAL_SPEC = { "openapi": "3.0.0", "info": {"title": "Test API", "version": "1.0.0"}, "paths": {}, } _GET_SPEC = { "openapi": "3.0.0", "info": {"title": "Orders API", "version": "1.0.0"}, "paths": { "/orders/{order_id}": { "get": { "operationId": "get_order", "summary": "Get an order", "description": "Retrieves a single order by ID", "parameters": [ { "name": "order_id", "in": "path", "required": True, "schema": {"type": "string"}, "description": "The order identifier", } ], "responses": { "200": { "description": "Order found", "content": { "application/json": { "schema": { "type": "object", "properties": {"id": {"type": "string"}}, } } }, } }, } } }, } _POST_SPEC = { "openapi": "3.0.0", "info": {"title": "Orders API", "version": "1.0.0"}, "paths": { "/orders": { "post": { "operationId": "create_order", "summary": "Create an order", "description": "Creates a new order", "requestBody": { "required": True, "content": { "application/json": { "schema": { "type": "object", "properties": { "item": {"type": "string"}, "quantity": {"type": "integer"}, }, } } }, }, "responses": {"201": {"description": "Created"}}, } } }, } _MULTI_PARAM_SPEC = { "openapi": "3.0.0", "info": {"title": "Items API", "version": "1.0.0"}, "paths": { "/items/{item_id}": { "get": { "operationId": "get_item", "summary": "Get item", "description": "", "parameters": [ { "name": "item_id", "in": "path", "required": True, "schema": {"type": "integer"}, }, { "name": "include_details", "in": "query", "required": False, "schema": {"type": "boolean"}, }, ], "responses": {"200": {"description": "OK"}}, } } }, } _REF_SPEC = { "openapi": "3.0.0", "info": {"title": "Ref API", "version": "1.0.0"}, "components": { "schemas": { "Item": { "type": "object", "properties": {"id": {"type": "string"}}, } } }, "paths": { "/items": { "get": { "operationId": "list_items", "summary": "List items", "description": "", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": {"$ref": "#/components/schemas/Item"} } }, } }, } } }, } _MULTI_ENDPOINT_SPEC = { "openapi": "3.0.0", "info": {"title": "Multi API", "version": "1.0.0"}, "paths": { "/users": { "get": { "operationId": "list_users", "summary": "List users", "description": "", "responses": {"200": {"description": "OK"}}, }, "post": { "operationId": "create_user", "summary": "Create user", "description": "", "responses": {"201": {"description": "Created"}}, }, }, "/users/{id}": { "delete": { "operationId": "delete_user", "summary": "Delete user", "description": "", "parameters": [ {"name": "id", "in": "path", "required": True, "schema": {"type": "string"}} ], "responses": {"204": {"description": "Deleted"}}, } }, }, } class TestParseEndpoints: """Tests for parse_endpoints function.""" def test_empty_paths_returns_empty_tuple(self) -> None: """Spec with no paths yields no endpoints.""" from app.openapi.parser import parse_endpoints result = parse_endpoints(_MINIMAL_SPEC) assert result == () def test_parse_get_endpoint(self) -> None: """Parse a GET endpoint with path parameter.""" from app.openapi.parser import parse_endpoints result = parse_endpoints(_GET_SPEC) assert len(result) == 1 ep = result[0] assert ep.path == "/orders/{order_id}" assert ep.method == "GET" assert ep.operation_id == "get_order" assert ep.summary == "Get an order" def test_parse_get_endpoint_parameters(self) -> None: """Path parameters are extracted correctly.""" from app.openapi.parser import parse_endpoints result = parse_endpoints(_GET_SPEC) ep = result[0] assert len(ep.parameters) == 1 param = ep.parameters[0] assert param.name == "order_id" assert param.location == "path" assert param.required is True assert param.schema_type == "string" def test_parse_post_with_request_body(self) -> None: """POST endpoint with request body is extracted.""" from app.openapi.parser import parse_endpoints result = parse_endpoints(_POST_SPEC) assert len(result) == 1 ep = result[0] assert ep.method == "POST" assert ep.request_body_schema is not None assert ep.request_body_schema["type"] == "object" def test_parse_path_and_query_params(self) -> None: """Both path and query parameters are extracted.""" from app.openapi.parser import parse_endpoints result = parse_endpoints(_MULTI_PARAM_SPEC) ep = result[0] locations = {p.location for p in ep.parameters} assert "path" in locations assert "query" in locations def test_autogenerate_operation_id_when_missing(self) -> None: """Auto-generate operation_id when not provided in spec.""" from app.openapi.parser import parse_endpoints spec = { "openapi": "3.0.0", "info": {"title": "Test", "version": "1.0"}, "paths": { "/things/{id}": { "get": { "summary": "Get thing", "description": "", "responses": {"200": {"description": "OK"}}, } } }, } result = parse_endpoints(spec) ep = result[0] assert ep.operation_id != "" assert len(ep.operation_id) > 0 def test_multiple_endpoints_extracted(self) -> None: """Multiple path+method combinations are all extracted.""" from app.openapi.parser import parse_endpoints result = parse_endpoints(_MULTI_ENDPOINT_SPEC) assert len(result) == 3 methods = {ep.method for ep in result} assert "GET" in methods assert "POST" in methods assert "DELETE" in methods def test_ref_in_response_schema_resolved(self) -> None: """$ref in response schema is resolved to the target schema.""" from app.openapi.parser import parse_endpoints result = parse_endpoints(_REF_SPEC) ep = result[0] assert ep.response_schema is not None # Resolved ref should contain the properties assert "properties" in ep.response_schema or "$ref" not in ep.response_schema def test_result_is_tuple(self) -> None: """parse_endpoints returns a tuple (immutable).""" from app.openapi.parser import parse_endpoints result = parse_endpoints(_GET_SPEC) assert isinstance(result, tuple) def test_endpoint_info_is_frozen(self) -> None: """EndpointInfo instances are frozen/immutable.""" from app.openapi.parser import parse_endpoints result = parse_endpoints(_GET_SPEC) ep = result[0] with pytest.raises((AttributeError, TypeError)): ep.method = "POST" # type: ignore[misc]