fix: add Gitea Actions CI workflow for automated testing

This commit is contained in:
CI Bot
2026-02-06 06:37:08 +00:00
parent 40a6a4f7d4
commit 839317c44b
24 changed files with 3115 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
"""Core module for API TestGen."""
from .spec_parser import SpecParser
from .auth import AuthConfig
__all__ = ["SpecParser", "AuthConfig"]

313
api_testgen/core/auth.py Normal file
View File

@@ -0,0 +1,313 @@
"""Authentication configuration for API TestGen."""
from typing import Any, Dict, List, Optional, Union
from enum import Enum
from .exceptions import AuthConfigError, MissingSecuritySchemeError
class AuthType(str, Enum):
"""Types of authentication."""
API_KEY = "apiKey"
BEARER = "bearer"
BASIC = "basic"
NONE = "none"
class AuthConfig:
"""Authentication configuration for generated tests."""
def __init__(self):
"""Initialize authentication configuration."""
self._auth_methods: Dict[str, Dict[str, Any]] = {}
def add_api_key(
self,
scheme_name: str,
header_name: str = "X-API-Key",
api_key: str = "",
) -> "AuthConfig":
"""Add API key authentication.
Args:
scheme_name: Name of the security scheme in OpenAPI spec.
header_name: Name of the header containing the API key.
api_key: The API key value (can be set later).
Returns:
Self for method chaining.
"""
self._auth_methods[scheme_name] = {
"type": AuthType.API_KEY,
"header_name": header_name,
"api_key": api_key,
}
return self
def add_bearer(
self,
scheme_name: str,
token: str = "",
token_prefix: str = "Bearer",
) -> "AuthConfig":
"""Add Bearer token authentication.
Args:
scheme_name: Name of the security scheme in OpenAPI spec.
token: The Bearer token value (can be set later).
token_prefix: The token prefix (default: Bearer).
Returns:
Self for method chaining.
"""
self._auth_methods[scheme_name] = {
"type": AuthType.BEARER,
"token": token,
"token_prefix": token_prefix,
}
return self
def add_basic(
self,
scheme_name: str,
username: str = "",
password: str = "",
) -> "AuthConfig":
"""Add Basic authentication.
Args:
scheme_name: Name of the security scheme in OpenAPI spec.
username: The username (can be set later).
password: The password (can be set later).
Returns:
Self for method chaining.
"""
self._auth_methods[scheme_name] = {
"type": AuthType.BASIC,
"username": username,
"password": password,
}
return self
def get_auth_method(self, scheme_name: str) -> Optional[Dict[str, Any]]:
"""Get authentication method by scheme name.
Args:
scheme_name: Name of the security scheme.
Returns:
Authentication method configuration or None.
"""
return self._auth_methods.get(scheme_name)
def get_all_methods(self) -> Dict[str, Dict[str, Any]]:
"""Get all configured authentication methods.
Returns:
Dictionary of scheme names and their configurations.
"""
return self._auth_methods.copy()
def get_headers(self, scheme_name: str) -> Dict[str, str]:
"""Get authentication headers for a scheme.
Args:
scheme_name: Name of the security scheme.
Returns:
Dictionary of header names and values.
Raises:
AuthConfigError: If scheme is not configured.
"""
method = self.get_auth_method(scheme_name)
if not method:
raise AuthConfigError(f"Authentication scheme '{scheme_name}' not configured")
if method["type"] == AuthType.API_KEY:
return {method["header_name"]: method["api_key"]}
elif method["type"] == AuthType.BEARER:
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}"}
return {}
def build_from_spec(
self,
security_schemes: Dict[str, Any],
security_requirements: List[Dict[str, Any]],
) -> "AuthConfig":
"""Build auth configuration from OpenAPI security schemes.
Args:
security_schemes: Security schemes from OpenAPI spec.
security_requirements: Security requirements from endpoint.
Returns:
Self for method chaining.
Raises:
MissingSecuritySchemeError: If required scheme is not defined.
"""
for requirement in security_requirements:
for scheme_name in requirement.keys():
if scheme_name not in self._auth_methods:
if scheme_name not in security_schemes:
raise MissingSecuritySchemeError(
f"Security scheme '{scheme_name}' not found in spec"
)
scheme = security_schemes[scheme_name]
self._add_scheme_from_spec(scheme_name, scheme)
return self
def _add_scheme_from_spec(self, scheme_name: str, scheme: Dict[str, Any]) -> None:
"""Add authentication scheme from OpenAPI spec definition.
Args:
scheme_name: Name of the security scheme.
scheme: The security scheme definition from OpenAPI spec.
"""
scheme_type = scheme.get("type", "")
if scheme_type == "apiKey":
self.add_api_key(
scheme_name,
header_name=scheme.get("name", "X-API-Key"),
)
elif scheme_type == "http":
scheme_scheme = scheme.get("scheme", "").lower()
if scheme_scheme == "bearer":
self.add_bearer(scheme_name)
elif scheme_scheme == "basic":
self.add_basic(scheme_name)
elif scheme_type == "openIdConnect":
self.add_bearer(scheme_name)
elif scheme_type == "oauth2":
self.add_bearer(scheme_name)
def generate_auth_code(self, scheme_name: str, framework: str = "pytest") -> str:
"""Generate authentication code for a test framework.
Args:
scheme_name: Name of the security scheme.
framework: Target test framework (pytest, jest, go).
Returns:
String containing authentication code snippet.
"""
method = self.get_auth_method(scheme_name)
if not method:
return ""
if framework == "pytest":
return self._generate_pytest_auth(method)
elif framework == "jest":
return self._generate_jest_auth(method)
elif framework == "go":
return self._generate_go_auth(method)
return ""
def _generate_pytest_auth(self, method: Dict[str, Any]) -> str:
"""Generate pytest authentication code.
Args:
method: Authentication method configuration.
Returns:
String containing pytest auth code.
"""
if method["type"] == AuthType.API_KEY:
return f'''
@pytest.fixture
def api_key_headers():
return {{"{method['header_name']}": "{method['api_key']}"}}
'''
elif method["type"] == AuthType.BEARER:
return f'''
@pytest.fixture
def bearer_headers():
return {{"Authorization": "{method['token_prefix']} {method['token']}"}}
'''
elif method["type"] == AuthType.BASIC:
return f'''
import base64
@pytest.fixture
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:
"""Generate Jest authentication code.
Args:
method: Authentication method configuration.
Returns:
String containing Jest auth code.
"""
if method["type"] == AuthType.API_KEY:
return f'''
const getApiKeyHeaders = () => ({{
"{method['header_name']}": process.env.API_KEY || "{method['api_key']}",
}});
'''
elif method["type"] == AuthType.BEARER:
return f'''
const getBearerHeaders = () => ({{
Authorization: `${{process.env.TOKEN_PREFIX || "{method['token_prefix']}"}} ${{process.env.TOKEN || "{method['token']}"}}`,
}});
'''
elif method["type"] == AuthType.BASIC:
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:
"""Generate Go authentication code.
Args:
method: Authentication method configuration.
Returns:
String containing Go auth code.
"""
if method["type"] == AuthType.API_KEY:
return f'''
func getAPIKeyHeaders() map[string]string {{
return map[string]string{{
"{method['header_name']}": os.Getenv("API_KEY"),
}}
}}
'''
elif method["type"] == AuthType.BEARER:
return f'''
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 f'''
func getBasicHeaders(username, password string) map[string]string {{
auth := username + ":" + password
encoded := base64.StdEncoding.EncodeToString([]byte(auth))
return map[string]string{{
"Authorization": "Basic " + encoded,
}}
}}
'''
return ""

