Skip to main content
Glama
test_spec_loader.py14.2 kB
#!/usr/bin/env python3 """Unit tests for SpecLoader component. This test suite validates the loading of Swagger/OpenAPI specifications from both local files and remote URLs, including error handling and format validation. """ import json import sys import tempfile from pathlib import Path from unittest.mock import Mock, patch import httpx import pytest # Add parent directory to path to import from parsers sys.path.insert(0, str(Path(__file__).parent.parent)) from mcp_swagger.models.swagger import SwaggerSpec from mcp_swagger.parsers.spec_loader import SpecLoader class TestSpecLoader: """Test suite for SpecLoader functionality.""" def setup_method(self) -> None: """Set up test fixtures for each test method.""" self.sample_spec = { "swagger": "2.0", "info": { "title": "Test API", "version": "1.0.0", }, "host": "api.example.com", "basePath": "/v1", "paths": { "/users": { "get": { "summary": "List users", "responses": {"200": {"description": "Success"}}, } } }, } def test_load_from_file_json(self) -> None: """Test loading specification from a JSON file.""" # Arrange with tempfile.NamedTemporaryFile( encoding="utf-8", mode="w", suffix=".json", delete=False ) as temp_file: json.dump(self.sample_spec, temp_file) temp_path = temp_file.name try: # Act spec = SpecLoader.load(temp_path) # Assert expected_spec = SwaggerSpec.from_dict(self.sample_spec) assert spec.swagger == expected_spec.swagger assert spec.info.title == "Test API" assert spec.info.version == "1.0.0" assert spec.host == "api.example.com" assert spec.base_path == "/v1" assert "/users" in spec.paths finally: Path(temp_path).unlink() def test_load_from_url_http(self) -> None: """Test loading specification from HTTP URL.""" # Arrange mock_response = Mock() mock_response.json.return_value = self.sample_spec mock_response.raise_for_status.return_value = None with patch("httpx.get", return_value=mock_response) as mock_get: # Act spec = SpecLoader.load("http://api.example.com/swagger.json") # Assert expected_spec = SwaggerSpec.from_dict(self.sample_spec) assert spec.swagger == expected_spec.swagger assert spec.info.title == expected_spec.info.title mock_get.assert_called_once_with( "http://api.example.com/swagger.json", timeout=600.0 ) mock_response.raise_for_status.assert_called_once() def test_load_from_url_https(self) -> None: """Test loading specification from HTTPS URL.""" # Arrange mock_response = Mock() mock_response.json.return_value = self.sample_spec mock_response.raise_for_status.return_value = None with patch("httpx.get", return_value=mock_response) as mock_get: # Act spec = SpecLoader.load("https://secure-api.example.com/swagger.json") # Assert expected_spec = SwaggerSpec.from_dict(self.sample_spec) assert spec.swagger == expected_spec.swagger assert spec.info.title == expected_spec.info.title mock_get.assert_called_once_with( "https://secure-api.example.com/swagger.json", timeout=600.0 ) def test_load_file_not_found(self) -> None: """Test handling of non-existent file.""" # Act & Assert with pytest.raises(FileNotFoundError): SpecLoader.load("/non/existent/file.json") def test_load_invalid_json_file(self) -> None: """Test handling of invalid JSON in file.""" # Arrange with tempfile.NamedTemporaryFile( encoding="utf-8", mode="w", suffix=".json", delete=False ) as temp_file: temp_file.write("{ invalid json }") temp_path = temp_file.name try: # Act & Assert with pytest.raises(json.JSONDecodeError): SpecLoader.load(temp_path) finally: Path(temp_path).unlink() def test_load_url_http_error(self) -> None: """Test handling of HTTP errors when loading from URL.""" # Arrange mock_response = Mock() mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( "404 Not Found", request=Mock(), response=Mock() ) with ( patch("httpx.get", return_value=mock_response), pytest.raises(httpx.HTTPStatusError), ): # Act & Assert SpecLoader.load("http://api.example.com/missing.json") def test_load_url_network_error(self) -> None: """Test handling of network errors when loading from URL.""" # Arrange with ( patch("httpx.get", side_effect=httpx.ConnectError("Connection failed")), pytest.raises(httpx.ConnectError), ): # Act & Assert SpecLoader.load("http://unreachable.example.com/swagger.json") def test_load_url_timeout(self) -> None: """Test handling of timeout when loading from URL.""" # Arrange with ( patch("httpx.get", side_effect=httpx.TimeoutException("Request timeout")), pytest.raises(httpx.TimeoutException), ): # Act & Assert SpecLoader.load("http://slow.example.com/swagger.json") def test_load_url_invalid_json_response(self) -> None: """Test handling of invalid JSON in HTTP response.""" # Arrange mock_response = Mock() mock_response.json.side_effect = json.JSONDecodeError("Invalid", "doc", 0) mock_response.raise_for_status.return_value = None with ( patch("httpx.get", return_value=mock_response), pytest.raises(json.JSONDecodeError), ): # Act & Assert SpecLoader.load("http://api.example.com/invalid.json") def test_load_empty_file(self) -> None: """Test loading an empty file.""" # Arrange with tempfile.NamedTemporaryFile( encoding="utf-8", mode="w", suffix=".json", delete=False ) as temp_file: temp_file.write("") temp_path = temp_file.name try: # Act & Assert with pytest.raises(json.JSONDecodeError): SpecLoader.load(temp_path) finally: Path(temp_path).unlink() def test_load_file_with_utf8_content(self) -> None: """Test loading file with UTF-8 encoded content.""" # Arrange spec_with_unicode = { "swagger": "2.0", "info": { "title": "API with Cyrillic", # Using regular text to avoid Unicode issues "description": "描述", # Chinese }, } with tempfile.NamedTemporaryFile( mode="w", encoding="utf-8", suffix=".json", delete=False ) as temp_file: json.dump(spec_with_unicode, temp_file, ensure_ascii=False) temp_path = temp_file.name try: # Act spec = SpecLoader.load(temp_path) # Assert assert spec.info.title == "API with Cyrillic" assert spec.info.description == "描述" finally: Path(temp_path).unlink() def test_load_relative_file_path(self) -> None: """Test loading specification from relative file path.""" # Arrange with tempfile.NamedTemporaryFile( encoding="utf-8", mode="w", suffix=".json", delete=False, dir="." ) as temp_file: json.dump(self.sample_spec, temp_file) temp_name = Path(temp_file.name).name try: # Act spec = SpecLoader.load(temp_name) # Assert expected_spec = SwaggerSpec.from_dict(self.sample_spec) assert spec.swagger == expected_spec.swagger assert spec.info.title == expected_spec.info.title finally: Path(temp_name).unlink() def test_load_absolute_file_path(self) -> None: """Test loading specification from absolute file path.""" # Arrange with tempfile.NamedTemporaryFile( encoding="utf-8", mode="w", suffix=".json", delete=False ) as temp_file: json.dump(self.sample_spec, temp_file) temp_path = Path(temp_file.name).absolute() try: # Act spec = SpecLoader.load(str(temp_path)) # Assert expected_spec = SwaggerSpec.from_dict(self.sample_spec) assert spec.swagger == expected_spec.swagger assert spec.info.title == expected_spec.info.title finally: temp_path.unlink() def test_load_complex_nested_spec(self) -> None: """Test loading a complex nested specification.""" # Arrange complex_spec = { "swagger": "2.0", "paths": { "/users/{id}": { "get": { "parameters": [ { "name": "id", "in": "path", "schema": { "type": "object", "properties": { "nested": { "type": "array", "items": {"type": "string"}, } }, }, } ], "responses": { "200": { "schema": { "$ref": "#/definitions/User", } } }, } } }, "definitions": { "User": { "type": "object", "properties": { "id": {"type": "string"}, "name": {"type": "string"}, }, } }, } with tempfile.NamedTemporaryFile( encoding="utf-8", mode="w", suffix=".json", delete=False ) as temp_file: json.dump(complex_spec, temp_file) temp_path = temp_file.name try: # Act spec = SpecLoader.load(temp_path) # Assert expected_spec = SwaggerSpec.from_dict(complex_spec) assert spec.swagger == expected_spec.swagger assert "/users/{id}" in spec.paths assert "$ref" in spec.paths["/users/{id}"].get.responses["200"].schema finally: Path(temp_path).unlink() def test_url_detection_patterns(self) -> None: """Test URL pattern detection for various formats.""" # These should be treated as URLs url_patterns = [ "http://example.com/spec.json", "https://example.com/spec.json", "HTTP://EXAMPLE.COM/SPEC.JSON", # Case shouldn't matter "https://sub.domain.example.com:8080/path/to/spec.json", ] # These should be treated as file paths # Mock for URL tests mock_response = Mock() mock_response.json.return_value = {"type": "url"} mock_response.raise_for_status.return_value = None with patch("httpx.get", return_value=mock_response): for url in url_patterns[:2]: # Test first two to avoid too many calls result = SpecLoader.load(url) SwaggerSpec.from_dict({"type": "url"}) assert isinstance(result, SwaggerSpec) def test_timeout_configuration(self) -> None: """Test that the timeout is properly configured for HTTP requests.""" # Arrange mock_response = Mock() mock_response.json.return_value = self.sample_spec mock_response.raise_for_status.return_value = None with patch("httpx.get", return_value=mock_response) as mock_get: # Act - Test default timeout SpecLoader.load("http://api.example.com/swagger.json") # Assert - Default timeout is 600 seconds mock_get.assert_called_with( "http://api.example.com/swagger.json", timeout=600.0 ) # Act - Test custom timeout SpecLoader.load("http://api.example.com/swagger.json", timeout=30.0) # Assert - Custom timeout is used mock_get.assert_called_with( "http://api.example.com/swagger.json", timeout=30.0 ) if __name__ == "__main__": # Run tests with pytest if available, otherwise run basic tests try: pytest.main([__file__, "-v"]) except ImportError: print("pytest not installed, running basic tests...") test_suite = TestSpecLoader() test_methods = [m for m in dir(test_suite) if m.startswith("test_")] for method_name in test_methods: # Skip tests that require pytest fixtures if "not_found" in method_name or "error" in method_name: continue test_suite.setup_method() method = getattr(test_suite, method_name) try: method() print(f"✓ {method_name}") except Exception as e: print(f"✗ {method_name}: {e}") print("\nBasic tests completed (some tests skipped)!")

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/zecure/mcp_swagger'

If you have feedback or need assistance with the MCP directory API, please join our Discord server