"""Tests for OpenAPI output schema extraction functionality.""" from fastmcp.utilities.openapi import ( ResponseInfo, _adjust_union_types, extract_output_schema_from_responses, ) class TestExtractOutputSchema: """Test the extract_output_schema_from_responses function.""" def test_extract_object_schema(self): """Test extracting object output schema (no wrapping needed).""" responses = { "200": ResponseInfo( description="Success", content_schema={ "application/json": { "type": "object", "properties": { "id": {"type": "integer"}, "name": {"type": "string"}, }, "required": ["id", "name"], } }, ) } result = extract_output_schema_from_responses(responses) assert result == { "type": "object", "properties": {"id": {"type": "integer"}, "name": {"type": "string"}}, "required": ["id", "name"], } assert result is not None and "x-fastmcp-wrap-result" not in result def test_extract_array_schema_with_wrapping(self): """Test extracting array output schema (should be wrapped).""" responses = { "200": ResponseInfo( description="Success", content_schema={ "application/json": { "type": "array", "items": { "type": "object", "properties": { "id": {"type": "integer"}, "name": {"type": "string"}, }, }, } }, ) } result = extract_output_schema_from_responses(responses) assert result == { "type": "object", "properties": { "result": { "type": "array", "items": { "type": "object", "properties": { "id": {"type": "integer"}, "name": {"type": "string"}, }, }, } }, "required": ["result"], "x-fastmcp-wrap-result": True, } def test_extract_primitive_schema_with_wrapping(self): """Test extracting primitive output schema (should be wrapped).""" responses = { "201": ResponseInfo( description="Created", content_schema={ "application/json": { "type": "string", "description": "ID of created resource", } }, ) } result = extract_output_schema_from_responses(responses) assert result == { "type": "object", "properties": { "result": {"type": "string", "description": "ID of created resource"} }, "required": ["result"], "x-fastmcp-wrap-result": True, } def test_priority_of_success_codes(self): """Test that 200 takes priority over other success codes.""" responses = { "201": ResponseInfo( description="Created", content_schema={"application/json": {"type": "string"}}, ), "200": ResponseInfo( description="Success", content_schema={ "application/json": { "type": "object", "properties": {"id": {"type": "integer"}}, } }, ), } result = extract_output_schema_from_responses(responses) # Should use the 200 response (object), not 201 (string) assert result is not None and result["type"] == "object" assert result is not None and "x-fastmcp-wrap-result" not in result def test_prefer_json_content_type(self): """Test that application/json is preferred over other content types.""" responses = { "200": ResponseInfo( description="Success", content_schema={ "text/plain": {"type": "string"}, "application/json": { "type": "object", "properties": {"id": {"type": "integer"}}, }, }, ) } result = extract_output_schema_from_responses(responses) # Should use the application/json schema (object), not text/plain (string) assert result is not None and result["type"] == "object" assert result is not None and "x-fastmcp-wrap-result" not in result def test_no_responses(self): """Test that None is returned when no responses are provided.""" result = extract_output_schema_from_responses({}) assert result is None def test_no_success_responses(self): """Test that None is returned when no success responses are found.""" responses = { "400": ResponseInfo( description="Bad Request", content_schema={ "application/json": { "type": "object", "properties": {"error": {"type": "string"}}, } }, ) } result = extract_output_schema_from_responses(responses) assert result is None def test_no_content_schema(self): """Test that None is returned when response has no content schema.""" responses = {"204": ResponseInfo(description="No Content")} result = extract_output_schema_from_responses(responses) assert result is None def test_schema_definitions_included(self): """Test that schema definitions are properly included in output schema.""" responses = { "200": ResponseInfo( description="Success", content_schema={ "application/json": { "type": "object", "properties": {"user": {"$ref": "#/$defs/User"}}, } }, ) } schema_definitions = { "User": { "type": "object", "properties": {"id": {"type": "integer"}, "name": {"type": "string"}}, "required": ["id", "name"], } } result = extract_output_schema_from_responses(responses, schema_definitions) assert result is not None assert "$defs" in result assert "User" in result["$defs"] assert result["$defs"]["User"] == schema_definitions["User"] def test_wrapped_schema_with_definitions(self): """Test that wrapped schemas properly include schema definitions.""" responses = { "200": ResponseInfo( description="Success", content_schema={ "application/json": { "type": "array", "items": {"$ref": "#/$defs/User"}, } }, ) } schema_definitions = { "User": { "type": "object", "properties": {"id": {"type": "integer"}, "name": {"type": "string"}}, "required": ["id", "name"], } } result = extract_output_schema_from_responses(responses, schema_definitions) assert result is not None assert result["x-fastmcp-wrap-result"] is True assert "$defs" in result assert "User" in result["$defs"] assert result["properties"]["result"]["type"] == "array" assert result["properties"]["result"]["items"]["$ref"] == "#/$defs/User" def test_adjust_union_types(): """Test that oneOf is replaced with anyOf in schemas.""" schema = {"oneOf": [{"type": "string"}, {"type": "number"}]} result = _adjust_union_types(schema) assert isinstance(result, dict) assert "anyOf" in result assert "oneOf" not in result assert len(result["anyOf"]) == 2 assert result["anyOf"][0] == {"type": "string"} assert result["anyOf"][1] == {"type": "number"} def test_extract_output_schema_converts_oneOf_to_anyOf(): """Test that extracted schema converts oneOf to anyOf.""" responses = { "200": ResponseInfo( description="Success", content_schema={ "application/json": { "type": "object", "properties": { "result": { "oneOf": [ {"$ref": "#/$defs/TypeA"}, {"$ref": "#/$defs/TypeB"}, ] } }, } }, ) } schema_definitions = {"TypeA": {"type": "string"}, "TypeB": {"type": "number"}} result = extract_output_schema_from_responses(responses, schema_definitions) assert result is not None assert "oneOf" not in str(result) # Ensure no oneOf remains assert "anyOf" in str(result) # Ensure anyOf is present