View File

@@ -0,0 +1,36 @@
"""Custom exceptions for API TestGen."""
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

View File

@@ -0,0 +1,307 @@
"""OpenAPI Specification Parser."""
import yaml
import json
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
from openapi_spec_validator import validate
from openapi_spec_validator.versions import consts as validator_consts
from .exceptions import InvalidOpenAPISpecError, UnsupportedVersionError
class SpecParser:
"""Parse and validate OpenAPI/Swagger specifications."""
SUPPORTED_VERSIONS = ["2.0", "3.0.0", "3.0.1", "3.0.2", "3.0.3", "3.1.0"]
def __init__(self, spec_path: Union[str, Path]):
"""Initialize the spec parser.
Args:
spec_path: Path to OpenAPI specification file (YAML or JSON).
"""
self.spec_path = Path(spec_path)
self.spec: Dict[str, Any] = {}
self.version: str = ""
self.base_path: str = ""
self.servers: List[Dict[str, str]] = []
def load(self) -> Dict[str, Any]:
"""Load and validate the OpenAPI specification.
Returns:
The parsed specification dictionary.
Raises:
InvalidOpenAPISpecError: If the specification is invalid.
UnsupportedVersionError: If the OpenAPI version is not supported.
"""
self.spec = self._load_file()
self._validate()
self._extract_metadata()
return self.spec
def _load_file(self) -> Dict[str, Any]:
"""Load the specification file.
Returns:
Parsed specification dictionary.
Raises:
InvalidOpenAPISpecError: If the file cannot be loaded or parsed.
"""
if not self.spec_path.exists():
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']:
return yaml.safe_load(f) or {}
elif self.spec_path.suffix == '.json':
return json.load(f)
else:
return yaml.safe_load(f) or {}
except (yaml.YAMLError, json.JSONDecodeError) as e:
raise InvalidOpenAPISpecError(f"Failed to parse specification: {e}")
def _validate(self) -> None:
"""Validate the specification.
Raises:
InvalidOpenAPISpecError: If the specification is invalid.
UnsupportedVersionError: If the OpenAPI version is not supported.
"""
try:
validate(self.spec)
except Exception as e:
raise InvalidOpenAPISpecError(f"Invalid OpenAPI specification: {e}")
version = self._get_version()
if version not in self.SUPPORTED_VERSIONS:
raise UnsupportedVersionError(
f"Unsupported OpenAPI version: {version}. "
f"Supported versions: {', '.join(self.SUPPORTED_VERSIONS)}"
)
def _get_version(self) -> str:
"""Extract the OpenAPI version from the spec.
Returns:
The OpenAPI version string.
"""
if "openapi" in self.spec:
return self.spec["openapi"]
elif "swagger" in self.spec:
return self.spec["swagger"]
return "2.0"
def _extract_metadata(self) -> None:
"""Extract metadata from the specification."""
self.version = self._get_version()
self.base_path = self.spec.get("basePath", "")
self.servers = self.spec.get("servers", [])
def get_paths(self) -> Dict[str, Any]:
"""Get all paths from the specification.
Returns:
Dictionary of paths and their operations.
"""
return self.spec.get("paths", {})
def get_endpoints(self) -> List[Dict[str, Any]]:
"""Extract all endpoints from the specification.
Returns:
List of endpoint dictionaries with path, method, and details.
"""
endpoints = []
paths = self.get_paths()
for path, path_item in paths.items():
for method, operation in path_item.items():
if method.lower() in ["get", "post", "put", "patch", "delete", "options", "head"]:
endpoint = {
"path": path,
"method": method.lower(),
"operation_id": operation.get("operationId", ""),
"summary": operation.get("summary", ""),
"description": operation.get("description", ""),
"tags": operation.get("tags", []),
"parameters": self._extract_parameters(path_item, operation),
"request_body": self._extract_request_body(operation),
"responses": self._extract_responses(operation),
"security": self._extract_security(operation),
}
endpoints.append(endpoint)
return endpoints
def _extract_parameters(self, path_item: Dict, operation: Dict) -> List[Dict[str, Any]]:
"""Extract parameters from path item and operation.
Args:
path_item: The path item dictionary.
operation: The operation dictionary.
Returns:
List of parameter dictionaries.
"""
parameters = []
for param in path_item.get("parameters", []):
if param.get("in") != "body":
parameters.append(self._normalize_parameter(param))
for param in operation.get("parameters", []):
if param.get("in") != "body":
parameters.append(self._normalize_parameter(param))
return parameters
def _normalize_parameter(self, param: Dict[str, Any]) -> Dict[str, Any]:
"""Normalize a parameter.
Args:
param: The parameter dictionary.
Returns:
Normalized parameter dictionary.
"""
return {
"name": param.get("name", ""),
"in": param.get("in", ""),
"description": param.get("description", ""),
"required": param.get("required", False),
"schema": param.get("schema", {}),
"type": param.get("type", ""),
"enum": param.get("enum", []),
"default": param.get("default"),
}
def _extract_request_body(self, operation: Dict) -> Optional[Dict[str, Any]]:
"""Extract request body from operation.
Args:
operation: The operation dictionary.
Returns:
Request body dictionary or None.
"""
if self.version.startswith("3."):
request_body = operation.get("requestBody", {})
if not request_body:
return None
content = request_body.get("content", {})
media_types = list(content.keys())
return {
"description": request_body.get("description", ""),
"required": request_body.get("required", False),
"media_types": media_types,
"schema": content.get(media_types[0], {}).get("schema", {}) if media_types else {},
}
else:
params = operation.get("parameters", [])
for param in params:
if param.get("in") == "body":
return {
"description": param.get("description", ""),
"required": param.get("required", False),
"schema": param.get("schema", {}),
}
return None
def _extract_responses(self, operation: Dict) -> Dict[str, Any]:
"""Extract responses from operation.
Args:
operation: The operation dictionary.
Returns:
Dictionary of response status codes and their details.
"""
responses = {}
for status_code, response in operation.get("responses", {}).items():
content = response.get("content", {})
if self.version.startswith("3."):
media_types = list(content.keys())
schema = content.get(media_types[0], {}).get("schema", {}) if media_types else {}
else:
schema = response.get("schema", {})
responses[status_code] = {
"description": response.get("description", ""),
"schema": schema,
"media_type": list(content.keys())[0] if content else "application/json",
}
return responses
def _extract_security(self, operation: Dict) -> List[Dict[str, Any]]:
"""Extract security requirements from operation.
Args:
operation: The operation dictionary.
Returns:
List of security requirement dictionaries.
"""
return operation.get("security", self.spec.get("security", []))
def get_security_schemes(self) -> Dict[str, Any]:
"""Get security schemes from the specification.
Returns:
Dictionary of security scheme names and their definitions.
"""
if self.version.startswith("3."):
return self.spec.get("components", {}).get("securitySchemes", {})
else:
return self.spec.get("securityDefinitions", {})
def get_definitions(self) -> Dict[str, Any]:
"""Get schema definitions from the specification.
Returns:
Dictionary of schema definitions.
"""
if self.version.startswith("3."):
return self.spec.get("components", {}).get("schemas", {})
else:
return self.spec.get("definitions", {})
def get_info(self) -> Dict[str, str]:
"""Get API info from the specification.
Returns:
Dictionary with title, version, and description.
"""
info = self.spec.get("info", {})
return {
"title": info.get("title", "API"),
"version": info.get("version", "1.0.0"),
"description": info.get("description", ""),
}
def to_dict(self) -> Dict[str, Any]:
"""Convert the spec to a dictionary.
Returns:
Dictionary representation of the parsed spec.
"""
return {
"version": self.version,
"base_path": self.base_path,
"servers": self.servers,
"info": self.get_info(),
"paths": self.get_paths(),
"endpoints": self.get_endpoints(),
"security_schemes": self.get_security_schemes(),
"definitions": self.get_definitions(),
}