diff --git a/tests/test_parser.py b/tests/test_parser.py index 845b540..b0ff8cc 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,74 +1,482 @@ +import json +import tempfile +from pathlib import Path + import pytest -from src.core.parser import parse_openapi_spec, load_spec_file, ParseError + +from src.core.parser import OpenAPIParser, ParseError, SpecValidationError +from src.core.models import OpenAPISpec -@pytest.fixture -def sample_spec(tmp_path): - spec = { - "openapi": "3.0.0", - "info": { - "title": "Test API", - "version": "1.0.0" - }, - "paths": { - "/users": { - "get": { - "summary": "Get users", - "description": "Retrieve a list of users", - "tags": ["users"], - "responses": { - "200": {"description": "Successful response"} +class TestOpenAPIParser: + """Test cases for OpenAPIParser.""" + + def test_parse_valid_json_spec(self): + """Test parsing a valid JSON OpenAPI spec.""" + spec = { + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "1.0.0" + }, + "paths": {} + } + + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + json.dump(spec, f) + f.flush() + + parser = OpenAPIParser(f.name) + result = parser.parse() + + assert isinstance(result, OpenAPISpec) + assert result.openapi == "3.0.0" + assert result.info.title == "Test API" + assert result.info.version == "1.0.0" + + Path(f.name).unlink() + + def test_parse_valid_yaml_spec(self): + """Test parsing a valid YAML OpenAPI spec.""" + spec_content = """ +openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: {} +""" + + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: + f.write(spec_content) + f.flush() + + parser = OpenAPIParser(f.name) + result = parser.parse() + + assert isinstance(result, OpenAPISpec) + assert result.openapi == "3.0.0" + assert result.info.title == "Test API" + + Path(f.name).unlink() + + def test_parse_missing_file(self): + """Test parsing a non-existent file.""" + parser = OpenAPIParser("nonexistent.json") + + with pytest.raises(ParseError) as exc_info: + parser.parse() + + assert "not found" in str(exc_info.value).lower() + + def test_parse_invalid_json(self): + """Test parsing invalid JSON file.""" + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + f.write("{ invalid json }") + f.flush() + + parser = OpenAPIParser(f.name) + + with pytest.raises(ParseError): + parser.parse() + + Path(f.name).unlink() + + def test_parse_invalid_yaml(self): + """Test parsing invalid YAML file.""" + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: + f.write(" - invalid: yaml: content:") + f.flush() + + parser = OpenAPIParser(f.name) + + with pytest.raises(ParseError): + parser.parse() + + Path(f.name).unlink() + + def test_parse_unsupported_format(self): + """Test parsing unsupported file format.""" + with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f: + f.write("test content") + f.flush() + + parser = OpenAPIParser(f.name) + + with pytest.raises(ParseError) as exc_info: + parser.parse() + + assert "unsupported" in str(exc_info.value).lower() + + Path(f.name).unlink() + + def test_parse_spec_with_endpoints(self): + """Test parsing spec with endpoints.""" + spec = { + "openapi": "3.0.0", + "info": { + "title": "Pet Store API", + "version": "1.0.0" + }, + "paths": { + "/pets": { + "get": { + "summary": "List pets", + "description": "Get all pets", + "responses": { + "200": {"description": "Success"} + } + }, + "post": { + "summary": "Create pet", + "responses": { + "201": {"description": "Created"} + } + } + }, + "/pets/{petId}": { + "get": { + "summary": "Get pet", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": True, + "schema": {"type": "string"} + } + ], + "responses": { + "200": {"description": "Success"}, + "404": {"description": "Not found"} + } } } } } - } - path = tmp_path / "test.json" - path.write_text(__import__('json').dumps(spec)) - return path + + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + json.dump(spec, f) + f.flush() + + parser = OpenAPIParser(f.name) + result = parser.parse() + + endpoints = result.get_endpoints() + assert len(endpoints) == 3 + + endpoint_paths = [e.path for e in endpoints] + assert "/pets" in endpoint_paths + assert "/pets/{petId}" in endpoint_paths + + endpoint_methods = [e.method for e in endpoints] + assert "GET" in endpoint_methods + assert "POST" in endpoint_methods + + Path(f.name).unlink() + + def test_parse_spec_with_schemas(self): + """Test parsing spec with schema definitions.""" + spec = { + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "1.0.0" + }, + "paths": {}, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "name": {"type": "string"} + } + }, + "User": { + "type": "object", + "properties": { + "id": {"type": "integer"}, + "username": {"type": "string"}, + "email": {"type": "string", "format": "email"} + } + } + } + } + } + + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + json.dump(spec, f) + f.flush() + + parser = OpenAPIParser(f.name) + result = parser.parse() + + schemas = result.get_schemas() + assert "Pet" in schemas + assert "User" in schemas + + Path(f.name).unlink() + + def test_parse_spec_with_tags(self): + """Test parsing spec with tag definitions.""" + spec = { + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "1.0.0" + }, + "paths": {}, + "tags": [ + {"name": "pets", "description": "Pet operations"}, + {"name": "users", "description": "User operations"} + ] + } + + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + json.dump(spec, f) + f.flush() + + parser = OpenAPIParser(f.name) + result = parser.parse() + + tags = result.get_tags() + assert len(tags) == 2 + tag_names = [t.name for t in tags] + assert "pets" in tag_names + assert "users" in tag_names + + Path(f.name).unlink() + + def test_parse_spec_with_parameters(self): + """Test parsing spec with endpoint parameters.""" + spec = { + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "1.0.0" + }, + "paths": { + "/items": { + "get": { + "summary": "List items", + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": {"type": "integer"}, + "description": "Max items to return" + }, + { + "name": "offset", + "in": "query", + "schema": {"type": "integer"}, + "description": "Items to skip" + } + ], + "responses": {"200": {"description": "Success"}} + } + } + } + } + + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + json.dump(spec, f) + f.flush() + + parser = OpenAPIParser(f.name) + result = parser.parse() + + endpoints = result.get_endpoints() + assert len(endpoints) == 1 + + params = endpoints[0].parameters or [] + assert len(params) == 2 + param_names = [p.name for p in params] + assert "limit" in param_names + assert "offset" in param_names + + Path(f.name).unlink() -def test_parse_valid_spec(sample_spec): - result = parse_openapi_spec(str(sample_spec)) - assert result['valid'] == True - assert result['title'] == 'Test API' - assert result['version_num'] == '1.0.0' +class TestSpecValidation: + """Test cases for OpenAPI spec validation.""" + + def test_validate_valid_spec(self): + """Test validating a valid spec.""" + spec = { + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "1.0.0" + }, + "paths": {} + } + + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + json.dump(spec, f) + f.flush() + + parser = OpenAPIParser(f.name) + is_valid, errors = parser.validate() + + assert is_valid is True + assert len(errors) == 0 + + Path(f.name).unlink() + + def test_validate_spec_missing_info(self): + """Test validating spec without info section.""" + spec = { + "openapi": "3.0.0", + "paths": {} + } + + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + json.dump(spec, f) + f.flush() + + parser = OpenAPIParser(f.name) + is_valid, errors = parser.validate() + + assert is_valid is False + assert len(errors) > 0 + + Path(f.name).unlink() + + def test_validate_spec_missing_paths(self): + """Test validating spec without paths section.""" + spec = { + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "1.0.0" + } + } + + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + json.dump(spec, f) + f.flush() + + parser = OpenAPIParser(f.name) + is_valid, errors = parser.validate() + + assert is_valid is False + + Path(f.name).unlink() -def test_parse_invalid_spec(tmp_path): - invalid = tmp_path / "invalid.json" - invalid.write_text('{"invalid": "spec"}') - result = parse_openapi_spec(str(invalid)) - assert result['valid'] == False - assert 'errors' in result +class TestParseWithExamples: + """Test cases for parsing with generated examples.""" + + def test_parse_with_examples(self): + """Test parsing spec and generating examples.""" + spec = { + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "1.0.0" + }, + "paths": { + "/users": { + "get": { + "summary": "List users", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": {"type": "integer"}, + "name": {"type": "string"} + } + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "User": { + "type": "object", + "properties": { + "id": {"type": "integer"}, + "name": {"type": "string", "format": "email"} + } + } + } + } + } + + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + json.dump(spec, f) + f.flush() + + parser = OpenAPIParser(f.name) + result = parser.parse_with_examples() + + assert "spec" in result + assert "endpoints" in result + assert "schemas" in result + + assert len(result["endpoints"]) == 1 + assert len(result["schemas"]) == 1 + + Path(f.name).unlink() -def test_load_json_spec(sample_spec): - spec = load_spec_file(str(sample_spec)) - assert spec['info']['title'] == 'Test API' +class TestConvenienceFunctions: + """Test cases for convenience functions.""" + def test_parse_spec_file(self): + """Test parse_spec_file convenience function.""" + from src.core.parser import parse_spec_file -def test_load_yaml_spec(tmp_path): - content = ''' -openapi: "3.0.0" -info: - title: Test API - version: "1.0.0" -paths: {} -''' - path = tmp_path / "test.yaml" - path.write_text(content) - spec = load_spec_file(str(path)) - assert spec['info']['title'] == 'Test API' + spec = { + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "1.0.0" + }, + "paths": {} + } + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + json.dump(spec, f) + f.flush() -def test_parse_error_missing_file(): - with pytest.raises(ParseError): - load_spec_file('/nonexistent/path.json') + result = parse_spec_file(f.name) + assert isinstance(result, OpenAPISpec) + assert result.info.title == "Test API" -def test_parse_error_unsupported_format(tmp_path): - path = tmp_path / "test.txt" - path.write_text('some content') - with pytest.raises(ParseError): - load_spec_file(str(path)) + Path(f.name).unlink() + + def test_validate_spec_file(self): + """Test validate_spec_file convenience function.""" + from src.core.parser import validate_spec_file + + spec = { + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "1.0.0" + }, + "paths": {} + } + + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + json.dump(spec, f) + f.flush() + + is_valid, errors = validate_spec_file(f.name) + + assert is_valid is True + + Path(f.name).unlink()