diff --git a/tests/test_cli.py b/tests/test_cli.py index c339b8d..e773f03 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,32 +1,565 @@ +import json +import os +import tempfile +from pathlib import Path +from unittest.mock import MagicMock, patch + import pytest from click.testing import CliRunner -from src.cli import main + +from src.cli import main, serve, generate, validate, search -@pytest.fixture -def runner(): - return CliRunner() +class TestCLIMain: + """Test cases for the main CLI entry point.""" + + def test_main_help(self): + """Test that main command shows help.""" + runner = CliRunner() + result = runner.invoke(main, ["--help"]) + + assert result.exit_code == 0 + assert "LocalAPI Docs" in result.output + assert "serve" in result.output + assert "generate" in result.output + assert "validate" in result.output + assert "search" in result.output + + def test_main_verbose_flag(self): + """Test verbose flag parsing.""" + runner = CliRunner() + result = runner.invoke(main, ["--help"]) + + assert result.exit_code == 0 -def test_cli_help(runner): - result = runner.invoke(main, ['--help']) - assert result.exit_code == 0 - assert 'LocalAPI Docs' in result.output +class TestValidateCommand: + """Test cases for the validate command.""" + + def test_validate_valid_spec(self): + """Test validating a valid 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() + + runner = CliRunner() + result = runner.invoke(validate, [f.name]) + + assert result.exit_code == 0 + assert "valid" in result.output.lower() + + os.unlink(f.name) + + def test_validate_invalid_spec(self): + """Test validating an invalid OpenAPI spec.""" + spec = { + "openapi": "3.0.0", + "info": { + "title": "Test API" + } + } + + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + json.dump(spec, f) + f.flush() + + runner = CliRunner() + result = runner.invoke(validate, [f.name]) + + assert result.exit_code == 1 + assert "invalid" in result.output.lower() or "error" in result.output.lower() + + os.unlink(f.name) + + def test_validate_missing_file(self): + """Test validating a non-existent file.""" + runner = CliRunner() + result = runner.invoke(validate, ["nonexistent.json"]) + + assert result.exit_code != 0 + assert "not found" in result.output.lower() or "error" in result.output.lower() + + def test_validate_yaml_spec(self): + """Test validating a YAML spec file.""" + 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() + + runner = CliRunner() + result = runner.invoke(validate, [f.name]) + + assert result.exit_code == 0 + + os.unlink(f.name) + + def test_validate_json_output(self): + """Test validate command with JSON output.""" + 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() + + runner = CliRunner() + result = runner.invoke(validate, [f.name, "--json"]) + + assert result.exit_code == 0 + assert "valid" in result.output.lower() + + os.unlink(f.name) -def test_cli_serve(runner, tmp_path, sample_spec): - result = runner.invoke(main, ['serve', str(sample_spec)]) - assert result.exit_code == 0 +class TestGenerateCommand: + """Test cases for the generate command.""" + + def test_generate_html(self): + """Test generating HTML documentation.""" + spec = { + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "1.0.0", + "description": "A test API" + }, + "paths": { + "/users": { + "get": { + "summary": "List users", + "description": "Get a list of users", + "responses": { + "200": { + "description": "Success" + } + } + } + } + } + } + + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + json.dump(spec, f) + f.flush() + + runner = CliRunner() + with runner.isolated_filesystem(): + result = runner.invoke(generate, [f.name, "--format", "html"]) + + assert result.exit_code == 0 + assert "api-docs.html" in result.output.lower() or "generated" in result.output.lower() + + os.unlink(f.name) + + def test_generate_markdown(self): + """Test generating Markdown documentation.""" + 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() + + runner = CliRunner() + with runner.isolated_filesystem(): + result = runner.invoke(generate, [f.name, "--format", "markdown"]) + + assert result.exit_code == 0 + + os.unlink(f.name) + + def test_generate_json(self): + """Test generating JSON documentation.""" + 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() + + runner = CliRunner() + with runner.isolated_filesystem(): + result = runner.invoke(generate, [f.name, "--format", "json"]) + + assert result.exit_code == 0 + + os.unlink(f.name) + + def test_generate_with_output_path(self): + """Test generating with custom output path.""" + 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() + + runner = CliRunner() + with runner.isolated_filesystem(): + result = runner.invoke(generate, [f.name, "--format", "html", "--output", "custom.html"]) + + assert result.exit_code == 0 + assert os.path.exists("custom.html") + + os.unlink(f.name) + + def test_generate_with_all_formats(self): + """Test generating all output formats.""" + 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() + + runner = CliRunner() + with runner.isolated_filesystem(): + result = runner.invoke(generate, [f.name, "--format", "all"]) + + assert result.exit_code == 0 + + os.unlink(f.name) -def test_cli_generate(runner, tmp_path, sample_spec): - output = tmp_path / "output.html" - result = runner.invoke(main, ['generate', str(sample_spec), '-o', str(output)]) - assert result.exit_code == 0 - assert output.exists() +class TestSearchCommand: + """Test cases for the search command.""" + + def test_search_basic(self): + """Test basic search functionality.""" + spec = { + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "1.0.0" + }, + "paths": { + "/users": { + "get": { + "summary": "List all users", + "description": "Get a list of all users in the system", + "responses": { + "200": {"description": "Success"} + } + } + } + } + } + + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + json.dump(spec, f) + f.flush() + + runner = CliRunner() + result = runner.invoke(search, [f.name, "users"]) + + assert result.exit_code == 0 + + os.unlink(f.name) + + def test_search_no_results(self): + """Test search with no results.""" + spec = { + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "1.0.0" + }, + "paths": { + "/users": { + "get": { + "summary": "List users", + "responses": {"200": {"description": "Success"}} + } + } + } + } + + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + json.dump(spec, f) + f.flush() + + runner = CliRunner() + result = runner.invoke(search, [f.name, "nonexistentendpoint12345"]) + + assert result.exit_code == 0 + assert "no results" in result.output.lower() + + os.unlink(f.name) + + def test_search_json_output(self): + """Test search with JSON output.""" + spec = { + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "1.0.0" + }, + "paths": { + "/users": { + "get": { + "summary": "List users", + "responses": {"200": {"description": "Success"}} + } + } + } + } + + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + json.dump(spec, f) + f.flush() + + runner = CliRunner() + result = runner.invoke(search, [f.name, "users", "--json"]) + + assert result.exit_code == 0 + assert "query" in result.output.lower() or "results" in result.output.lower() + + os.unlink(f.name) + + def test_search_with_limit(self): + """Test search with result limit.""" + spec = { + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "1.0.0" + }, + "paths": { + "users": { + "get": { + "summary": "List users", + "responses": {"200": {"description": "Success"}} + } + } + } + } + + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + json.dump(spec, f) + f.flush() + + runner = CliRunner() + result = runner.invoke(search, [f.name, "users", "--limit", "5"]) + + assert result.exit_code == 0 + + os.unlink(f.name) -def test_cli_validate(runner, sample_spec): - result = runner.invoke(main, ['validate', str(sample_spec)]) - assert result.exit_code == 0 - assert 'valid' in result.output.lower() +class TestServeCommand: + """Test cases for the serve command.""" + + def test_serve_invalid_spec(self): + """Test serve command with invalid spec.""" + spec = {"invalid": "spec"} + + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + json.dump(spec, f) + f.flush() + + runner = CliRunner() + result = runner.invoke(serve, [f.name, "--port", "9999", "--no-browser"]) + + assert result.exit_code == 1 + assert "error" in result.output.lower() or "invalid" in result.output.lower() + + os.unlink(f.name) + + def test_serve_missing_file(self): + """Test serve command with missing file.""" + runner = CliRunner() + result = runner.invoke(serve, ["nonexistent.json"]) + + assert result.exit_code == 1 + + def test_serve_with_custom_host_port(self): + """Test serve command with custom host and port.""" + 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() + + runner = CliRunner() + with runner.isolated_filesystem(): + result = runner.invoke(serve, [f.name, "--host", "0.0.0.0", "--port", "9998", "--no-browser"]) + + assert result.exit_code != 0 or "9998" in result.output or "running" in result.output.lower() + + os.unlink(f.name) + + +class TestCLIIntegration: + """Integration tests for CLI commands.""" + + def test_full_parse_and_generate(self): + """Test complete parse and generate workflow.""" + spec = { + "openapi": "3.0.0", + "info": { + "title": "Pet Store API", + "version": "1.0.0", + "description": "A sample pet store API" + }, + "paths": { + "/pets": { + "get": { + "summary": "List all pets", + "description": "Returns a list of all pets", + "tags": ["pets"], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": {"type": "integer"}, + "description": "Maximum number of pets to return" + } + ], + "responses": { + "200": { + "description": "A list of pets", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": {"type": "object"} + } + } + } + } + } + }, + "post": { + "summary": "Create a pet", + "tags": ["pets"], + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": {"type": "object"} + } + } + }, + "responses": { + "201": {"description": "Pet created"} + } + } + }, + "/pets/{petId}": { + "get": { + "summary": "Get a pet", + "tags": ["pets"], + "parameters": [ + { + "name": "petId", + "in": "path", + "required": True, + "schema": {"type": "string"} + } + ], + "responses": { + "200": {"description": "Pet found"}, + "404": {"description": "Pet not found"} + } + }, + "delete": { + "summary": "Delete a pet", + "tags": ["pets"], + "parameters": [ + { + "name": "petId", + "in": "path", + "required": True, + "schema": {"type": "string"} + } + ], + "responses": { + "204": {"description": "Pet deleted"} + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "name": {"type": "string"}, + "type": {"type": "string"} + } + } + } + } + } + + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + json.dump(spec, f) + f.flush() + + runner = CliRunner() + + validate_result = runner.invoke(validate, [f.name]) + assert validate_result.exit_code == 0 + + with runner.isolated_filesystem(): + generate_result = runner.invoke(generate, [f.name, "--format", "html"]) + assert generate_result.exit_code == 0 + + search_result = runner.invoke(search, [f.name, "pet"]) + assert search_result.exit_code == 0 + + os.unlink(f.name)