diff --git a/api_testgen/cli/main.py b/api_testgen/cli/main.py index eada49a..8da570e 100644 --- a/api_testgen/cli/main.py +++ b/api_testgen/cli/main.py @@ -238,7 +238,12 @@ def generate_all( @main.command("auth") @click.argument("scheme_name") -@click.option("--type", "auth_type", type=click.Choice(["apiKey", "bearer", "basic"]), help="Authentication type") +@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") diff --git a/api_testgen/core/auth.py b/api_testgen/core/auth.py index f1bbebb..7f609f5 100644 --- a/api_testgen/core/auth.py +++ b/api_testgen/core/auth.py @@ -8,6 +8,7 @@ from .exceptions import AuthConfigError, MissingSecuritySchemeError class AuthType(str, Enum): """Types of authentication.""" + API_KEY = "apiKey" BEARER = "bearer" BASIC = "basic" @@ -131,6 +132,7 @@ class AuthConfig: return {"Authorization": f"{method['token_prefix']} {method['token']}"} elif method["type"] == AuthType.BASIC: import base64 + credentials = f"{method['username']}:{method['password']}" encoded = base64.b64encode(credentials.encode()).decode() return {"Authorization": f"Basic {encoded}"} @@ -222,19 +224,19 @@ class AuthConfig: String containing pytest auth code. """ if method["type"] == AuthType.API_KEY: - return f''' + return f""" @pytest.fixture def api_key_headers(): return {{"{method['header_name']}": "{method['api_key']}"}} -''' +""" elif method["type"] == AuthType.BEARER: - return f''' + return f""" @pytest.fixture def bearer_headers(): return {{"Authorization": "{method['token_prefix']} {method['token']}"}} -''' +""" elif method["type"] == AuthType.BASIC: - return f''' + return f""" import base64 @pytest.fixture @@ -242,7 +244,7 @@ def basic_headers(): credentials = f"{{"{method['username']}"}}:{{"{method['password']}"}}" encoded = base64.b64encode(credentials.encode()).decode() return {{"Authorization": f"Basic {{encoded}}"}} -''' +""" return "" def _generate_jest_auth(self, method: Dict[str, Any]) -> str: @@ -255,24 +257,24 @@ def basic_headers(): String containing Jest auth code. """ if method["type"] == AuthType.API_KEY: - return f''' + return f""" const getApiKeyHeaders = () => ({{ "{method['header_name']}": process.env.API_KEY || "{method['api_key']}", }}); -''' +""" elif method["type"] == AuthType.BEARER: - return f''' + return f""" const getBearerHeaders = () => ({{ Authorization: `${{process.env.TOKEN_PREFIX || "{method['token_prefix']}"}} ${{process.env.TOKEN || "{method['token']}"}}`, }}); -''' +""" elif method["type"] == AuthType.BASIC: - return f''' + return f""" const getBasicHeaders = () => {{ const credentials = Buffer.from(`${{process.env.USERNAME || "{method['username']}"}}:${{process.env.PASSWORD || "{method['password']}"}}`).toString('base64'); return {{ Authorization: `Basic ${{credentials}}` }}; }}; -''' +""" return "" def _generate_go_auth(self, method: Dict[str, Any]) -> str: @@ -285,23 +287,23 @@ const getBasicHeaders = () => {{ String containing Go auth code. """ if method["type"] == AuthType.API_KEY: - return f''' + return f""" func getAPIKeyHeaders() map[string]string {{ return map[string]string{{ "{method['header_name']}": os.Getenv("API_KEY"), }} }} -''' +""" elif method["type"] == AuthType.BEARER: - return ''' + return """ func getBearerHeaders() map[string]string { return map[string]string{ "Authorization": fmt.Sprintf("%s %s", os.Getenv("TOKEN_PREFIX"), os.Getenv("TOKEN")), } } -''' +""" elif method["type"] == AuthType.BASIC: - return ''' + return """ func getBasicHeaders(username, password string) map[string]string { auth := username + ":" + password encoded := base64.StdEncoding.EncodeToString([]byte(auth)) @@ -309,5 +311,5 @@ func getBasicHeaders(username, password string) map[string]string { "Authorization": "Basic " + encoded, } } -''' +""" return "" diff --git a/api_testgen/core/exceptions.py b/api_testgen/core/exceptions.py index 5f55cc4..7ae64bd 100644 --- a/api_testgen/core/exceptions.py +++ b/api_testgen/core/exceptions.py @@ -3,34 +3,41 @@ class SpecParserError(Exception): """Base exception for spec parser errors.""" + pass class InvalidOpenAPISpecError(SpecParserError): """Raised when OpenAPI specification is invalid.""" + pass class UnsupportedVersionError(SpecParserError): """Raised when OpenAPI version is not supported.""" + pass class AuthConfigError(Exception): """Base exception for auth configuration errors.""" + pass class MissingSecuritySchemeError(AuthConfigError): """Raised when security scheme is not defined in spec.""" + pass class GeneratorError(Exception): """Base exception for generator errors.""" + pass class TemplateRenderError(GeneratorError): """Raised when template rendering fails.""" + pass diff --git a/api_testgen/core/spec_parser.py b/api_testgen/core/spec_parser.py index aab1047..0ec8cc1 100644 --- a/api_testgen/core/spec_parser.py +++ b/api_testgen/core/spec_parser.py @@ -55,10 +55,10 @@ class SpecParser: raise InvalidOpenAPISpecError(f"Specification file not found: {self.spec_path}") try: - with open(self.spec_path, 'r', encoding='utf-8') as f: - if self.spec_path.suffix in ['.yaml', '.yml']: + with open(self.spec_path, "r", encoding="utf-8") as f: + if self.spec_path.suffix in [".yaml", ".yml"]: return yaml.safe_load(f) or {} - elif self.spec_path.suffix == '.json': + elif self.spec_path.suffix == ".json": return json.load(f) else: return yaml.safe_load(f) or {} diff --git a/api_testgen/generators/go.py b/api_testgen/generators/go.py index 33ccd3c..3a138aa 100644 --- a/api_testgen/generators/go.py +++ b/api_testgen/generators/go.py @@ -86,7 +86,9 @@ class GoGenerator: return generated_files - def _group_endpoints_by_path(self, endpoints: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]: + def _group_endpoints_by_path( + self, endpoints: List[Dict[str, Any]] + ) -> Dict[str, List[Dict[str, Any]]]: """Group endpoints by their path. Args: @@ -133,7 +135,7 @@ class GoGenerator: test_name = self._generate_test_name(endpoint) params = self._generate_params(endpoint) - test_code = f''' + test_code = f""" func Test{test_name}(t *testing.T) {{ client := &http.Client{{Timeout: 10 * time.Second}} url := baseURL + "{endpoint['path']}" @@ -162,7 +164,7 @@ func Test{test_name}(t *testing.T) {{ t.Errorf("Expected status code in [200, 201, 204], got %d", resp.StatusCode) }} }} -''' +""" return test_code def _generate_test_name(self, endpoint: Dict[str, Any]) -> str: @@ -200,7 +202,15 @@ func Test{test_name}(t *testing.T) {{ if param["in"] == "path": params.append(f'{param_name} := "test_{param_name}"') - params.append('url = strings.Replace(url, "' + '{' + param_name + '}' + '", ' + param_name + ', 1)') + params.append( + 'url = strings.Replace(url, "' + + "{" + + param_name + + "}" + + '", ' + + param_name + + ", 1)" + ) elif param["in"] == "query": params.append(f'q := url.Values{{{param_name}: []string{{"test"}}}}') @@ -222,6 +232,14 @@ func Test{test_name}(t *testing.T) {{ parts = [] for param in path_params: - parts.append('strings.Replace(url, "' + '{' + param['name'] + '}' + '", "test_' + param['name'] + '", 1)') + parts.append( + 'strings.Replace(url, "' + + "{" + + param["name"] + + "}" + + '", "test_' + + param["name"] + + '", 1)' + ) return "" diff --git a/api_testgen/generators/jest.py b/api_testgen/generators/jest.py index 406b6bf..b56e357 100644 --- a/api_testgen/generators/jest.py +++ b/api_testgen/generators/jest.py @@ -30,7 +30,9 @@ class JestGenerator: self.output_dir = Path(output_dir) self.mock_server_url = mock_server_url self.env = Environment( - loader=FileSystemLoader(str(Path(__file__).parent.parent.parent / "templates" / "jest")), + loader=FileSystemLoader( + str(Path(__file__).parent.parent.parent / "templates" / "jest") + ), trim_blocks=True, lstrip_blocks=True, ) @@ -113,7 +115,7 @@ class JestGenerator: endpoint_path = endpoint["path"] endpoint_method = endpoint["method"] - test_code = f''' + test_code = f""" describe('{describe_name}', () => {{ it('should {endpoint_method.upper()} {endpoint_path}', async () => {{ const response = await request(baseUrl) @@ -122,7 +124,7 @@ describe('{describe_name}', () => {{ expect([200, 201, 204]).toContain(response.status); }}); }}); -''' +""" return test_code def _generate_test_name(self, endpoint: Dict[str, Any]) -> str: @@ -161,7 +163,7 @@ describe('{describe_name}', () => {{ parts.append(f'{param_name}="test_{param_name}"') elif param["in"] == "query": - parts.append(f'{param_name}') + parts.append(f"{param_name}") if parts: return ", {" + ", ".join(parts) + "}" diff --git a/api_testgen/generators/pytest.py b/api_testgen/generators/pytest.py index 83504f0..c50cbc9 100644 --- a/api_testgen/generators/pytest.py +++ b/api_testgen/generators/pytest.py @@ -30,7 +30,9 @@ class PytestGenerator: self.output_dir = Path(output_dir) self.mock_server_url = mock_server_url self.env = Environment( - loader=FileSystemLoader(str(Path(__file__).parent.parent.parent / "templates" / "pytest")), + loader=FileSystemLoader( + str(Path(__file__).parent.parent.parent / "templates" / "pytest") + ), trim_blocks=True, lstrip_blocks=True, ) @@ -160,7 +162,7 @@ def test_{test_name}(base_url, {params}): params.append(f'{param_name}="test_{param_name}"') elif param["in"] == "query": - params.append(f'{param_name}=None') + params.append(f"{param_name}=None") return ", ".join(params) diff --git a/api_testgen/mocks/generator.py b/api_testgen/mocks/generator.py index 06cacf0..0c2c3ba 100644 --- a/api_testgen/mocks/generator.py +++ b/api_testgen/mocks/generator.py @@ -112,7 +112,7 @@ class MockServerGenerator: spec_info = self.spec_parser.get_info() - compose_content = f'''version: '3.8' + compose_content = f"""version: '3.8' services: mock-server: @@ -147,7 +147,7 @@ services: volumes: - ./:/app restart: unless-stopped -''' +""" output_path.write_text(compose_content) return output_path @@ -168,7 +168,7 @@ services: spec_info = self.spec_parser.get_info() - dockerfile_content = f'''FROM stoplight/prism:latest + dockerfile_content = f"""FROM stoplight/prism:latest LABEL maintainer="developer@example.com" LABEL description="Mock server for {spec_info['title']} API" @@ -180,7 +180,7 @@ COPY openapi.yaml . EXPOSE {self.DEFAULT_PORT} CMD ["mock", "--spec", "openapi.yaml", "--port", "{self.DEFAULT_PORT}", "--host", "0.0.0.0"] -''' +""" output_path.write_text(dockerfile_content) return output_path @@ -199,7 +199,7 @@ CMD ["mock", "--spec", "openapi.yaml", "--port", "{self.DEFAULT_PORT}", "--host" else: output_path = self.output_dir / "start-mock-server.sh" - script_content = f'''#!/bin/bash + script_content = f"""#!/bin/bash set -e @@ -216,7 +216,7 @@ docker compose up -d mock-server echo "Mock server started successfully!" echo "To stop: docker compose down" -''' +""" output_path.write_text(script_content) output_path.chmod(0o755)