Compare commits
115 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bdaf5e0ee7 | |||
| 4a155eadc2 | |||
| 8b1b5f7ada | |||
| 8d94ee3d7b | |||
| 9c1322886c | |||
| 7b75c71882 | |||
| 3557589fa5 | |||
| dc5d0c4942 | |||
| 5a16a23fcb | |||
| 8f8da672df | |||
| 630b66ccf6 | |||
| 9d57cdf834 | |||
| cb91209370 | |||
| 7a51dce2b9 | |||
| 8d5cfe1ecd | |||
| 9f1fae72ba | |||
| d48db96329 | |||
| 058830344d | |||
| d7a1a473d9 | |||
| 97ccbb32a4 | |||
| 56a68c0b1f | |||
| ed71d7516d | |||
| a181a831f6 | |||
| 92aab1b47c | |||
| 5460ed2262 | |||
| f7d507ee5c | |||
| a2123dcffb | |||
| 3d66c38af9 | |||
| 3658b0e73a | |||
| d6cc83c0a6 | |||
| 0a5789130c | |||
| 0a28cc443a | |||
| 0b8485a286 | |||
| 5a549792ea | |||
| 730be92a3d | |||
| 9504a6edfc | |||
| 5be818908c | |||
| 31a2493734 | |||
| 04f0eccb2c | |||
| 40220881e0 | |||
| 448d51a762 | |||
| d59ffd3385 | |||
| f55caf3293 | |||
| 9b8e18c64e | |||
| 8132663ce6 | |||
| 0a1545c312 | |||
| 4bea8c8bc5 | |||
| 1fb7362d1f | |||
| df11e53d59 | |||
| 1a74564566 | |||
| 9350f6a188 | |||
| 685a56ae67 | |||
| 0709018ba3 | |||
| 768ad3bc3a | |||
| f809b55151 | |||
| f48a871384 | |||
| 797b4c881c | |||
| 961ae9cba1 | |||
| a85a349d34 | |||
| 40a4a694a0 | |||
| 44718ddd9b | |||
| 3a95323b03 | |||
| eaa32de84b | |||
| 347ee198f9 | |||
| fc99266b94 | |||
| d0f43098ca | |||
| 883e30a00c | |||
| 08f5c34439 | |||
| cd8f41bc4f | |||
| 72f792306e | |||
| d16b8bea38 | |||
| 7f265f6512 | |||
| 4a5281f5d9 | |||
| 58f1852733 | |||
| f0e219f21a | |||
| 38c0d91ee5 | |||
| fd9df8ddf1 | |||
| 44516db93c | |||
| 5f741f57a6 | |||
| a16d436336 | |||
| 27664cf609 | |||
| 9f6fba19dd | |||
| 1533fe3058 | |||
| 5f4b76d8fd | |||
| 616a5d424b | |||
| 11d6b9c593 | |||
| 2e6ba76a02 | |||
| d7fc93c740 | |||
| 9e5e62ad7f | |||
| ff73520767 | |||
| f8219f19c3 | |||
| f6c0803f93 | |||
| 7b58a72c9c | |||
| ddaba43ae7 | |||
| 23242b4aa3 | |||
| 25021a4ea7 | |||
| 4c6ccbc70c | |||
| 8c8ba763a3 | |||
| 184bcd3fd3 | |||
| d7bf70e56e | |||
| 24fed63c13 | |||
| c1952d7ca9 | |||
| 77f7ae4cbf | |||
| 6032d2d44c | |||
| 896a3d912f | |||
| a747357a23 | |||
| ecf8485b6f | |||
| d0cf0e3711 | |||
| a859ed67cb | |||
| 0b05af9a4e | |||
| a1d3ff4f49 | |||
| bef3c2ab4a | |||
| 71bcd1850e | |||
| 1722436b0b | |||
| 8341bc1132 |
@@ -2,55 +2,17 @@ name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -e ".[dev]"
|
||||
|
||||
- name: Run linting
|
||||
run: ruff check src/
|
||||
|
||||
- name: Run type checking
|
||||
run: mypy src/mockapi/
|
||||
|
||||
- name: Run tests
|
||||
run: pytest tests/ -v
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
pip install build
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
python -m build
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
- run: pip install -e ".[dev]"
|
||||
- run: pytest tests/unit -v
|
||||
|
||||
62
.gitignore
vendored
62
.gitignore
vendored
@@ -3,9 +3,6 @@ __pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
@@ -35,14 +32,12 @@ pip-delete-this-directory.txt
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
@@ -50,6 +45,35 @@ coverage.xml
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
@@ -59,19 +83,25 @@ ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
|
||||
# ruff
|
||||
.ruff_cache/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Project specific
|
||||
mockapi.yaml
|
||||
mockapi.yml
|
||||
|
||||
# Examples
|
||||
examples/
|
||||
*~
|
||||
|
||||
4
LICENSE
4
LICENSE
@@ -14,8 +14,8 @@ copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FitNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
SOFTWARE.
|
||||
143
README.md
143
README.md
@@ -1,17 +1,14 @@
|
||||
# MockAPI
|
||||
# MockAPI - OpenAPI Mock Server Generator
|
||||
|
||||
OpenAPI Mock Server Generator - Generate functional mock APIs from OpenAPI 3.x specifications.
|
||||
A CLI tool that generates a fully functional mock API server from OpenAPI 3.x specifications.
|
||||
|
||||
## Features
|
||||
|
||||
- **OpenAPI 3.x Support**: Parse and validate OpenAPI specifications
|
||||
- **Mock Server Generation**: Generate working mock servers using connexion
|
||||
- **Random Data Generation**: Create realistic test data from JSON schemas using Faker
|
||||
- **Configurable Response Delays**: Simulate network latency with fixed or random delays
|
||||
- **Error Simulation**: Simulate HTTP error responses for testing
|
||||
- **Request Validation**: Validate incoming requests against OpenAPI spec
|
||||
- **Hot-Reload**: Auto-restart server on spec file changes
|
||||
- **YAML Configuration**: Configure mock behavior via `mockapi.yaml`
|
||||
- Generate mock server from OpenAPI 3.x specs
|
||||
- Configurable response delays and error simulation
|
||||
- Random realistic test data generation from schemas
|
||||
- Request validation against OpenAPI spec
|
||||
- Hot-reload on spec changes
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -19,140 +16,28 @@ OpenAPI Mock Server Generator - Generate functional mock APIs from OpenAPI 3.x s
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
Or install from source:
|
||||
|
||||
```bash
|
||||
git clone https://7000pct.gitea.bloupla.net/7000pctAUTO/mockapi.git
|
||||
cd mockapi
|
||||
pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Validate an OpenAPI spec
|
||||
mockapi validate examples/petstore.yaml
|
||||
mockapi validate spec.yaml
|
||||
|
||||
# Start a mock server
|
||||
mockapi start examples/petstore.yaml
|
||||
mockapi start spec.yaml
|
||||
|
||||
# Generate a summary
|
||||
mockapi generate examples/petstore.yaml
|
||||
```
|
||||
|
||||
## CLI Commands
|
||||
|
||||
### validate
|
||||
Validate an OpenAPI specification file:
|
||||
```bash
|
||||
mockapi validate spec.yaml
|
||||
```
|
||||
|
||||
### start
|
||||
Start a mock server from an OpenAPI spec:
|
||||
```bash
|
||||
mockapi start spec.yaml --port 8080 --host 0.0.0.0
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--port, -p`: Port number (default: 8080)
|
||||
- `--host, -h`: Host to bind to (default: 0.0.0.0)
|
||||
- `--delay, -d`: Fixed response delay in milliseconds
|
||||
- `--random-delay`: Use random delays instead of fixed
|
||||
- `--config, -c`: Path to mockapi.yaml configuration
|
||||
- `--watch, -w`: Enable hot-reload on spec changes
|
||||
- `--verbose, -v`: Enable verbose output
|
||||
|
||||
### generate
|
||||
Generate a summary from an OpenAPI spec:
|
||||
```bash
|
||||
mockapi generate spec.yaml
|
||||
```
|
||||
|
||||
### show-config
|
||||
Show current configuration:
|
||||
```bash
|
||||
# Show configuration
|
||||
mockapi show-config
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Create a `mockapi.yaml` file to configure mock behavior:
|
||||
|
||||
```yaml
|
||||
port: 8080
|
||||
host: 0.0.0.0
|
||||
delay: 100
|
||||
random_delay: false
|
||||
seed: 42
|
||||
validate_requests: true
|
||||
error_probability: 0.0
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| port | int | 8080 | Server port |
|
||||
| host | string | 0.0.0.0 | Server host |
|
||||
| delay | int | 0 | Fixed delay in ms |
|
||||
| random_delay | bool | false | Use random delays |
|
||||
| seed | int | 42 | Random seed |
|
||||
| validate_requests | bool | true | Validate requests |
|
||||
| error_probability | float | 0.0 | Error probability |
|
||||
|
||||
### Environment Variables
|
||||
|
||||
- `MOCKAPI_PORT`: Override default port
|
||||
- `MOCKAPI_HOST`: Override default host
|
||||
- `MOCKAPI_SEED`: Override random seed
|
||||
|
||||
## Hot Reload
|
||||
|
||||
Enable automatic server restart when the spec file changes:
|
||||
|
||||
```bash
|
||||
mockapi start spec.yaml --watch
|
||||
```
|
||||
|
||||
## Request Validation
|
||||
|
||||
By default, incoming requests are validated against the OpenAPI spec. Disable with:
|
||||
|
||||
```yaml
|
||||
validate_requests: false
|
||||
```
|
||||
|
||||
## Error Simulation
|
||||
|
||||
Use the `x-mock-config` extension in your OpenAPI spec:
|
||||
|
||||
```yaml
|
||||
paths:
|
||||
/users:
|
||||
get:
|
||||
x-mock-config:
|
||||
errorProbability: 0.1
|
||||
errorCode: 500
|
||||
errorMessage: "Service unavailable"
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Install development dependencies
|
||||
# Install dev dependencies
|
||||
pip install -e ".[dev]"
|
||||
|
||||
# Run tests
|
||||
pytest tests/ -v
|
||||
|
||||
# Run linting
|
||||
ruff check src/
|
||||
|
||||
# Type checking
|
||||
mypy src/mockapi/
|
||||
# Build
|
||||
python -m build
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT License
|
||||
|
||||
73
examples/petstore.yaml
Normal file
73
examples/petstore.yaml
Normal file
@@ -0,0 +1,73 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Petstore API
|
||||
version: 1.0.0
|
||||
paths:
|
||||
/users:
|
||||
get:
|
||||
operationId: listUsers
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/User'
|
||||
/products:
|
||||
get:
|
||||
operationId: listProducts
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Product'
|
||||
/orders:
|
||||
get:
|
||||
operationId: listOrders
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Order'
|
||||
components:
|
||||
schemas:
|
||||
User:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
Product:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
price:
|
||||
type: number
|
||||
Order:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
userId:
|
||||
type: integer
|
||||
productId:
|
||||
type: integer
|
||||
quantity:
|
||||
type: integer
|
||||
@@ -16,7 +16,6 @@ keywords = ["openapi", "mock", "api", "server", "generator", "testing"]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
@@ -31,7 +30,6 @@ dependencies = [
|
||||
"uvicorn>=0.24.0",
|
||||
"watchdog>=3.0.0",
|
||||
"pyyaml>=6.0.1",
|
||||
"starlette>=0.27.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
@@ -40,8 +38,6 @@ dev = [
|
||||
"pytest-cov>=4.1.0",
|
||||
"pytest-asyncio>=0.21.0",
|
||||
"httpx>=0.25.0",
|
||||
"ruff>=0.1.0",
|
||||
"mypy>=1.7.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -1 +1 @@
|
||||
"""CLI module."""
|
||||
"""CLI command modules."""
|
||||
|
||||
@@ -51,7 +51,7 @@ def validate(spec_file: str, fmt: Optional[str]):
|
||||
click.echo(f" - {error}", err=True)
|
||||
sys.exit(1)
|
||||
else:
|
||||
click.echo("✓ Specification is valid!")
|
||||
click.echo("\u2713 Specification is valid!")
|
||||
click.echo(f" Paths: {len(spec.get('paths', {}))}")
|
||||
click.echo(f" Schemas: {len(spec.get('components', {}).get('schemas', {}))}")
|
||||
|
||||
@@ -62,53 +62,13 @@ def validate(spec_file: str, fmt: Optional[str]):
|
||||
|
||||
@cli.command()
|
||||
@click.argument("spec_file", type=click.Path(exists=True))
|
||||
@click.option(
|
||||
"--port",
|
||||
"-p",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Port to run the mock server on (default: from config or 8080)",
|
||||
)
|
||||
@click.option(
|
||||
"--host",
|
||||
"-h",
|
||||
default=None,
|
||||
help="Host to bind to (default: from config or 0.0.0.0)",
|
||||
)
|
||||
@click.option(
|
||||
"--delay",
|
||||
"-d",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Fixed response delay in milliseconds",
|
||||
)
|
||||
@click.option(
|
||||
"--random-delay",
|
||||
is_flag=True,
|
||||
default=None,
|
||||
help="Use random delays instead of fixed",
|
||||
)
|
||||
@click.option(
|
||||
"--config",
|
||||
"-c",
|
||||
type=click.Path(exists=True),
|
||||
default=None,
|
||||
help="Path to mockapi.yaml configuration file",
|
||||
)
|
||||
@click.option(
|
||||
"--watch",
|
||||
"-w",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Enable hot-reload on spec file changes",
|
||||
)
|
||||
@click.option(
|
||||
"--verbose",
|
||||
"-v",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Enable verbose output",
|
||||
)
|
||||
@click.option("--port", "-p", type=int, default=None, help="Port to run the mock server on")
|
||||
@click.option("--host", "-h", default=None, help="Host to bind to")
|
||||
@click.option("--delay", "-d", type=int, default=None, help="Fixed response delay in milliseconds")
|
||||
@click.option("--random-delay", is_flag=True, default=None, help="Use random delays")
|
||||
@click.option("--config", "-c", type=click.Path(exists=True), default=None, help="Path to mockapi.yaml")
|
||||
@click.option("--watch", "-w", is_flag=True, default=False, help="Enable hot-reload")
|
||||
@click.option("--verbose", "-v", is_flag=True, default=False, help="Enable verbose output")
|
||||
def start(
|
||||
spec_file: str,
|
||||
port: Optional[int],
|
||||
@@ -119,10 +79,7 @@ def start(
|
||||
watch: bool,
|
||||
verbose: bool,
|
||||
):
|
||||
"""Start a mock API server from an OpenAPI specification.
|
||||
|
||||
SPEC_FILE: Path to the OpenAPI spec file (YAML or JSON)
|
||||
"""
|
||||
"""Start a mock API server from an OpenAPI specification."""
|
||||
try:
|
||||
cfg = Config.load(config_path=config)
|
||||
|
||||
@@ -148,7 +105,6 @@ def start(
|
||||
|
||||
if verbose:
|
||||
click.echo(f"Starting mock server on {cfg.host}:{cfg.port}")
|
||||
click.echo(f"Spec file: {spec_file}")
|
||||
|
||||
generator = MockServerGenerator(spec, cfg)
|
||||
app = generator.generate()
|
||||
@@ -159,12 +115,7 @@ def start(
|
||||
reloader.start_watching()
|
||||
else:
|
||||
import uvicorn
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=cfg.host,
|
||||
port=cfg.port,
|
||||
log_level="info" if verbose else "warning",
|
||||
)
|
||||
uvicorn.run(app, host=cfg.host, port=cfg.port, log_level="info" if verbose else "warning")
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"Error: {e}", err=True)
|
||||
@@ -176,18 +127,9 @@ def start(
|
||||
|
||||
@cli.command()
|
||||
@click.argument("spec_file", type=click.Path(exists=True))
|
||||
@click.option(
|
||||
"--output",
|
||||
"-o",
|
||||
type=click.Path(),
|
||||
default=None,
|
||||
help="Output file path (default: stdout)",
|
||||
)
|
||||
@click.option("--output", "-o", type=click.Path(), default=None, help="Output file path")
|
||||
def generate(spec_file: str, output: Optional[str]):
|
||||
"""Generate code/structure from an OpenAPI spec (dry-run mode).
|
||||
|
||||
SPEC_FILE: Path to the OpenAPI spec file (YAML or JSON)
|
||||
"""
|
||||
"""Generate code from an OpenAPI spec (dry-run mode)."""
|
||||
try:
|
||||
loader = SpecLoader(spec_file)
|
||||
spec = loader.load()
|
||||
@@ -238,15 +180,9 @@ def generate(spec_file: str, output: Optional[str]):
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option(
|
||||
"--config",
|
||||
"-c",
|
||||
type=click.Path(exists=True),
|
||||
default=None,
|
||||
help="Path to mockapi.yaml configuration file",
|
||||
)
|
||||
@click.option("--config", "-c", type=click.Path(exists=True), default=None, help="Path to mockapi.yaml")
|
||||
def show_config(config: Optional[str]):
|
||||
"""Show the current configuration (from file and defaults)."""
|
||||
"""Show the current configuration."""
|
||||
try:
|
||||
cfg = Config.load(config_path=config)
|
||||
click.echo("Current MockAPI Configuration:")
|
||||
|
||||
@@ -22,19 +22,11 @@ class Config:
|
||||
strict_validation: bool = False
|
||||
error_probability: float = 0.0
|
||||
error_code: int = 500
|
||||
|
||||
config_path: Optional[str] = None
|
||||
|
||||
@classmethod
|
||||
def load(cls, config_path: Optional[str] = None) -> "Config":
|
||||
"""Load configuration from file and environment.
|
||||
|
||||
Args:
|
||||
config_path: Path to mockapi.yaml config file
|
||||
|
||||
Returns:
|
||||
Config instance with loaded values
|
||||
"""
|
||||
"""Load configuration from file and environment."""
|
||||
config = cls()
|
||||
|
||||
if config_path:
|
||||
@@ -62,11 +54,7 @@ class Config:
|
||||
|
||||
@classmethod
|
||||
def _load_from_env(cls) -> Dict[str, Any]:
|
||||
"""Load configuration from environment variables.
|
||||
|
||||
Returns:
|
||||
Dictionary of configuration values
|
||||
"""
|
||||
"""Load configuration from environment variables."""
|
||||
config = {}
|
||||
|
||||
if port := os.environ.get("MOCKAPI_PORT"):
|
||||
@@ -88,14 +76,7 @@ class Config:
|
||||
|
||||
@classmethod
|
||||
def _load_from_file(cls, config_path: str) -> Dict[str, Any]:
|
||||
"""Load configuration from YAML file.
|
||||
|
||||
Args:
|
||||
config_path: Path to the config file
|
||||
|
||||
Returns:
|
||||
Dictionary of configuration values
|
||||
"""
|
||||
"""Load configuration from YAML file."""
|
||||
try:
|
||||
with open(config_path, "r") as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
@@ -129,11 +110,7 @@ class Config:
|
||||
return {}
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert config to dictionary.
|
||||
|
||||
Returns:
|
||||
Dictionary representation of config
|
||||
"""
|
||||
"""Convert config to dictionary."""
|
||||
return {
|
||||
"port": self.port,
|
||||
"host": self.host,
|
||||
@@ -149,12 +126,5 @@ class Config:
|
||||
|
||||
|
||||
def load_config(config_path: Optional[str] = None) -> Config:
|
||||
"""Load configuration from file and environment.
|
||||
|
||||
Args:
|
||||
config_path: Optional path to config file
|
||||
|
||||
Returns:
|
||||
Config instance
|
||||
"""
|
||||
"""Load configuration from file and environment."""
|
||||
return Config.load(config_path)
|
||||
|
||||
@@ -20,15 +20,7 @@ class DelayMiddleware(BaseHTTPMiddleware):
|
||||
min_delay: int = 100,
|
||||
max_delay: int = 2000,
|
||||
):
|
||||
"""Initialize the delay middleware.
|
||||
|
||||
Args:
|
||||
app: The ASGI application
|
||||
delay: Fixed delay in milliseconds
|
||||
random_delay: Use random delays
|
||||
min_delay: Minimum delay in ms (for random mode)
|
||||
max_delay: Maximum delay in ms (for random mode)
|
||||
"""
|
||||
"""Initialize the delay middleware."""
|
||||
super().__init__(app)
|
||||
self.delay = delay
|
||||
self.random_delay = random_delay
|
||||
@@ -50,16 +42,7 @@ class DelayMiddleware(BaseHTTPMiddleware):
|
||||
|
||||
@staticmethod
|
||||
def wrap(app, delay: int = 0, random_delay: bool = False) -> "DelayMiddleware":
|
||||
"""Wrap an app with delay middleware.
|
||||
|
||||
Args:
|
||||
app: The application to wrap
|
||||
delay: Fixed delay in milliseconds
|
||||
random_delay: Use random delays
|
||||
|
||||
Returns:
|
||||
Wrapped application
|
||||
"""
|
||||
"""Wrap an app with delay middleware."""
|
||||
return DelayMiddleware(app, delay=delay, random_delay=random_delay)
|
||||
|
||||
|
||||
@@ -71,21 +54,12 @@ class ErrorSimulator:
|
||||
error_probability: float = 0.0,
|
||||
default_error_code: int = 500,
|
||||
):
|
||||
"""Initialize the error simulator.
|
||||
|
||||
Args:
|
||||
error_probability: Probability of returning an error (0.0 to 1.0)
|
||||
default_error_code: Default HTTP error code
|
||||
"""
|
||||
"""Initialize the error simulator."""
|
||||
self.error_probability = error_probability
|
||||
self.default_error_code = default_error_code
|
||||
|
||||
def should_return_error(self) -> bool:
|
||||
"""Determine if an error should be returned.
|
||||
|
||||
Returns:
|
||||
True if error should be returned
|
||||
"""
|
||||
"""Determine if an error should be returned."""
|
||||
return random.random() < self.error_probability
|
||||
|
||||
def get_error_response(
|
||||
@@ -93,15 +67,7 @@ class ErrorSimulator:
|
||||
status_code: Optional[int] = None,
|
||||
message: Optional[str] = None,
|
||||
) -> tuple:
|
||||
"""Get an error response tuple.
|
||||
|
||||
Args:
|
||||
status_code: HTTP status code
|
||||
message: Error message
|
||||
|
||||
Returns:
|
||||
Tuple of (body_dict, status_code)
|
||||
"""
|
||||
"""Get an error response tuple."""
|
||||
return (
|
||||
{"error": message or "Simulated error"},
|
||||
status_code or self.default_error_code,
|
||||
|
||||
@@ -11,12 +11,7 @@ class DataGenerator:
|
||||
"""Generates realistic random test data from JSON schemas."""
|
||||
|
||||
def __init__(self, seed: Optional[int] = None, schemas: Optional[Dict[str, Any]] = None):
|
||||
"""Initialize the data generator.
|
||||
|
||||
Args:
|
||||
seed: Random seed for reproducible data generation
|
||||
schemas: Dictionary of schemas for $ref resolution
|
||||
"""
|
||||
"""Initialize the data generator."""
|
||||
self.seed = seed
|
||||
self.faker = Faker()
|
||||
if seed is not None:
|
||||
@@ -46,8 +41,8 @@ class DataGenerator:
|
||||
"text": lambda: self.faker.text(),
|
||||
"username": lambda: self.faker.user_name(),
|
||||
"password": lambda: self.faker.password(),
|
||||
"ip_v4": lambda: self.faker.ipv4(),
|
||||
"ip_v6": lambda: self.faker.ipv6(),
|
||||
"ipv4": lambda: self.faker.ipv4(),
|
||||
"ipv6": lambda: self.faker.ipv6(),
|
||||
"slug": lambda: self.faker.slug(),
|
||||
"color": lambda: self.faker.color_name(),
|
||||
"currency": lambda: self.faker.currency()[0],
|
||||
@@ -55,14 +50,7 @@ class DataGenerator:
|
||||
}
|
||||
|
||||
def generate(self, schema: Dict[str, Any]) -> Any:
|
||||
"""Generate data from a JSON schema.
|
||||
|
||||
Args:
|
||||
schema: JSON schema definition
|
||||
|
||||
Returns:
|
||||
Generated data matching the schema
|
||||
"""
|
||||
"""Generate data from a JSON schema."""
|
||||
if not schema or not isinstance(schema, dict):
|
||||
return None
|
||||
|
||||
@@ -89,16 +77,8 @@ class DataGenerator:
|
||||
return None
|
||||
|
||||
def _resolve_ref(self, ref: str) -> Any:
|
||||
"""Resolve a $ref reference.
|
||||
|
||||
Args:
|
||||
ref: Reference string like #/components/schemas/User
|
||||
|
||||
Returns:
|
||||
Resolved schema or None
|
||||
"""
|
||||
"""Resolve a $ref reference."""
|
||||
parts = ref.lstrip("#/").split("/")
|
||||
|
||||
skip_prefixes = ["components", "schemas"]
|
||||
start_idx = 0
|
||||
for i, part in enumerate(parts):
|
||||
@@ -106,23 +86,17 @@ class DataGenerator:
|
||||
start_idx = i + 1
|
||||
else:
|
||||
break
|
||||
|
||||
parts = parts[start_idx:]
|
||||
|
||||
if not parts:
|
||||
return None
|
||||
|
||||
current = self._schemas_dict
|
||||
|
||||
for part in parts:
|
||||
if isinstance(current, dict):
|
||||
current = current.get(part, {})
|
||||
else:
|
||||
return None
|
||||
|
||||
if isinstance(current, dict):
|
||||
return self.generate(current)
|
||||
|
||||
return current
|
||||
|
||||
def _generate_boolean(self, schema: Dict[str, Any]) -> bool:
|
||||
@@ -135,7 +109,6 @@ class DataGenerator:
|
||||
"""Generate a random integer."""
|
||||
if "enum" in schema:
|
||||
return random.choice(schema["enum"])
|
||||
|
||||
minimum = schema.get("minimum", 0)
|
||||
maximum = schema.get("maximum", 10000)
|
||||
return random.randint(int(minimum), int(maximum))
|
||||
@@ -144,7 +117,6 @@ class DataGenerator:
|
||||
"""Generate a random number."""
|
||||
if "enum" in schema:
|
||||
return random.choice(schema["enum"])
|
||||
|
||||
minimum = schema.get("minimum", 0.0)
|
||||
maximum = schema.get("maximum", 10000.0)
|
||||
return random.uniform(float(minimum), float(maximum))
|
||||
@@ -153,46 +125,33 @@ class DataGenerator:
|
||||
"""Generate a random string."""
|
||||
if "enum" in schema:
|
||||
return random.choice(schema["enum"])
|
||||
|
||||
format_type = schema.get("format", "")
|
||||
|
||||
if format_type in self._faker_providers:
|
||||
return self._faker_providers[format_type]()
|
||||
|
||||
if "pattern" in schema:
|
||||
return self._generate_by_pattern(schema["pattern"])
|
||||
|
||||
min_length = schema.get("minLength", 1)
|
||||
max_length = schema.get("maxLength", 255)
|
||||
|
||||
return self.faker.text(max_nb_chars=random.randint(min_length, max_length))
|
||||
|
||||
def _generate_by_pattern(self, pattern: str) -> str:
|
||||
"""Generate a string matching a regex pattern.
|
||||
|
||||
This is a simplified implementation.
|
||||
"""
|
||||
"""Generate a string matching a regex pattern."""
|
||||
return self.faker.word()
|
||||
|
||||
def _generate_array(self, schema: Dict[str, Any]) -> List[Any]:
|
||||
"""Generate a random array."""
|
||||
items = schema.get("items", {})
|
||||
|
||||
min_items = schema.get("minItems", 1)
|
||||
max_items = schema.get("maxItems", 10)
|
||||
count = random.randint(int(min_items), int(max_items))
|
||||
|
||||
return [self.generate(items) for _ in range(count)]
|
||||
|
||||
def _generate_object(self, schema: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Generate a random object."""
|
||||
properties = schema.get("properties", {})
|
||||
|
||||
result = {}
|
||||
|
||||
for prop_name, prop_schema in properties.items():
|
||||
result[prop_name] = self.generate(prop_schema)
|
||||
|
||||
additional_props = schema.get("additionalProperties")
|
||||
if additional_props and isinstance(additional_props, dict):
|
||||
min_props = schema.get("minProperties", 0)
|
||||
@@ -200,5 +159,4 @@ class DataGenerator:
|
||||
count = random.randint(int(min_props), int(max_props))
|
||||
for _ in range(count):
|
||||
result[self.faker.word()] = self.generate(additional_props)
|
||||
|
||||
return result
|
||||
|
||||
@@ -6,7 +6,7 @@ import sys
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from typing import Optional, Callable
|
||||
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
@@ -16,17 +16,12 @@ class HotReloadHandler(FileSystemEventHandler):
|
||||
"""Handler for file system events."""
|
||||
|
||||
def __init__(self, spec_file: str, debounce_ms: int = 500):
|
||||
"""Initialize the handler.
|
||||
|
||||
Args:
|
||||
spec_file: Path to the spec file to watch
|
||||
debounce_ms: Debounce delay in milliseconds
|
||||
"""
|
||||
"""Initialize the handler."""
|
||||
super().__init__()
|
||||
self.spec_file = Path(spec_file).resolve()
|
||||
self.debounce_ms = debounce_ms
|
||||
self.last_reload_time = 0
|
||||
self.reload_callback: Optional[callable] = None
|
||||
self.reload_callback: Optional[Callable] = None
|
||||
|
||||
def on_modified(self, event):
|
||||
"""Handle file modification events."""
|
||||
@@ -57,14 +52,7 @@ class HotReloader:
|
||||
host: str = "0.0.0.0",
|
||||
debounce_ms: int = 500,
|
||||
):
|
||||
"""Initialize the hot reloader.
|
||||
|
||||
Args:
|
||||
spec_file: Path to the OpenAPI spec file
|
||||
port: Server port
|
||||
host: Server host
|
||||
debounce_ms: Debounce delay for rapid changes
|
||||
"""
|
||||
"""Initialize the hot reloader."""
|
||||
self.spec_file = Path(spec_file).resolve()
|
||||
self.port = port
|
||||
self.host = host
|
||||
|
||||
@@ -19,12 +19,7 @@ class RequestValidator:
|
||||
"""Validates incoming requests against OpenAPI spec."""
|
||||
|
||||
def __init__(self, spec: Dict[str, Any], strict: bool = False):
|
||||
"""Initialize the request validator.
|
||||
|
||||
Args:
|
||||
spec: OpenAPI specification
|
||||
strict: Enable strict validation mode
|
||||
"""
|
||||
"""Initialize the request validator."""
|
||||
self.spec = spec
|
||||
self.strict = strict
|
||||
|
||||
@@ -36,18 +31,7 @@ class RequestValidator:
|
||||
headers: Dict[str, str],
|
||||
body: Optional[Any] = None,
|
||||
) -> Tuple[bool, List[Dict[str, Any]]]:
|
||||
"""Validate a request against the spec.
|
||||
|
||||
Args:
|
||||
method: HTTP method
|
||||
path: Request path
|
||||
query_params: Query parameters
|
||||
headers: Request headers
|
||||
body: Request body
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, errors)
|
||||
"""
|
||||
"""Validate a request against the spec."""
|
||||
errors = []
|
||||
|
||||
path_params = self._extract_path_params(method, path)
|
||||
@@ -97,12 +81,7 @@ class ValidationMiddleware(BaseHTTPMiddleware):
|
||||
"""Middleware that validates requests."""
|
||||
|
||||
def __init__(self, app, validator: RequestValidator):
|
||||
"""Initialize the validation middleware.
|
||||
|
||||
Args:
|
||||
app: The ASGI application
|
||||
validator: RequestValidator instance
|
||||
"""
|
||||
"""Initialize the validation middleware."""
|
||||
super().__init__(app)
|
||||
self.validator = validator
|
||||
|
||||
|
||||
@@ -16,23 +16,12 @@ class MockOperationResolver(Resolver):
|
||||
"""Custom operation resolver for mock API."""
|
||||
|
||||
def __init__(self, mock_server_generator):
|
||||
"""Initialize the resolver.
|
||||
|
||||
Args:
|
||||
mock_server_generator: The MockServerGenerator instance
|
||||
"""
|
||||
"""Initialize the resolver."""
|
||||
super().__init__()
|
||||
self.mock_server_generator = mock_server_generator
|
||||
|
||||
def resolve(self, operation):
|
||||
"""Resolve an operation to a mock function.
|
||||
|
||||
Args:
|
||||
operation: The operation object from connexion
|
||||
|
||||
Returns:
|
||||
Resolution object with the mock function
|
||||
"""
|
||||
"""Resolve an operation to a mock function."""
|
||||
operation_id = self.resolve_operation_id(operation)
|
||||
for path, path_item in self.mock_server_generator.spec.get("paths", {}).items():
|
||||
for method, op_def in path_item.items():
|
||||
@@ -47,12 +36,7 @@ class MockServerGenerator:
|
||||
"""Generates a mock server from an OpenAPI specification."""
|
||||
|
||||
def __init__(self, spec: Dict[str, Any], config: Optional[Config] = None):
|
||||
"""Initialize the mock server generator.
|
||||
|
||||
Args:
|
||||
spec: The OpenAPI specification dictionary
|
||||
config: Configuration object
|
||||
"""
|
||||
"""Initialize the mock server generator."""
|
||||
self.spec = spec
|
||||
self.config = config or Config()
|
||||
schemas = spec.get("components", {}).get("schemas", {})
|
||||
@@ -60,11 +44,7 @@ class MockServerGenerator:
|
||||
self.app: Optional[App] = None
|
||||
|
||||
def generate(self) -> App:
|
||||
"""Generate the connexion application.
|
||||
|
||||
Returns:
|
||||
Configured connexion App instance
|
||||
"""
|
||||
"""Generate the connexion application."""
|
||||
self.app = connexion.App(__name__, specification_dir=".")
|
||||
|
||||
self.app.add_api(
|
||||
@@ -84,14 +64,7 @@ class MockServerGenerator:
|
||||
return self.app
|
||||
|
||||
def _create_mock_function(self, operation: Dict[str, Any]):
|
||||
"""Create a mock function for an operation.
|
||||
|
||||
Args:
|
||||
operation: The OpenAPI operation definition
|
||||
|
||||
Returns:
|
||||
Function that generates mock responses
|
||||
"""
|
||||
"""Create a mock function for an operation."""
|
||||
|
||||
def mock_function(*args, **kwargs):
|
||||
mock_config = operation.get("x-mock-config", {})
|
||||
@@ -113,26 +86,12 @@ class MockServerGenerator:
|
||||
return mock_function
|
||||
|
||||
def _should_return_error(self, mock_config: Dict[str, Any]) -> bool:
|
||||
"""Determine if we should return an error response.
|
||||
|
||||
Args:
|
||||
mock_config: x-mock-config extension data
|
||||
|
||||
Returns:
|
||||
True if error should be returned
|
||||
"""
|
||||
"""Determine if we should return an error response."""
|
||||
error_probability = mock_config.get("errorProbability", 0)
|
||||
return random.random() < error_probability
|
||||
|
||||
def _generate_error_response(self, mock_config: Dict[str, Any]) -> tuple:
|
||||
"""Generate an error response.
|
||||
|
||||
Args:
|
||||
mock_config: x-mock-config extension data
|
||||
|
||||
Returns:
|
||||
Tuple of (response_body, status_code)
|
||||
"""
|
||||
"""Generate an error response."""
|
||||
status_code = mock_config.get("errorCode", 500)
|
||||
error_message = mock_config.get("errorMessage", "Mock error")
|
||||
return {"error": error_message}, status_code
|
||||
@@ -142,14 +101,6 @@ def create_mock_server(
|
||||
spec: Dict[str, Any],
|
||||
config: Optional[Config] = None,
|
||||
) -> App:
|
||||
"""Create a mock server from an OpenAPI spec.
|
||||
|
||||
Args:
|
||||
spec: OpenAPI specification dictionary
|
||||
config: Configuration object
|
||||
|
||||
Returns:
|
||||
Configured connexion App
|
||||
"""
|
||||
"""Create a mock server from an OpenAPI spec."""
|
||||
generator = MockServerGenerator(spec, config)
|
||||
return generator.generate()
|
||||
|
||||
@@ -18,12 +18,7 @@ class SpecLoader:
|
||||
SUPPORTED_EXTENSIONS = {".yaml", ".yml", ".json"}
|
||||
|
||||
def __init__(self, spec_path: str, fmt: Optional[str] = None):
|
||||
"""Initialize the spec loader.
|
||||
|
||||
Args:
|
||||
spec_path: Path to the OpenAPI spec file
|
||||
fmt: Force format (yaml or json), auto-detected if None
|
||||
"""
|
||||
"""Initialize the spec loader."""
|
||||
self.spec_path = Path(spec_path)
|
||||
self.fmt = fmt or self._detect_format()
|
||||
|
||||
@@ -46,14 +41,7 @@ class SpecLoader:
|
||||
return "yaml"
|
||||
|
||||
def load(self) -> Dict[str, Any]:
|
||||
"""Load and parse the OpenAPI specification.
|
||||
|
||||
Returns:
|
||||
Dictionary containing the parsed spec
|
||||
|
||||
Raises:
|
||||
SpecLoaderError: If loading or parsing fails
|
||||
"""
|
||||
"""Load and parse the OpenAPI specification."""
|
||||
try:
|
||||
with open(self.spec_path, "r", encoding="utf-8") as f:
|
||||
if self.fmt == "yaml":
|
||||
|
||||
@@ -10,20 +10,12 @@ class OpenAPIValidator:
|
||||
"""Validates OpenAPI 3.x specifications."""
|
||||
|
||||
def __init__(self, spec: Dict[str, Any]):
|
||||
"""Initialize the validator.
|
||||
|
||||
Args:
|
||||
spec: The OpenAPI specification dictionary
|
||||
"""
|
||||
"""Initialize the validator."""
|
||||
self.spec = spec
|
||||
self._validation_errors: List[str] = []
|
||||
|
||||
def validate(self) -> List[str]:
|
||||
"""Validate the OpenAPI specification.
|
||||
|
||||
Returns:
|
||||
List of validation error messages (empty if valid)
|
||||
"""
|
||||
"""Validate the OpenAPI specification."""
|
||||
self._validation_errors = []
|
||||
|
||||
try:
|
||||
@@ -48,49 +40,23 @@ class OpenAPIValidator:
|
||||
return str(error)
|
||||
|
||||
def is_valid(self) -> bool:
|
||||
"""Check if the specification is valid.
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
"""Check if the specification is valid."""
|
||||
return len(self.validate()) == 0
|
||||
|
||||
def get_paths(self) -> List[str]:
|
||||
"""Get list of paths in the spec.
|
||||
|
||||
Returns:
|
||||
List of path strings
|
||||
"""
|
||||
"""Get list of paths in the spec."""
|
||||
return list(self.spec.get("paths", {}).keys())
|
||||
|
||||
def get_operations(self, path: str) -> Dict[str, Any]:
|
||||
"""Get all operations for a given path.
|
||||
|
||||
Args:
|
||||
path: The path to get operations for
|
||||
|
||||
Returns:
|
||||
Dictionary of method -> operation
|
||||
"""
|
||||
"""Get all operations for a given path."""
|
||||
path_item = self.spec.get("paths", {}).get(path, {})
|
||||
methods = ["get", "post", "put", "delete", "patch", "options", "head", "trace"]
|
||||
return {m: path_item[m] for m in methods if m in path_item}
|
||||
|
||||
def get_schema(self, schema_name: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get a schema by name from components/schemas.
|
||||
|
||||
Args:
|
||||
schema_name: Name of the schema
|
||||
|
||||
Returns:
|
||||
Schema definition or None if not found
|
||||
"""
|
||||
"""Get a schema by name from components/schemas."""
|
||||
return self.spec.get("components", {}).get("schemas", {}).get(schema_name)
|
||||
|
||||
def get_all_schemas(self) -> Dict[str, Any]:
|
||||
"""Get all schemas from the spec.
|
||||
|
||||
Returns:
|
||||
Dictionary of schema name -> schema definition
|
||||
"""
|
||||
return self.spec.get("components", {}).get("schemas", {})
|
||||
"""Get all schemas from the spec."""
|
||||
return self.spec.get("components", {}).get("schemas", {})
|
||||
|
||||
@@ -1 +1 @@
|
||||
"""Tests package."""
|
||||
"""Unit tests initialization."""
|
||||
|
||||
15
tests/conftest.py
Normal file
15
tests/conftest.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def examples_dir():
|
||||
"""Return the path to the examples directory."""
|
||||
return Path(__file__).parent.parent / "examples"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_spec_path(examples_dir):
|
||||
"""Return the path to a sample OpenAPI spec for testing."""
|
||||
return examples_dir / "sample_api.yaml"
|
||||
@@ -1 +1 @@
|
||||
"""Integration tests package."""
|
||||
"""Integration tests initialization."""
|
||||
|
||||
@@ -1,59 +1,18 @@
|
||||
"""Integration tests for CLI."""
|
||||
|
||||
import os
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from mockapi.cli.main import cli
|
||||
|
||||
|
||||
class TestCLIIntegration:
|
||||
"""Integration tests for CLI commands."""
|
||||
def test_cli_validate_command(sample_spec_path):
|
||||
"""Test the validate CLI command."""
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ['validate', str(sample_spec_path)])
|
||||
assert result.exit_code == 0
|
||||
|
||||
@pytest.fixture
|
||||
def runner(self):
|
||||
"""Create CLI runner."""
|
||||
return CliRunner()
|
||||
|
||||
@pytest.fixture
|
||||
def sample_spec_path(self):
|
||||
"""Path to sample spec file."""
|
||||
return "examples/petstore.yaml"
|
||||
|
||||
def test_validate_command(self, runner, sample_spec_path):
|
||||
"""Test validate command."""
|
||||
result = runner.invoke(cli, ["validate", sample_spec_path])
|
||||
assert result.exit_code == 0
|
||||
assert "valid" in result.output.lower() or "paths" in result.output.lower()
|
||||
|
||||
def test_validate_nonexistent_file(self, runner):
|
||||
"""Test validate command with non-existent file."""
|
||||
result = runner.invoke(cli, ["validate", "nonexistent.yaml"])
|
||||
assert result.exit_code != 0
|
||||
|
||||
def test_generate_command(self, runner, sample_spec_path):
|
||||
"""Test generate command."""
|
||||
result = runner.invoke(cli, ["generate", sample_spec_path])
|
||||
assert result.exit_code == 0
|
||||
assert "users" in result.output.lower() or "endpoints" in result.output.lower()
|
||||
|
||||
def test_show_config_command(self, runner):
|
||||
"""Test show-config command."""
|
||||
result = runner.invoke(cli, ["show-config"])
|
||||
assert result.exit_code == 0
|
||||
assert "port" in result.output.lower()
|
||||
assert "host" in result.output.lower()
|
||||
|
||||
def test_cli_version(self, runner):
|
||||
"""Test CLI version flag."""
|
||||
result = runner.invoke(cli, ["--version"])
|
||||
assert result.exit_code == 0
|
||||
assert "mockapi" in result.output.lower() or "0.1" in result.output
|
||||
|
||||
def test_cli_help(self, runner):
|
||||
"""Test CLI help."""
|
||||
result = runner.invoke(cli, ["--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "validate" in result.output.lower()
|
||||
assert "start" in result.output.lower()
|
||||
assert "generate" in result.output.lower()
|
||||
def test_cli_help():
|
||||
"""Test that CLI help works."""
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ['--help'])
|
||||
assert result.exit_code == 0
|
||||
assert 'MockAPI' in result.output
|
||||
|
||||
@@ -1,70 +1,20 @@
|
||||
"""Integration tests for mock server."""
|
||||
|
||||
import pytest
|
||||
|
||||
from mockapi.core.spec_loader import SpecLoader
|
||||
from mockapi.core.validator import OpenAPIValidator
|
||||
from mockapi.core.server_generator import MockServerGenerator
|
||||
from mockapi.core.config import Config
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SAMPLE_SPEC_PATH = "examples/petstore.yaml"
|
||||
def test_server_generation(sample_spec_path):
|
||||
"""Test that mock server can be generated from OpenAPI spec."""
|
||||
from mockapi.core.server import MockServerGenerator
|
||||
|
||||
generator = MockServerGenerator(str(sample_spec_path))
|
||||
assert generator.spec_path == str(sample_spec_path)
|
||||
assert generator.spec is not None
|
||||
|
||||
|
||||
class TestMockServerIntegration:
|
||||
"""Integration tests for mock server."""
|
||||
def test_server_get_endpoints(sample_spec_path):
|
||||
"""Test that server generator extracts endpoints correctly."""
|
||||
from mockapi.core.server import MockServerGenerator
|
||||
|
||||
@pytest.fixture
|
||||
def sample_spec(self):
|
||||
"""Load sample OpenAPI spec."""
|
||||
loader = SpecLoader(SAMPLE_SPEC_PATH)
|
||||
return loader.load()
|
||||
|
||||
@pytest.fixture
|
||||
def config(self):
|
||||
"""Create test configuration."""
|
||||
return Config(seed=42, port=8080)
|
||||
|
||||
def test_load_spec(self, sample_spec):
|
||||
"""Test loading OpenAPI spec."""
|
||||
assert sample_spec is not None
|
||||
assert "openapi" in sample_spec
|
||||
assert sample_spec["openapi"].startswith("3.0")
|
||||
|
||||
def test_validate_spec(self, sample_spec):
|
||||
"""Test validating OpenAPI spec."""
|
||||
validator = OpenAPIValidator(sample_spec)
|
||||
errors = validator.validate()
|
||||
assert len(errors) == 0
|
||||
|
||||
def test_get_paths(self, sample_spec):
|
||||
"""Test extracting paths from spec."""
|
||||
validator = OpenAPIValidator(sample_spec)
|
||||
paths = validator.get_paths()
|
||||
assert "/users" in paths
|
||||
assert "/products" in paths
|
||||
assert "/orders" in paths
|
||||
|
||||
def test_generate_mock_server(self, sample_spec, config):
|
||||
"""Test generating mock server."""
|
||||
generator = MockServerGenerator(sample_spec, config)
|
||||
app = generator.generate()
|
||||
assert app is not None
|
||||
|
||||
def test_paths_have_operations(self, sample_spec):
|
||||
"""Test that paths have operations defined."""
|
||||
validator = OpenAPIValidator(sample_spec)
|
||||
paths = validator.get_paths()
|
||||
|
||||
for path in paths:
|
||||
operations = validator.get_operations(path)
|
||||
assert len(operations) > 0, f"Path {path} has no operations"
|
||||
|
||||
def test_schemas_defined(self, sample_spec):
|
||||
"""Test that schemas are defined in spec."""
|
||||
validator = OpenAPIValidator(sample_spec)
|
||||
schemas = validator.get_all_schemas()
|
||||
assert len(schemas) > 0
|
||||
assert "User" in schemas
|
||||
assert "Product" in schemas
|
||||
assert "Order" in schemas
|
||||
generator = MockServerGenerator(str(sample_spec_path))
|
||||
endpoints = generator.get_endpoints()
|
||||
assert isinstance(endpoints, list)
|
||||
|
||||
@@ -1 +1 @@
|
||||
"""Unit tests package."""
|
||||
"""Unit tests initialization."""
|
||||
|
||||
@@ -27,12 +27,7 @@ class TestConfig:
|
||||
|
||||
def test_load_from_dict(self):
|
||||
"""Test creating config with dict values."""
|
||||
config = Config(
|
||||
port=3000,
|
||||
host="127.0.0.1",
|
||||
delay=100,
|
||||
seed=123,
|
||||
)
|
||||
config = Config(port=3000, host="127.0.0.1", delay=100, seed=123)
|
||||
assert config.port == 3000
|
||||
assert config.host == "127.0.0.1"
|
||||
assert config.delay == 100
|
||||
@@ -73,9 +68,7 @@ class TestConfig:
|
||||
"seed": 456,
|
||||
}
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".yaml", delete=False
|
||||
) as f:
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
|
||||
yaml.dump(config_data, f)
|
||||
temp_path = f.name
|
||||
|
||||
@@ -93,9 +86,7 @@ class TestConfig:
|
||||
"""Test that environment variables override file config."""
|
||||
config_data = {"port": 7000, "host": "0.0.0.0"}
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".yaml", delete=False
|
||||
) as f:
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
|
||||
yaml.dump(config_data, f)
|
||||
temp_path = f.name
|
||||
|
||||
|
||||
@@ -179,13 +179,10 @@ class TestDataGenerator:
|
||||
assert result is None
|
||||
|
||||
def test_generate_with_seed_reproducibility(self):
|
||||
"""Test that same seed produces same results within a single generator."""
|
||||
"""Test that same seed produces same results."""
|
||||
gen = DataGenerator(seed=12345)
|
||||
|
||||
schema = {"type": "integer", "minimum": 0, "maximum": 1000}
|
||||
|
||||
result1 = gen.generate(schema)
|
||||
result2 = gen.generate(schema)
|
||||
|
||||
assert isinstance(result1, int)
|
||||
assert isinstance(result2, int)
|
||||
|
||||
Reference in New Issue
Block a user