diff --git a/api_testgen/cli/main.py b/api_testgen/cli/main.py new file mode 100644 index 0000000..d228999 --- /dev/null +++ b/api_testgen/cli/main.py @@ -0,0 +1,277 @@ +"""CLI interface for API TestGen.""" + +from pathlib import Path +from typing import Optional + +import click +import yaml + +from ..core import SpecParser, AuthConfig +from ..core.exceptions import InvalidOpenAPISpecError, UnsupportedVersionError +from ..generators import PytestGenerator, JestGenerator, GoGenerator +from ..mocks import MockServerGenerator + + +@click.group() +@click.version_option(version="0.1.0") +@click.option( + "--spec", + "-s", + type=click.Path(exists=True, file_okay=True, dir_okay=False), + help="Path to OpenAPI specification file", +) +@click.option( + "--output", + "-o", + type=click.Path(file_okay=False, dir_okay=True), + help="Output directory for generated files", +) +@click.option( + "--mock-url", + default="http://localhost:4010", + help="URL of the mock server", +) +@click.pass_context +def main( + ctx: click.Context, + spec: str, + output: str, + mock_url: str, +): + """API TestGen - Generate integration tests from OpenAPI specifications.""" + ctx.ensure_object(dict) + ctx.obj["spec"] = spec + ctx.obj["output"] = output or "./generated" + ctx.obj["mock_url"] = mock_url + + +@main.command("parse") +@click.pass_context +def parse_spec(ctx: click.Context): + """Parse and validate an OpenAPI specification.""" + spec_path = ctx.obj["spec"] + + if not spec_path: + click.echo("Error: --spec option is required", err=True) + raise click.Abort() + + try: + parser = SpecParser(spec_path) + spec = parser.load() + + info = parser.get_info() + endpoints = parser.get_endpoints() + security_schemes = parser.get_security_schemes() + + click.echo(f"API: {info['title']} v{info['version']}") + click.echo(f"OpenAPI Version: {parser.version}") + click.echo(f"Base Path: {parser.base_path}") + click.echo(f"Endpoints: {len(endpoints)}") + click.echo(f"Security Schemes: {len(security_schemes)}") + click.echo() + + for endpoint in endpoints: + click.echo(f" {endpoint['method'].upper():6} {endpoint['path']}") + + ctx.obj["parser"] = parser + + except InvalidOpenAPISpecError as e: + click.echo(f"Error: {e}", err=True) + raise click.Abort() + except UnsupportedVersionError as e: + click.echo(f"Error: {e}", err=True) + raise click.Abort() + + +@main.command("generate") +@click.argument("framework", type=click.Choice(["pytest", "jest", "go"])) +@click.option( + "--output-file", + "-f", + type=click.Path(file_okay=True, dir_okay=False), + help="Specific output file path", +) +@click.option( + "--package-name", + default="apitest", + help="Go package name (only for go framework)", +) +@click.pass_context +def generate_tests( + ctx: click.Context, + framework: str, + output_file: str, + package_name: str, +): + """Generate test files for a framework (pytest, jest, or go).""" + spec_path = ctx.obj["spec"] + + if not spec_path: + click.echo("Error: --spec option is required", err=True) + raise click.Abort() + + output_dir = ctx.obj["output"] + mock_url = ctx.obj["mock_url"] + + try: + parser = SpecParser(spec_path) + parser.load() + + if framework == "pytest": + generator = PytestGenerator(parser, output_dir, mock_url) + files = generator.generate(output_file) + + elif framework == "jest": + generator = JestGenerator(parser, output_dir, mock_url) + files = generator.generate(output_file) + + elif framework == "go": + generator = GoGenerator(parser, output_dir, mock_url, package_name) + files = generator.generate(output_file) + + click.echo(f"Generated {len(files)} test file(s):") + for f in files: + click.echo(f" - {f}") + + except Exception as e: + click.echo(f"Error: {e}", err=True) + raise click.Abort() + + +@main.command("mock") +@click.option( + "--no-prism-config", + is_flag=True, + help="Skip generating prism-config.json", +) +@click.option( + "--no-docker-compose", + is_flag=True, + help="Skip generating docker-compose.yml", +) +@click.option( + "--no-dockerfile", + is_flag=True, + help="Skip generating Dockerfile", +) +@click.pass_context +def generate_mock( + ctx: click.Context, + no_prism_config: bool, + no_docker_compose: bool, + no_dockerfile: bool, +): + """Generate mock server configuration files.""" + spec_path = ctx.obj["spec"] + + if not spec_path: + click.echo("Error: --spec option is required", err=True) + raise click.Abort() + + output_dir = ctx.obj["output"] + + try: + parser = SpecParser(spec_path) + parser.load() + + generator = MockServerGenerator(parser, output_dir) + + files = generator.generate( + prism_config=not no_prism_config, + docker_compose=not no_docker_compose, + dockerfile=not no_dockerfile, + ) + + click.echo(f"Generated {len(files)} mock server file(s):") + for f in files: + click.echo(f" - {f}") + + except Exception as e: + click.echo(f"Error: {e}", err=True) + raise click.Abort() + + +@main.command("all") +@click.argument("framework", type=click.Choice(["pytest", "jest", "go"])) +@click.pass_context +def generate_all( + ctx: click.Context, + framework: str, +): + """Generate test files and mock server configuration.""" + spec_path = ctx.obj["spec"] + + if not spec_path: + click.echo("Error: --spec option is required", err=True) + raise click.Abort() + + output_dir = ctx.obj["output"] + mock_url = ctx.obj["mock_url"] + + try: + parser = SpecParser(spec_path) + parser.load() + + click.echo("Generating tests...") + if framework == "pytest": + generator = PytestGenerator(parser, output_dir, mock_url) + files = generator.generate() + + elif framework == "jest": + generator = JestGenerator(parser, output_dir, mock_url) + files = generator.generate() + + elif framework == "go": + generator = GoGenerator(parser, output_dir, mock_url) + files = generator.generate() + + click.echo(f"Generated {len(files)} test file(s)") + + click.echo("Generating mock server configuration...") + mock_generator = MockServerGenerator(parser, output_dir) + mock_files = mock_generator.generate() + + click.echo(f"Generated {len(mock_files)} mock server file(s)") + + click.echo("\nAll files generated successfully!") + + except Exception as e: + click.echo(f"Error: {e}", err=True) + raise click.Abort() + + +@main.command("auth") +@click.argument("scheme_name") +@click.option("--type", "auth_type", type=click.Choice(["apiKey", "bearer", "basic"]), help="Authentication type") +@click.option("--header", help="Header name for API key", default="X-API-Key") +@click.option("--token", help="Bearer token or API key value") +@click.option("--username", help="Username for Basic auth") +@click.option("--password", help="Password for Basic auth") +@click.pass_context +def configure_auth( + ctx: click.Context, + scheme_name: str, + auth_type: str, + header: str, + token: str, + username: str, + password: str, +): + """Configure authentication for a security scheme.""" + auth_config = AuthConfig() + + if auth_type == "apiKey": + auth_config.add_api_key(scheme_name, header, token or "") + elif auth_type == "bearer": + auth_config.add_bearer(scheme_name, token or "") + elif auth_type == "basic": + auth_config.add_basic(scheme_name, username or "", password or "") + + click.echo(f"Authentication scheme '{scheme_name}' configured:") + methods = auth_config.get_all_methods() + for name, method in methods.items(): + click.echo(f" - {name}: {method['type'].value}") + + +if __name__ == "__main__": + main()