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:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main, master ]
|
branches: [main]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main, master ]
|
branches: [main]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
python-version: '3.11'
|
||||||
|
- run: pip install -e ".[dev]"
|
||||||
- name: Install dependencies
|
- run: pytest tests/unit -v
|
||||||
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/
|
|
||||||
|
|||||||
62
.gitignore
vendored
62
.gitignore
vendored
@@ -3,9 +3,6 @@ __pycache__/
|
|||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
# Distribution / packaging
|
||||||
.Python
|
.Python
|
||||||
build/
|
build/
|
||||||
@@ -35,14 +32,12 @@ pip-delete-this-directory.txt
|
|||||||
# Unit test / coverage reports
|
# Unit test / coverage reports
|
||||||
htmlcov/
|
htmlcov/
|
||||||
.tox/
|
.tox/
|
||||||
.nox/
|
|
||||||
.coverage
|
.coverage
|
||||||
.coverage.*
|
.coverage.*
|
||||||
.cache
|
.cache
|
||||||
nosetests.xml
|
nosetests.xml
|
||||||
coverage.xml
|
coverage.xml
|
||||||
*.cover
|
*.cover
|
||||||
*.py,cover
|
|
||||||
.hypothesis/
|
.hypothesis/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
|
||||||
@@ -50,6 +45,35 @@ coverage.xml
|
|||||||
*.mo
|
*.mo
|
||||||
*.pot
|
*.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
|
# Environments
|
||||||
.env
|
.env
|
||||||
.venv
|
.venv
|
||||||
@@ -59,19 +83,25 @@ ENV/
|
|||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
|
||||||
|
# ruff
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
|
*~
|
||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Project specific
|
|
||||||
mockapi.yaml
|
|
||||||
mockapi.yml
|
|
||||||
|
|
||||||
# Examples
|
|
||||||
examples/
|
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -14,7 +14,7 @@ copies or substantial portions of the Software.
|
|||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
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
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
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
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
|||||||
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
|
## Features
|
||||||
|
|
||||||
- **OpenAPI 3.x Support**: Parse and validate OpenAPI specifications
|
- Generate mock server from OpenAPI 3.x specs
|
||||||
- **Mock Server Generation**: Generate working mock servers using connexion
|
- Configurable response delays and error simulation
|
||||||
- **Random Data Generation**: Create realistic test data from JSON schemas using Faker
|
- Random realistic test data generation from schemas
|
||||||
- **Configurable Response Delays**: Simulate network latency with fixed or random delays
|
- Request validation against OpenAPI spec
|
||||||
- **Error Simulation**: Simulate HTTP error responses for testing
|
- Hot-reload on spec changes
|
||||||
- **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`
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -19,140 +16,28 @@ OpenAPI Mock Server Generator - Generate functional mock APIs from OpenAPI 3.x s
|
|||||||
pip install -e .
|
pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
Or install from source:
|
## Usage
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://7000pct.gitea.bloupla.net/7000pctAUTO/mockapi.git
|
|
||||||
cd mockapi
|
|
||||||
pip install -e ".[dev]"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Validate an OpenAPI spec
|
# Validate an OpenAPI spec
|
||||||
mockapi validate examples/petstore.yaml
|
mockapi validate spec.yaml
|
||||||
|
|
||||||
# Start a mock server
|
# Start a mock server
|
||||||
mockapi start examples/petstore.yaml
|
mockapi start spec.yaml
|
||||||
|
|
||||||
# Generate a summary
|
# Show configuration
|
||||||
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
|
|
||||||
mockapi show-config
|
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
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install development dependencies
|
# Install dev dependencies
|
||||||
pip install -e ".[dev]"
|
pip install -e ".[dev]"
|
||||||
|
|
||||||
# Run tests
|
# Run tests
|
||||||
pytest tests/ -v
|
pytest tests/ -v
|
||||||
|
|
||||||
# Run linting
|
# Build
|
||||||
ruff check src/
|
python -m build
|
||||||
|
|
||||||
# Type checking
|
|
||||||
mypy src/mockapi/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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 = [
|
classifiers = [
|
||||||
"Development Status :: 3 - Alpha",
|
"Development Status :: 3 - Alpha",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"License :: OSI Approved :: MIT License",
|
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
@@ -31,7 +30,6 @@ dependencies = [
|
|||||||
"uvicorn>=0.24.0",
|
"uvicorn>=0.24.0",
|
||||||
"watchdog>=3.0.0",
|
"watchdog>=3.0.0",
|
||||||
"pyyaml>=6.0.1",
|
"pyyaml>=6.0.1",
|
||||||
"starlette>=0.27.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
@@ -40,8 +38,6 @@ dev = [
|
|||||||
"pytest-cov>=4.1.0",
|
"pytest-cov>=4.1.0",
|
||||||
"pytest-asyncio>=0.21.0",
|
"pytest-asyncio>=0.21.0",
|
||||||
"httpx>=0.25.0",
|
"httpx>=0.25.0",
|
||||||
"ruff>=0.1.0",
|
|
||||||
"mypy>=1.7.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[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)
|
click.echo(f" - {error}", err=True)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
click.echo("✓ Specification is valid!")
|
click.echo("\u2713 Specification is valid!")
|
||||||
click.echo(f" Paths: {len(spec.get('paths', {}))}")
|
click.echo(f" Paths: {len(spec.get('paths', {}))}")
|
||||||
click.echo(f" Schemas: {len(spec.get('components', {}).get('schemas', {}))}")
|
click.echo(f" Schemas: {len(spec.get('components', {}).get('schemas', {}))}")
|
||||||
|
|
||||||
@@ -62,53 +62,13 @@ def validate(spec_file: str, fmt: Optional[str]):
|
|||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.argument("spec_file", type=click.Path(exists=True))
|
@click.argument("spec_file", type=click.Path(exists=True))
|
||||||
@click.option(
|
@click.option("--port", "-p", type=int, default=None, help="Port to run the mock server on")
|
||||||
"--port",
|
@click.option("--host", "-h", default=None, help="Host to bind to")
|
||||||
"-p",
|
@click.option("--delay", "-d", type=int, default=None, help="Fixed response delay in milliseconds")
|
||||||
type=int,
|
@click.option("--random-delay", is_flag=True, default=None, help="Use random delays")
|
||||||
default=None,
|
@click.option("--config", "-c", type=click.Path(exists=True), default=None, help="Path to mockapi.yaml")
|
||||||
help="Port to run the mock server on (default: from config or 8080)",
|
@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")
|
||||||
@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",
|
|
||||||
)
|
|
||||||
def start(
|
def start(
|
||||||
spec_file: str,
|
spec_file: str,
|
||||||
port: Optional[int],
|
port: Optional[int],
|
||||||
@@ -119,10 +79,7 @@ def start(
|
|||||||
watch: bool,
|
watch: bool,
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
):
|
):
|
||||||
"""Start a mock API server from an OpenAPI specification.
|
"""Start a mock API server from an OpenAPI specification."""
|
||||||
|
|
||||||
SPEC_FILE: Path to the OpenAPI spec file (YAML or JSON)
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
cfg = Config.load(config_path=config)
|
cfg = Config.load(config_path=config)
|
||||||
|
|
||||||
@@ -148,7 +105,6 @@ def start(
|
|||||||
|
|
||||||
if verbose:
|
if verbose:
|
||||||
click.echo(f"Starting mock server on {cfg.host}:{cfg.port}")
|
click.echo(f"Starting mock server on {cfg.host}:{cfg.port}")
|
||||||
click.echo(f"Spec file: {spec_file}")
|
|
||||||
|
|
||||||
generator = MockServerGenerator(spec, cfg)
|
generator = MockServerGenerator(spec, cfg)
|
||||||
app = generator.generate()
|
app = generator.generate()
|
||||||
@@ -159,12 +115,7 @@ def start(
|
|||||||
reloader.start_watching()
|
reloader.start_watching()
|
||||||
else:
|
else:
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(
|
uvicorn.run(app, host=cfg.host, port=cfg.port, log_level="info" if verbose else "warning")
|
||||||
app,
|
|
||||||
host=cfg.host,
|
|
||||||
port=cfg.port,
|
|
||||||
log_level="info" if verbose else "warning",
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
click.echo(f"Error: {e}", err=True)
|
click.echo(f"Error: {e}", err=True)
|
||||||
@@ -176,18 +127,9 @@ def start(
|
|||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.argument("spec_file", type=click.Path(exists=True))
|
@click.argument("spec_file", type=click.Path(exists=True))
|
||||||
@click.option(
|
@click.option("--output", "-o", type=click.Path(), default=None, help="Output file path")
|
||||||
"--output",
|
|
||||||
"-o",
|
|
||||||
type=click.Path(),
|
|
||||||
default=None,
|
|
||||||
help="Output file path (default: stdout)",
|
|
||||||
)
|
|
||||||
def generate(spec_file: str, output: Optional[str]):
|
def generate(spec_file: str, output: Optional[str]):
|
||||||
"""Generate code/structure from an OpenAPI spec (dry-run mode).
|
"""Generate code from an OpenAPI spec (dry-run mode)."""
|
||||||
|
|
||||||
SPEC_FILE: Path to the OpenAPI spec file (YAML or JSON)
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
loader = SpecLoader(spec_file)
|
loader = SpecLoader(spec_file)
|
||||||
spec = loader.load()
|
spec = loader.load()
|
||||||
@@ -238,15 +180,9 @@ def generate(spec_file: str, output: Optional[str]):
|
|||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option(
|
@click.option("--config", "-c", type=click.Path(exists=True), default=None, help="Path to mockapi.yaml")
|
||||||
"--config",
|
|
||||||
"-c",
|
|
||||||
type=click.Path(exists=True),
|
|
||||||
default=None,
|
|
||||||
help="Path to mockapi.yaml configuration file",
|
|
||||||
)
|
|
||||||
def show_config(config: Optional[str]):
|
def show_config(config: Optional[str]):
|
||||||
"""Show the current configuration (from file and defaults)."""
|
"""Show the current configuration."""
|
||||||
try:
|
try:
|
||||||
cfg = Config.load(config_path=config)
|
cfg = Config.load(config_path=config)
|
||||||
click.echo("Current MockAPI Configuration:")
|
click.echo("Current MockAPI Configuration:")
|
||||||
|
|||||||
@@ -22,19 +22,11 @@ class Config:
|
|||||||
strict_validation: bool = False
|
strict_validation: bool = False
|
||||||
error_probability: float = 0.0
|
error_probability: float = 0.0
|
||||||
error_code: int = 500
|
error_code: int = 500
|
||||||
|
|
||||||
config_path: Optional[str] = None
|
config_path: Optional[str] = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, config_path: Optional[str] = None) -> "Config":
|
def load(cls, config_path: Optional[str] = None) -> "Config":
|
||||||
"""Load configuration from file and environment.
|
"""Load configuration from file and environment."""
|
||||||
|
|
||||||
Args:
|
|
||||||
config_path: Path to mockapi.yaml config file
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Config instance with loaded values
|
|
||||||
"""
|
|
||||||
config = cls()
|
config = cls()
|
||||||
|
|
||||||
if config_path:
|
if config_path:
|
||||||
@@ -62,11 +54,7 @@ class Config:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _load_from_env(cls) -> Dict[str, Any]:
|
def _load_from_env(cls) -> Dict[str, Any]:
|
||||||
"""Load configuration from environment variables.
|
"""Load configuration from environment variables."""
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary of configuration values
|
|
||||||
"""
|
|
||||||
config = {}
|
config = {}
|
||||||
|
|
||||||
if port := os.environ.get("MOCKAPI_PORT"):
|
if port := os.environ.get("MOCKAPI_PORT"):
|
||||||
@@ -88,14 +76,7 @@ class Config:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _load_from_file(cls, config_path: str) -> Dict[str, Any]:
|
def _load_from_file(cls, config_path: str) -> Dict[str, Any]:
|
||||||
"""Load configuration from YAML file.
|
"""Load configuration from YAML file."""
|
||||||
|
|
||||||
Args:
|
|
||||||
config_path: Path to the config file
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary of configuration values
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
with open(config_path, "r") as f:
|
with open(config_path, "r") as f:
|
||||||
data = yaml.safe_load(f) or {}
|
data = yaml.safe_load(f) or {}
|
||||||
@@ -129,11 +110,7 @@ class Config:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
"""Convert config to dictionary.
|
"""Convert config to dictionary."""
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary representation of config
|
|
||||||
"""
|
|
||||||
return {
|
return {
|
||||||
"port": self.port,
|
"port": self.port,
|
||||||
"host": self.host,
|
"host": self.host,
|
||||||
@@ -149,12 +126,5 @@ class Config:
|
|||||||
|
|
||||||
|
|
||||||
def load_config(config_path: Optional[str] = None) -> Config:
|
def load_config(config_path: Optional[str] = None) -> Config:
|
||||||
"""Load configuration from file and environment.
|
"""Load configuration from file and environment."""
|
||||||
|
|
||||||
Args:
|
|
||||||
config_path: Optional path to config file
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Config instance
|
|
||||||
"""
|
|
||||||
return Config.load(config_path)
|
return Config.load(config_path)
|
||||||
|
|||||||
@@ -20,15 +20,7 @@ class DelayMiddleware(BaseHTTPMiddleware):
|
|||||||
min_delay: int = 100,
|
min_delay: int = 100,
|
||||||
max_delay: int = 2000,
|
max_delay: int = 2000,
|
||||||
):
|
):
|
||||||
"""Initialize the delay middleware.
|
"""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)
|
|
||||||
"""
|
|
||||||
super().__init__(app)
|
super().__init__(app)
|
||||||
self.delay = delay
|
self.delay = delay
|
||||||
self.random_delay = random_delay
|
self.random_delay = random_delay
|
||||||
@@ -50,16 +42,7 @@ class DelayMiddleware(BaseHTTPMiddleware):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def wrap(app, delay: int = 0, random_delay: bool = False) -> "DelayMiddleware":
|
def wrap(app, delay: int = 0, random_delay: bool = False) -> "DelayMiddleware":
|
||||||
"""Wrap an app with delay middleware.
|
"""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
|
|
||||||
"""
|
|
||||||
return DelayMiddleware(app, delay=delay, random_delay=random_delay)
|
return DelayMiddleware(app, delay=delay, random_delay=random_delay)
|
||||||
|
|
||||||
|
|
||||||
@@ -71,21 +54,12 @@ class ErrorSimulator:
|
|||||||
error_probability: float = 0.0,
|
error_probability: float = 0.0,
|
||||||
default_error_code: int = 500,
|
default_error_code: int = 500,
|
||||||
):
|
):
|
||||||
"""Initialize the error simulator.
|
"""Initialize the error simulator."""
|
||||||
|
|
||||||
Args:
|
|
||||||
error_probability: Probability of returning an error (0.0 to 1.0)
|
|
||||||
default_error_code: Default HTTP error code
|
|
||||||
"""
|
|
||||||
self.error_probability = error_probability
|
self.error_probability = error_probability
|
||||||
self.default_error_code = default_error_code
|
self.default_error_code = default_error_code
|
||||||
|
|
||||||
def should_return_error(self) -> bool:
|
def should_return_error(self) -> bool:
|
||||||
"""Determine if an error should be returned.
|
"""Determine if an error should be returned."""
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if error should be returned
|
|
||||||
"""
|
|
||||||
return random.random() < self.error_probability
|
return random.random() < self.error_probability
|
||||||
|
|
||||||
def get_error_response(
|
def get_error_response(
|
||||||
@@ -93,15 +67,7 @@ class ErrorSimulator:
|
|||||||
status_code: Optional[int] = None,
|
status_code: Optional[int] = None,
|
||||||
message: Optional[str] = None,
|
message: Optional[str] = None,
|
||||||
) -> tuple:
|
) -> tuple:
|
||||||
"""Get an error response tuple.
|
"""Get an error response tuple."""
|
||||||
|
|
||||||
Args:
|
|
||||||
status_code: HTTP status code
|
|
||||||
message: Error message
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple of (body_dict, status_code)
|
|
||||||
"""
|
|
||||||
return (
|
return (
|
||||||
{"error": message or "Simulated error"},
|
{"error": message or "Simulated error"},
|
||||||
status_code or self.default_error_code,
|
status_code or self.default_error_code,
|
||||||
|
|||||||
@@ -11,12 +11,7 @@ class DataGenerator:
|
|||||||
"""Generates realistic random test data from JSON schemas."""
|
"""Generates realistic random test data from JSON schemas."""
|
||||||
|
|
||||||
def __init__(self, seed: Optional[int] = None, schemas: Optional[Dict[str, Any]] = None):
|
def __init__(self, seed: Optional[int] = None, schemas: Optional[Dict[str, Any]] = None):
|
||||||
"""Initialize the data generator.
|
"""Initialize the data generator."""
|
||||||
|
|
||||||
Args:
|
|
||||||
seed: Random seed for reproducible data generation
|
|
||||||
schemas: Dictionary of schemas for $ref resolution
|
|
||||||
"""
|
|
||||||
self.seed = seed
|
self.seed = seed
|
||||||
self.faker = Faker()
|
self.faker = Faker()
|
||||||
if seed is not None:
|
if seed is not None:
|
||||||
@@ -46,8 +41,8 @@ class DataGenerator:
|
|||||||
"text": lambda: self.faker.text(),
|
"text": lambda: self.faker.text(),
|
||||||
"username": lambda: self.faker.user_name(),
|
"username": lambda: self.faker.user_name(),
|
||||||
"password": lambda: self.faker.password(),
|
"password": lambda: self.faker.password(),
|
||||||
"ip_v4": lambda: self.faker.ipv4(),
|
"ipv4": lambda: self.faker.ipv4(),
|
||||||
"ip_v6": lambda: self.faker.ipv6(),
|
"ipv6": lambda: self.faker.ipv6(),
|
||||||
"slug": lambda: self.faker.slug(),
|
"slug": lambda: self.faker.slug(),
|
||||||
"color": lambda: self.faker.color_name(),
|
"color": lambda: self.faker.color_name(),
|
||||||
"currency": lambda: self.faker.currency()[0],
|
"currency": lambda: self.faker.currency()[0],
|
||||||
@@ -55,14 +50,7 @@ class DataGenerator:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def generate(self, schema: Dict[str, Any]) -> Any:
|
def generate(self, schema: Dict[str, Any]) -> Any:
|
||||||
"""Generate data from a JSON schema.
|
"""Generate data from a JSON schema."""
|
||||||
|
|
||||||
Args:
|
|
||||||
schema: JSON schema definition
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Generated data matching the schema
|
|
||||||
"""
|
|
||||||
if not schema or not isinstance(schema, dict):
|
if not schema or not isinstance(schema, dict):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -89,16 +77,8 @@ class DataGenerator:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _resolve_ref(self, ref: str) -> Any:
|
def _resolve_ref(self, ref: str) -> Any:
|
||||||
"""Resolve a $ref reference.
|
"""Resolve a $ref reference."""
|
||||||
|
|
||||||
Args:
|
|
||||||
ref: Reference string like #/components/schemas/User
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Resolved schema or None
|
|
||||||
"""
|
|
||||||
parts = ref.lstrip("#/").split("/")
|
parts = ref.lstrip("#/").split("/")
|
||||||
|
|
||||||
skip_prefixes = ["components", "schemas"]
|
skip_prefixes = ["components", "schemas"]
|
||||||
start_idx = 0
|
start_idx = 0
|
||||||
for i, part in enumerate(parts):
|
for i, part in enumerate(parts):
|
||||||
@@ -106,23 +86,17 @@ class DataGenerator:
|
|||||||
start_idx = i + 1
|
start_idx = i + 1
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
parts = parts[start_idx:]
|
parts = parts[start_idx:]
|
||||||
|
|
||||||
if not parts:
|
if not parts:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
current = self._schemas_dict
|
current = self._schemas_dict
|
||||||
|
|
||||||
for part in parts:
|
for part in parts:
|
||||||
if isinstance(current, dict):
|
if isinstance(current, dict):
|
||||||
current = current.get(part, {})
|
current = current.get(part, {})
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if isinstance(current, dict):
|
if isinstance(current, dict):
|
||||||
return self.generate(current)
|
return self.generate(current)
|
||||||
|
|
||||||
return current
|
return current
|
||||||
|
|
||||||
def _generate_boolean(self, schema: Dict[str, Any]) -> bool:
|
def _generate_boolean(self, schema: Dict[str, Any]) -> bool:
|
||||||
@@ -135,7 +109,6 @@ class DataGenerator:
|
|||||||
"""Generate a random integer."""
|
"""Generate a random integer."""
|
||||||
if "enum" in schema:
|
if "enum" in schema:
|
||||||
return random.choice(schema["enum"])
|
return random.choice(schema["enum"])
|
||||||
|
|
||||||
minimum = schema.get("minimum", 0)
|
minimum = schema.get("minimum", 0)
|
||||||
maximum = schema.get("maximum", 10000)
|
maximum = schema.get("maximum", 10000)
|
||||||
return random.randint(int(minimum), int(maximum))
|
return random.randint(int(minimum), int(maximum))
|
||||||
@@ -144,7 +117,6 @@ class DataGenerator:
|
|||||||
"""Generate a random number."""
|
"""Generate a random number."""
|
||||||
if "enum" in schema:
|
if "enum" in schema:
|
||||||
return random.choice(schema["enum"])
|
return random.choice(schema["enum"])
|
||||||
|
|
||||||
minimum = schema.get("minimum", 0.0)
|
minimum = schema.get("minimum", 0.0)
|
||||||
maximum = schema.get("maximum", 10000.0)
|
maximum = schema.get("maximum", 10000.0)
|
||||||
return random.uniform(float(minimum), float(maximum))
|
return random.uniform(float(minimum), float(maximum))
|
||||||
@@ -153,46 +125,33 @@ class DataGenerator:
|
|||||||
"""Generate a random string."""
|
"""Generate a random string."""
|
||||||
if "enum" in schema:
|
if "enum" in schema:
|
||||||
return random.choice(schema["enum"])
|
return random.choice(schema["enum"])
|
||||||
|
|
||||||
format_type = schema.get("format", "")
|
format_type = schema.get("format", "")
|
||||||
|
|
||||||
if format_type in self._faker_providers:
|
if format_type in self._faker_providers:
|
||||||
return self._faker_providers[format_type]()
|
return self._faker_providers[format_type]()
|
||||||
|
|
||||||
if "pattern" in schema:
|
if "pattern" in schema:
|
||||||
return self._generate_by_pattern(schema["pattern"])
|
return self._generate_by_pattern(schema["pattern"])
|
||||||
|
|
||||||
min_length = schema.get("minLength", 1)
|
min_length = schema.get("minLength", 1)
|
||||||
max_length = schema.get("maxLength", 255)
|
max_length = schema.get("maxLength", 255)
|
||||||
|
|
||||||
return self.faker.text(max_nb_chars=random.randint(min_length, max_length))
|
return self.faker.text(max_nb_chars=random.randint(min_length, max_length))
|
||||||
|
|
||||||
def _generate_by_pattern(self, pattern: str) -> str:
|
def _generate_by_pattern(self, pattern: str) -> str:
|
||||||
"""Generate a string matching a regex pattern.
|
"""Generate a string matching a regex pattern."""
|
||||||
|
|
||||||
This is a simplified implementation.
|
|
||||||
"""
|
|
||||||
return self.faker.word()
|
return self.faker.word()
|
||||||
|
|
||||||
def _generate_array(self, schema: Dict[str, Any]) -> List[Any]:
|
def _generate_array(self, schema: Dict[str, Any]) -> List[Any]:
|
||||||
"""Generate a random array."""
|
"""Generate a random array."""
|
||||||
items = schema.get("items", {})
|
items = schema.get("items", {})
|
||||||
|
|
||||||
min_items = schema.get("minItems", 1)
|
min_items = schema.get("minItems", 1)
|
||||||
max_items = schema.get("maxItems", 10)
|
max_items = schema.get("maxItems", 10)
|
||||||
count = random.randint(int(min_items), int(max_items))
|
count = random.randint(int(min_items), int(max_items))
|
||||||
|
|
||||||
return [self.generate(items) for _ in range(count)]
|
return [self.generate(items) for _ in range(count)]
|
||||||
|
|
||||||
def _generate_object(self, schema: Dict[str, Any]) -> Dict[str, Any]:
|
def _generate_object(self, schema: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Generate a random object."""
|
"""Generate a random object."""
|
||||||
properties = schema.get("properties", {})
|
properties = schema.get("properties", {})
|
||||||
|
|
||||||
result = {}
|
result = {}
|
||||||
|
|
||||||
for prop_name, prop_schema in properties.items():
|
for prop_name, prop_schema in properties.items():
|
||||||
result[prop_name] = self.generate(prop_schema)
|
result[prop_name] = self.generate(prop_schema)
|
||||||
|
|
||||||
additional_props = schema.get("additionalProperties")
|
additional_props = schema.get("additionalProperties")
|
||||||
if additional_props and isinstance(additional_props, dict):
|
if additional_props and isinstance(additional_props, dict):
|
||||||
min_props = schema.get("minProperties", 0)
|
min_props = schema.get("minProperties", 0)
|
||||||
@@ -200,5 +159,4 @@ class DataGenerator:
|
|||||||
count = random.randint(int(min_props), int(max_props))
|
count = random.randint(int(min_props), int(max_props))
|
||||||
for _ in range(count):
|
for _ in range(count):
|
||||||
result[self.faker.word()] = self.generate(additional_props)
|
result[self.faker.word()] = self.generate(additional_props)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import sys
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional, Callable
|
||||||
|
|
||||||
from watchdog.observers import Observer
|
from watchdog.observers import Observer
|
||||||
from watchdog.events import FileSystemEventHandler
|
from watchdog.events import FileSystemEventHandler
|
||||||
@@ -16,17 +16,12 @@ class HotReloadHandler(FileSystemEventHandler):
|
|||||||
"""Handler for file system events."""
|
"""Handler for file system events."""
|
||||||
|
|
||||||
def __init__(self, spec_file: str, debounce_ms: int = 500):
|
def __init__(self, spec_file: str, debounce_ms: int = 500):
|
||||||
"""Initialize the handler.
|
"""Initialize the handler."""
|
||||||
|
|
||||||
Args:
|
|
||||||
spec_file: Path to the spec file to watch
|
|
||||||
debounce_ms: Debounce delay in milliseconds
|
|
||||||
"""
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.spec_file = Path(spec_file).resolve()
|
self.spec_file = Path(spec_file).resolve()
|
||||||
self.debounce_ms = debounce_ms
|
self.debounce_ms = debounce_ms
|
||||||
self.last_reload_time = 0
|
self.last_reload_time = 0
|
||||||
self.reload_callback: Optional[callable] = None
|
self.reload_callback: Optional[Callable] = None
|
||||||
|
|
||||||
def on_modified(self, event):
|
def on_modified(self, event):
|
||||||
"""Handle file modification events."""
|
"""Handle file modification events."""
|
||||||
@@ -57,14 +52,7 @@ class HotReloader:
|
|||||||
host: str = "0.0.0.0",
|
host: str = "0.0.0.0",
|
||||||
debounce_ms: int = 500,
|
debounce_ms: int = 500,
|
||||||
):
|
):
|
||||||
"""Initialize the hot reloader.
|
"""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
|
|
||||||
"""
|
|
||||||
self.spec_file = Path(spec_file).resolve()
|
self.spec_file = Path(spec_file).resolve()
|
||||||
self.port = port
|
self.port = port
|
||||||
self.host = host
|
self.host = host
|
||||||
|
|||||||
@@ -19,12 +19,7 @@ class RequestValidator:
|
|||||||
"""Validates incoming requests against OpenAPI spec."""
|
"""Validates incoming requests against OpenAPI spec."""
|
||||||
|
|
||||||
def __init__(self, spec: Dict[str, Any], strict: bool = False):
|
def __init__(self, spec: Dict[str, Any], strict: bool = False):
|
||||||
"""Initialize the request validator.
|
"""Initialize the request validator."""
|
||||||
|
|
||||||
Args:
|
|
||||||
spec: OpenAPI specification
|
|
||||||
strict: Enable strict validation mode
|
|
||||||
"""
|
|
||||||
self.spec = spec
|
self.spec = spec
|
||||||
self.strict = strict
|
self.strict = strict
|
||||||
|
|
||||||
@@ -36,18 +31,7 @@ class RequestValidator:
|
|||||||
headers: Dict[str, str],
|
headers: Dict[str, str],
|
||||||
body: Optional[Any] = None,
|
body: Optional[Any] = None,
|
||||||
) -> Tuple[bool, List[Dict[str, Any]]]:
|
) -> Tuple[bool, List[Dict[str, Any]]]:
|
||||||
"""Validate a request against the spec.
|
"""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)
|
|
||||||
"""
|
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
path_params = self._extract_path_params(method, path)
|
path_params = self._extract_path_params(method, path)
|
||||||
@@ -97,12 +81,7 @@ class ValidationMiddleware(BaseHTTPMiddleware):
|
|||||||
"""Middleware that validates requests."""
|
"""Middleware that validates requests."""
|
||||||
|
|
||||||
def __init__(self, app, validator: RequestValidator):
|
def __init__(self, app, validator: RequestValidator):
|
||||||
"""Initialize the validation middleware.
|
"""Initialize the validation middleware."""
|
||||||
|
|
||||||
Args:
|
|
||||||
app: The ASGI application
|
|
||||||
validator: RequestValidator instance
|
|
||||||
"""
|
|
||||||
super().__init__(app)
|
super().__init__(app)
|
||||||
self.validator = validator
|
self.validator = validator
|
||||||
|
|
||||||
|
|||||||
@@ -16,23 +16,12 @@ class MockOperationResolver(Resolver):
|
|||||||
"""Custom operation resolver for mock API."""
|
"""Custom operation resolver for mock API."""
|
||||||
|
|
||||||
def __init__(self, mock_server_generator):
|
def __init__(self, mock_server_generator):
|
||||||
"""Initialize the resolver.
|
"""Initialize the resolver."""
|
||||||
|
|
||||||
Args:
|
|
||||||
mock_server_generator: The MockServerGenerator instance
|
|
||||||
"""
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.mock_server_generator = mock_server_generator
|
self.mock_server_generator = mock_server_generator
|
||||||
|
|
||||||
def resolve(self, operation):
|
def resolve(self, operation):
|
||||||
"""Resolve an operation to a mock function.
|
"""Resolve an operation to a mock function."""
|
||||||
|
|
||||||
Args:
|
|
||||||
operation: The operation object from connexion
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Resolution object with the mock function
|
|
||||||
"""
|
|
||||||
operation_id = self.resolve_operation_id(operation)
|
operation_id = self.resolve_operation_id(operation)
|
||||||
for path, path_item in self.mock_server_generator.spec.get("paths", {}).items():
|
for path, path_item in self.mock_server_generator.spec.get("paths", {}).items():
|
||||||
for method, op_def in path_item.items():
|
for method, op_def in path_item.items():
|
||||||
@@ -47,12 +36,7 @@ class MockServerGenerator:
|
|||||||
"""Generates a mock server from an OpenAPI specification."""
|
"""Generates a mock server from an OpenAPI specification."""
|
||||||
|
|
||||||
def __init__(self, spec: Dict[str, Any], config: Optional[Config] = None):
|
def __init__(self, spec: Dict[str, Any], config: Optional[Config] = None):
|
||||||
"""Initialize the mock server generator.
|
"""Initialize the mock server generator."""
|
||||||
|
|
||||||
Args:
|
|
||||||
spec: The OpenAPI specification dictionary
|
|
||||||
config: Configuration object
|
|
||||||
"""
|
|
||||||
self.spec = spec
|
self.spec = spec
|
||||||
self.config = config or Config()
|
self.config = config or Config()
|
||||||
schemas = spec.get("components", {}).get("schemas", {})
|
schemas = spec.get("components", {}).get("schemas", {})
|
||||||
@@ -60,11 +44,7 @@ class MockServerGenerator:
|
|||||||
self.app: Optional[App] = None
|
self.app: Optional[App] = None
|
||||||
|
|
||||||
def generate(self) -> App:
|
def generate(self) -> App:
|
||||||
"""Generate the connexion application.
|
"""Generate the connexion application."""
|
||||||
|
|
||||||
Returns:
|
|
||||||
Configured connexion App instance
|
|
||||||
"""
|
|
||||||
self.app = connexion.App(__name__, specification_dir=".")
|
self.app = connexion.App(__name__, specification_dir=".")
|
||||||
|
|
||||||
self.app.add_api(
|
self.app.add_api(
|
||||||
@@ -84,14 +64,7 @@ class MockServerGenerator:
|
|||||||
return self.app
|
return self.app
|
||||||
|
|
||||||
def _create_mock_function(self, operation: Dict[str, Any]):
|
def _create_mock_function(self, operation: Dict[str, Any]):
|
||||||
"""Create a mock function for an operation.
|
"""Create a mock function for an operation."""
|
||||||
|
|
||||||
Args:
|
|
||||||
operation: The OpenAPI operation definition
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Function that generates mock responses
|
|
||||||
"""
|
|
||||||
|
|
||||||
def mock_function(*args, **kwargs):
|
def mock_function(*args, **kwargs):
|
||||||
mock_config = operation.get("x-mock-config", {})
|
mock_config = operation.get("x-mock-config", {})
|
||||||
@@ -113,26 +86,12 @@ class MockServerGenerator:
|
|||||||
return mock_function
|
return mock_function
|
||||||
|
|
||||||
def _should_return_error(self, mock_config: Dict[str, Any]) -> bool:
|
def _should_return_error(self, mock_config: Dict[str, Any]) -> bool:
|
||||||
"""Determine if we should return an error response.
|
"""Determine if we should return an error response."""
|
||||||
|
|
||||||
Args:
|
|
||||||
mock_config: x-mock-config extension data
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if error should be returned
|
|
||||||
"""
|
|
||||||
error_probability = mock_config.get("errorProbability", 0)
|
error_probability = mock_config.get("errorProbability", 0)
|
||||||
return random.random() < error_probability
|
return random.random() < error_probability
|
||||||
|
|
||||||
def _generate_error_response(self, mock_config: Dict[str, Any]) -> tuple:
|
def _generate_error_response(self, mock_config: Dict[str, Any]) -> tuple:
|
||||||
"""Generate an error response.
|
"""Generate an error response."""
|
||||||
|
|
||||||
Args:
|
|
||||||
mock_config: x-mock-config extension data
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple of (response_body, status_code)
|
|
||||||
"""
|
|
||||||
status_code = mock_config.get("errorCode", 500)
|
status_code = mock_config.get("errorCode", 500)
|
||||||
error_message = mock_config.get("errorMessage", "Mock error")
|
error_message = mock_config.get("errorMessage", "Mock error")
|
||||||
return {"error": error_message}, status_code
|
return {"error": error_message}, status_code
|
||||||
@@ -142,14 +101,6 @@ def create_mock_server(
|
|||||||
spec: Dict[str, Any],
|
spec: Dict[str, Any],
|
||||||
config: Optional[Config] = None,
|
config: Optional[Config] = None,
|
||||||
) -> App:
|
) -> App:
|
||||||
"""Create a mock server from an OpenAPI spec.
|
"""Create a mock server from an OpenAPI spec."""
|
||||||
|
|
||||||
Args:
|
|
||||||
spec: OpenAPI specification dictionary
|
|
||||||
config: Configuration object
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Configured connexion App
|
|
||||||
"""
|
|
||||||
generator = MockServerGenerator(spec, config)
|
generator = MockServerGenerator(spec, config)
|
||||||
return generator.generate()
|
return generator.generate()
|
||||||
|
|||||||
@@ -18,12 +18,7 @@ class SpecLoader:
|
|||||||
SUPPORTED_EXTENSIONS = {".yaml", ".yml", ".json"}
|
SUPPORTED_EXTENSIONS = {".yaml", ".yml", ".json"}
|
||||||
|
|
||||||
def __init__(self, spec_path: str, fmt: Optional[str] = None):
|
def __init__(self, spec_path: str, fmt: Optional[str] = None):
|
||||||
"""Initialize the spec loader.
|
"""Initialize the spec loader."""
|
||||||
|
|
||||||
Args:
|
|
||||||
spec_path: Path to the OpenAPI spec file
|
|
||||||
fmt: Force format (yaml or json), auto-detected if None
|
|
||||||
"""
|
|
||||||
self.spec_path = Path(spec_path)
|
self.spec_path = Path(spec_path)
|
||||||
self.fmt = fmt or self._detect_format()
|
self.fmt = fmt or self._detect_format()
|
||||||
|
|
||||||
@@ -46,14 +41,7 @@ class SpecLoader:
|
|||||||
return "yaml"
|
return "yaml"
|
||||||
|
|
||||||
def load(self) -> Dict[str, Any]:
|
def load(self) -> Dict[str, Any]:
|
||||||
"""Load and parse the OpenAPI specification.
|
"""Load and parse the OpenAPI specification."""
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary containing the parsed spec
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
SpecLoaderError: If loading or parsing fails
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
with open(self.spec_path, "r", encoding="utf-8") as f:
|
with open(self.spec_path, "r", encoding="utf-8") as f:
|
||||||
if self.fmt == "yaml":
|
if self.fmt == "yaml":
|
||||||
|
|||||||
@@ -10,20 +10,12 @@ class OpenAPIValidator:
|
|||||||
"""Validates OpenAPI 3.x specifications."""
|
"""Validates OpenAPI 3.x specifications."""
|
||||||
|
|
||||||
def __init__(self, spec: Dict[str, Any]):
|
def __init__(self, spec: Dict[str, Any]):
|
||||||
"""Initialize the validator.
|
"""Initialize the validator."""
|
||||||
|
|
||||||
Args:
|
|
||||||
spec: The OpenAPI specification dictionary
|
|
||||||
"""
|
|
||||||
self.spec = spec
|
self.spec = spec
|
||||||
self._validation_errors: List[str] = []
|
self._validation_errors: List[str] = []
|
||||||
|
|
||||||
def validate(self) -> List[str]:
|
def validate(self) -> List[str]:
|
||||||
"""Validate the OpenAPI specification.
|
"""Validate the OpenAPI specification."""
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of validation error messages (empty if valid)
|
|
||||||
"""
|
|
||||||
self._validation_errors = []
|
self._validation_errors = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -48,49 +40,23 @@ class OpenAPIValidator:
|
|||||||
return str(error)
|
return str(error)
|
||||||
|
|
||||||
def is_valid(self) -> bool:
|
def is_valid(self) -> bool:
|
||||||
"""Check if the specification is valid.
|
"""Check if the specification is valid."""
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if valid, False otherwise
|
|
||||||
"""
|
|
||||||
return len(self.validate()) == 0
|
return len(self.validate()) == 0
|
||||||
|
|
||||||
def get_paths(self) -> List[str]:
|
def get_paths(self) -> List[str]:
|
||||||
"""Get list of paths in the spec.
|
"""Get list of paths in the spec."""
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of path strings
|
|
||||||
"""
|
|
||||||
return list(self.spec.get("paths", {}).keys())
|
return list(self.spec.get("paths", {}).keys())
|
||||||
|
|
||||||
def get_operations(self, path: str) -> Dict[str, Any]:
|
def get_operations(self, path: str) -> Dict[str, Any]:
|
||||||
"""Get all operations for a given path.
|
"""Get all operations for a given path."""
|
||||||
|
|
||||||
Args:
|
|
||||||
path: The path to get operations for
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary of method -> operation
|
|
||||||
"""
|
|
||||||
path_item = self.spec.get("paths", {}).get(path, {})
|
path_item = self.spec.get("paths", {}).get(path, {})
|
||||||
methods = ["get", "post", "put", "delete", "patch", "options", "head", "trace"]
|
methods = ["get", "post", "put", "delete", "patch", "options", "head", "trace"]
|
||||||
return {m: path_item[m] for m in methods if m in path_item}
|
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]]:
|
def get_schema(self, schema_name: str) -> Optional[Dict[str, Any]]:
|
||||||
"""Get a schema by name from components/schemas.
|
"""Get a schema by name from components/schemas."""
|
||||||
|
|
||||||
Args:
|
|
||||||
schema_name: Name of the schema
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Schema definition or None if not found
|
|
||||||
"""
|
|
||||||
return self.spec.get("components", {}).get("schemas", {}).get(schema_name)
|
return self.spec.get("components", {}).get("schemas", {}).get(schema_name)
|
||||||
|
|
||||||
def get_all_schemas(self) -> Dict[str, Any]:
|
def get_all_schemas(self) -> Dict[str, Any]:
|
||||||
"""Get all schemas from the spec.
|
"""Get all schemas from the spec."""
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary of schema name -> schema definition
|
|
||||||
"""
|
|
||||||
return self.spec.get("components", {}).get("schemas", {})
|
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
|
import pytest
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
|
||||||
from mockapi.cli.main import cli
|
from mockapi.cli.main import cli
|
||||||
|
|
||||||
|
|
||||||
class TestCLIIntegration:
|
def test_cli_validate_command(sample_spec_path):
|
||||||
"""Integration tests for CLI commands."""
|
"""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 test_cli_help():
|
||||||
def sample_spec_path(self):
|
"""Test that CLI help works."""
|
||||||
"""Path to sample spec file."""
|
runner = CliRunner()
|
||||||
return "examples/petstore.yaml"
|
result = runner.invoke(cli, ['--help'])
|
||||||
|
assert result.exit_code == 0
|
||||||
def test_validate_command(self, runner, sample_spec_path):
|
assert 'MockAPI' in result.output
|
||||||
"""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()
|
|
||||||
|
|||||||
@@ -1,70 +1,20 @@
|
|||||||
"""Integration tests for mock server."""
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from pathlib import Path
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
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:
|
def test_server_get_endpoints(sample_spec_path):
|
||||||
"""Integration tests for mock server."""
|
"""Test that server generator extracts endpoints correctly."""
|
||||||
|
from mockapi.core.server import MockServerGenerator
|
||||||
|
|
||||||
@pytest.fixture
|
generator = MockServerGenerator(str(sample_spec_path))
|
||||||
def sample_spec(self):
|
endpoints = generator.get_endpoints()
|
||||||
"""Load sample OpenAPI spec."""
|
assert isinstance(endpoints, list)
|
||||||
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
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
"""Unit tests package."""
|
"""Unit tests initialization."""
|
||||||
|
|||||||
@@ -27,12 +27,7 @@ class TestConfig:
|
|||||||
|
|
||||||
def test_load_from_dict(self):
|
def test_load_from_dict(self):
|
||||||
"""Test creating config with dict values."""
|
"""Test creating config with dict values."""
|
||||||
config = Config(
|
config = Config(port=3000, host="127.0.0.1", delay=100, seed=123)
|
||||||
port=3000,
|
|
||||||
host="127.0.0.1",
|
|
||||||
delay=100,
|
|
||||||
seed=123,
|
|
||||||
)
|
|
||||||
assert config.port == 3000
|
assert config.port == 3000
|
||||||
assert config.host == "127.0.0.1"
|
assert config.host == "127.0.0.1"
|
||||||
assert config.delay == 100
|
assert config.delay == 100
|
||||||
@@ -73,9 +68,7 @@ class TestConfig:
|
|||||||
"seed": 456,
|
"seed": 456,
|
||||||
}
|
}
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
|
||||||
mode="w", suffix=".yaml", delete=False
|
|
||||||
) as f:
|
|
||||||
yaml.dump(config_data, f)
|
yaml.dump(config_data, f)
|
||||||
temp_path = f.name
|
temp_path = f.name
|
||||||
|
|
||||||
@@ -93,9 +86,7 @@ class TestConfig:
|
|||||||
"""Test that environment variables override file config."""
|
"""Test that environment variables override file config."""
|
||||||
config_data = {"port": 7000, "host": "0.0.0.0"}
|
config_data = {"port": 7000, "host": "0.0.0.0"}
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
|
||||||
mode="w", suffix=".yaml", delete=False
|
|
||||||
) as f:
|
|
||||||
yaml.dump(config_data, f)
|
yaml.dump(config_data, f)
|
||||||
temp_path = f.name
|
temp_path = f.name
|
||||||
|
|
||||||
|
|||||||
@@ -179,13 +179,10 @@ class TestDataGenerator:
|
|||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
def test_generate_with_seed_reproducibility(self):
|
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)
|
gen = DataGenerator(seed=12345)
|
||||||
|
|
||||||
schema = {"type": "integer", "minimum": 0, "maximum": 1000}
|
schema = {"type": "integer", "minimum": 0, "maximum": 1000}
|
||||||
|
|
||||||
result1 = gen.generate(schema)
|
result1 = gen.generate(schema)
|
||||||
result2 = gen.generate(schema)
|
result2 = gen.generate(schema)
|
||||||
|
|
||||||
assert isinstance(result1, int)
|
assert isinstance(result1, int)
|
||||||
assert isinstance(result2, int)
|
assert isinstance(result2, int)
|
||||||
|
|||||||
Reference in New Issue
Block a user