""" Tests for Contract Tool (OpenAPI/JSON Schema) """ import pytest import os import sys import yaml sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from services.router.tool_manager import ToolManager, ToolResult class TestContractToolLinting: """Test OpenAPI linting functionality""" @pytest.mark.asyncio async def test_lint_missing_operation_id(self): """Test detection of missing operationId""" tool_mgr = ToolManager({}) spec = """ openapi: "3.0.0" info: title: Test API version: "1.0.0" paths: /users: get: responses: "200": description: Success """ result = await tool_mgr._contract_tool({ "action": "lint_openapi", "inputs": { "format": "openapi_yaml", "base": {"source": "text", "value": spec} }, "options": {} }) assert result.success is True assert len(result.result["lint"]) > 0 assert any("operationId" in i["message"] for i in result.result["lint"]) @pytest.mark.asyncio async def test_lint_good_spec(self): """Test that valid spec passes lint""" tool_mgr = ToolManager({}) spec = """ openapi: "3.0.0" info: title: Test API version: "1.0.0" paths: /users: get: operationId: getUsers responses: "200": description: Success content: application/json: schema: type: array """ result = await tool_mgr._contract_tool({ "action": "lint_openapi", "inputs": { "format": "openapi_yaml", "base": {"source": "text", "value": spec} }, "options": {} }) assert result.success is True errors = [i for i in result.result["lint"] if i["severity"] == "error"] assert len(errors) == 0 @pytest.mark.asyncio async def test_lint_no_spec(self): """Test error when no spec provided""" tool_mgr = ToolManager({}) result = await tool_mgr._contract_tool({ "action": "lint_openapi", "inputs": {}, "options": {} }) assert result.success is False class TestContractToolDiff: """Test OpenAPI diff/breaking changes detection""" @pytest.mark.asyncio async def test_endpoint_removed_breaking(self): """Test detection of removed endpoint""" tool_mgr = ToolManager({}) base_spec = """ openapi: "3.0.0" info: title: Test API version: "1.0.0" paths: /users: get: operationId: getUsers responses: "200": description: Success """ head_spec = """ openapi: "3.0.0" info: title: Test API version: "1.0.0" paths: {} """ result = await tool_mgr._contract_tool({ "action": "diff_openapi", "inputs": { "format": "openapi_yaml", "base": {"source": "text", "value": base_spec}, "head": {"source": "text", "value": head_spec} }, "options": {} }) assert result.success is True assert len(result.result["breaking"]) > 0 assert any("endpoint_removed" in i["type"] for i in result.result["breaking"]) @pytest.mark.asyncio async def test_required_field_added_breaking(self): """Test detection of required field added""" tool_mgr = ToolManager({}) base_spec = """ openapi: "3.0.0" info: title: Test API version: "1.0.0" paths: {} components: schemas: User: type: object properties: name: type: string """ head_spec = """ openapi: "3.0.0" info: title: Test API version: "1.0.0" paths: {} components: schemas: User: type: object required: - email properties: name: type: string email: type: string """ result = await tool_mgr._contract_tool({ "action": "diff_openapi", "inputs": { "format": "openapi_yaml", "base": {"source": "text", "value": base_spec}, "head": {"source": "text", "value": head_spec} }, "options": {} }) assert result.success is True assert len(result.result["breaking"]) > 0 assert any("required_added" in i["type"] for i in result.result["breaking"]) @pytest.mark.asyncio async def test_optional_field_added_nonbreaking(self): """Test that optional field addition is non-breaking""" tool_mgr = ToolManager({}) base_spec = """ openapi: "3.0.0" info: title: Test API version: "1.0.0" paths: {} components: schemas: User: type: object properties: name: type: string """ head_spec = """ openapi: "3.0.0" info: title: Test API version: "1.0.0" paths: {} components: schemas: User: type: object properties: name: type: string email: type: string """ result = await tool_mgr._contract_tool({ "action": "diff_openapi", "inputs": { "format": "openapi_yaml", "base": {"source": "text", "value": base_spec}, "head": {"source": "text", "value": head_spec} }, "options": {} }) assert result.success is True # Should have non-breaking, no breaking breaking = result.result.get("breaking", []) assert len(breaking) == 0 @pytest.mark.asyncio async def test_enum_narrowed_breaking(self): """Test detection of enum narrowing""" tool_mgr = ToolManager({}) base_spec = """ openapi: "3.0.0" info: title: Test API version: "1.0.0" paths: {} components: schemas: Status: type: string enum: - pending - active - completed """ head_spec = """ openapi: "3.0.0" info: title: Test API version: "1.0.0" paths: {} components: schemas: Status: type: string enum: - pending - active """ result = await tool_mgr._contract_tool({ "action": "diff_openapi", "inputs": { "format": "openapi_yaml", "base": {"source": "text", "value": base_spec}, "head": {"source": "text", "value": head_spec} }, "options": {} }) assert result.success is True assert len(result.result["breaking"]) > 0 @pytest.mark.asyncio async def test_fail_on_breaking(self): """Test fail_on_breaking option""" tool_mgr = ToolManager({}) base_spec = """ openapi: "3.0.0" info: title: Test API version: "1.0.0" paths: /users: get: operationId: getUsers responses: "200": description: Success """ head_spec = """ openapi: "3.0.0" info: title: Test API version: "1.0.0" paths: {} """ result = await tool_mgr._contract_tool({ "action": "diff_openapi", "inputs": { "format": "openapi_yaml", "base": {"source": "text", "value": base_spec}, "head": {"source": "text", "value": head_spec} }, "options": { "fail_on_breaking": True } }) assert result.success is False assert result.error is not None class TestContractToolLimits: """Test limit enforcement""" @pytest.mark.asyncio async def test_max_chars_limit(self): """Test that max_chars limit is enforced""" tool_mgr = ToolManager({}) large_spec = "x" * 900000 # 900KB result = await tool_mgr._contract_tool({ "action": "lint_openapi", "inputs": { "format": "openapi_yaml", "base": {"source": "text", "value": large_spec} }, "options": { "max_chars": 800000 } }) assert result.success is False assert "too large" in result.error.lower() @pytest.mark.asyncio async def test_missing_base_or_head(self): """Test error when base or head missing for diff""" tool_mgr = ToolManager({}) spec = """ openapi: "3.0.0" info: title: Test API version: "1.0.0" paths: {} """ result = await tool_mgr._contract_tool({ "action": "diff_openapi", "inputs": { "format": "openapi_yaml", "base": {"source": "text", "value": spec} }, "options": {} }) assert result.success is False class TestContractToolStub: """Test client stub generation""" @pytest.mark.asyncio async def test_generate_python_stub(self): """Test Python client stub generation""" tool_mgr = ToolManager({}) spec = """ openapi: "3.0.0" info: title: User API version: "1.0.0" paths: /users: get: operationId: getUsers summary: Get all users responses: "200": description: Success post: operationId: createUser summary: Create user parameters: - name: data in: body required: true responses: "201": description: Created """ result = await tool_mgr._contract_tool({ "action": "generate_client_stub", "inputs": { "format": "openapi_yaml", "base": {"source": "text", "value": spec} }, "options": {} }) assert result.success is True assert "client_stub" in result.result assert "def getUsers" in result.result["client_stub"] assert "def createUser" in result.result["client_stub"] if __name__ == "__main__": pytest.main([__file__, "-v"])