swaggerforge 0.1.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- swaggerforge-0.3.0/PKG-INFO +170 -0
- swaggerforge-0.3.0/README.md +146 -0
- {swaggerforge-0.1.0 → swaggerforge-0.3.0}/pyproject.toml +3 -3
- swaggerforge-0.3.0/src/swaggerforge/__main__.py +5 -0
- swaggerforge-0.3.0/src/swaggerforge/auth.py +143 -0
- {swaggerforge-0.1.0 → swaggerforge-0.3.0}/src/swaggerforge/cli.py +43 -9
- swaggerforge-0.3.0/src/swaggerforge/config.py +206 -0
- {swaggerforge-0.1.0 → swaggerforge-0.3.0}/src/swaggerforge/models.py +1 -0
- swaggerforge-0.3.0/src/swaggerforge/output.py +91 -0
- {swaggerforge-0.1.0 → swaggerforge-0.3.0}/src/swaggerforge/parser.py +36 -3
- {swaggerforge-0.1.0 → swaggerforge-0.3.0}/src/swaggerforge/template.py +21 -5
- {swaggerforge-0.1.0 → swaggerforge-0.3.0}/src/swaggerforge/templates/test_file.py.j2 +6 -1
- swaggerforge-0.3.0/src/swaggerforge.egg-info/PKG-INFO +170 -0
- {swaggerforge-0.1.0 → swaggerforge-0.3.0}/src/swaggerforge.egg-info/SOURCES.txt +5 -0
- {swaggerforge-0.1.0 → swaggerforge-0.3.0}/src/swaggerforge.egg-info/requires.txt +5 -1
- swaggerforge-0.3.0/tests/test_auth.py +142 -0
- swaggerforge-0.3.0/tests/test_config.py +332 -0
- swaggerforge-0.3.0/tests/test_e2e_auth.py +109 -0
- {swaggerforge-0.1.0 → swaggerforge-0.3.0}/tests/test_generator.py +4 -4
- swaggerforge-0.3.0/tests/test_output.py +112 -0
- swaggerforge-0.3.0/tests/test_parser.py +200 -0
- swaggerforge-0.3.0/tests/test_template.py +138 -0
- swaggerforge-0.1.0/PKG-INFO +0 -99
- swaggerforge-0.1.0/README.md +0 -77
- swaggerforge-0.1.0/src/swaggerforge/__main__.py +0 -0
- swaggerforge-0.1.0/src/swaggerforge/output.py +0 -53
- swaggerforge-0.1.0/src/swaggerforge.egg-info/PKG-INFO +0 -99
- swaggerforge-0.1.0/tests/test_output.py +0 -46
- swaggerforge-0.1.0/tests/test_parser.py +0 -79
- swaggerforge-0.1.0/tests/test_template.py +0 -73
- {swaggerforge-0.1.0 → swaggerforge-0.3.0}/LICENSE +0 -0
- {swaggerforge-0.1.0 → swaggerforge-0.3.0}/setup.cfg +0 -0
- {swaggerforge-0.1.0 → swaggerforge-0.3.0}/src/swaggerforge/__init__.py +0 -0
- {swaggerforge-0.1.0 → swaggerforge-0.3.0}/src/swaggerforge/generator.py +0 -0
- {swaggerforge-0.1.0 → swaggerforge-0.3.0}/src/swaggerforge/validator.py +0 -0
- {swaggerforge-0.1.0 → swaggerforge-0.3.0}/src/swaggerforge.egg-info/dependency_links.txt +0 -0
- {swaggerforge-0.1.0 → swaggerforge-0.3.0}/src/swaggerforge.egg-info/entry_points.txt +0 -0
- {swaggerforge-0.1.0 → swaggerforge-0.3.0}/src/swaggerforge.egg-info/top_level.txt +0 -0
- {swaggerforge-0.1.0 → swaggerforge-0.3.0}/tests/test_validator.py +0 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: swaggerforge
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Automatic pytest test generation from OpenAPI (Swagger) specifications
|
|
5
|
+
Author: Viktor Pylypenko
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: openapi,swagger,pytest,test generation,api testing
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: click>=8.0
|
|
12
|
+
Requires-Dist: prance>=23.6.21.0
|
|
13
|
+
Requires-Dist: openapi-spec-validator>=0.7
|
|
14
|
+
Requires-Dist: jinja2>=3.0
|
|
15
|
+
Requires-Dist: requests>=2.28
|
|
16
|
+
Requires-Dist: jsonschema>=4.0
|
|
17
|
+
Requires-Dist: tomli>=2.0; python_version < "3.11"
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=9.0; extra == "dev"
|
|
20
|
+
Requires-Dist: flake8>=7.0; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-cov>=7.0; extra == "dev"
|
|
22
|
+
Requires-Dist: tox>=4.0; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# SwaggerForge
|
|
26
|
+
|
|
27
|
+
Automatic pytest test generation from OpenAPI (Swagger) specifications.
|
|
28
|
+
|
|
29
|
+
SwaggerForge is a Python library and command-line tool that reads an OpenAPI
|
|
30
|
+
specification and generates ready-to-run pytest test files - one per resource
|
|
31
|
+
covering positive, negative, boundary, and boolean scenarios grounded in
|
|
32
|
+
established test-design techniques.
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
- Reads OpenAPI 3.x specifications in JSON or YAML
|
|
37
|
+
- Resolves `$ref` references automatically
|
|
38
|
+
- Generates one pytest file per resource tag
|
|
39
|
+
- Produces six scenario types per endpoint where applicable:
|
|
40
|
+
- **Positive** >>> valid request, expects a 2xx response and validates the
|
|
41
|
+
response schema
|
|
42
|
+
- **Missing required field** >>> omits a required field, expects 400
|
|
43
|
+
- **Wrong data type** >>> sends a mistyped field, expects 400/422
|
|
44
|
+
- **Nonexistent resource** >>> requests an unlikely identifier, expects 404
|
|
45
|
+
- **Boundary values** >>> tests values at and just beyond declared
|
|
46
|
+
numeric/length limits (Boundary Value Analysis)
|
|
47
|
+
- **Boolean coverage** >>> exercises both `true` and `false` for boolean fields
|
|
48
|
+
- Deterministic output: the same specification always produces identical tests
|
|
49
|
+
- Generated files use session-scoped pytest fixtures and run with no manual edits
|
|
50
|
+
- Optional `swaggerforge.toml` config file for project-level defaults
|
|
51
|
+
- Authentication support: bearer, API key, and basic schemes, with
|
|
52
|
+
environment-variable substitution for secrets
|
|
53
|
+
|
|
54
|
+
## Requirements
|
|
55
|
+
|
|
56
|
+
- Python 3.10 or newer
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install swaggerforge
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
Generate tests from a specification, pointing at the base URL of the API
|
|
67
|
+
under test:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
swaggerforge generate --spec swagger.json --url http://localhost:8080
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
This reads `swagger.json`, writes one `test_<resource>.py` file per resource
|
|
74
|
+
tag into the output directory (default: `tests_generated/`), and the files can
|
|
75
|
+
be run immediately:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pytest tests_generated
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Options
|
|
82
|
+
|
|
83
|
+
| Option | Description | Default |
|
|
84
|
+
|------------|----------------------------------------------------|-----------------------------------|
|
|
85
|
+
| `--spec` | Path to the OpenAPI specification (JSON or YAML) | *(required)* |
|
|
86
|
+
| `--url` | Base URL of the API under test | *(required unless in config)* |
|
|
87
|
+
| `--output` | Directory for the generated test files | `tests_generated` |
|
|
88
|
+
| `--config` | Path to a configuration file | `./swaggerforge.toml` if present |
|
|
89
|
+
|
|
90
|
+
## Configuration
|
|
91
|
+
|
|
92
|
+
Options that stay the same across runs can be kept in a `swaggerforge.toml`
|
|
93
|
+
file instead of being passed on the command line. The file is picked up
|
|
94
|
+
automatically from the directory where the tool is run, or an explicit path
|
|
95
|
+
can be given with `--config`.
|
|
96
|
+
|
|
97
|
+
```toml
|
|
98
|
+
# swaggerforge.toml
|
|
99
|
+
base_url = "http://localhost:8080"
|
|
100
|
+
output_dir = "tests_generated"
|
|
101
|
+
timeout = 30
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
| Key | Type | Effect |
|
|
105
|
+
|--------------|---------|---------------------------------------------------------------|
|
|
106
|
+
| `base_url` | string | Base URL of the API; makes `--url` optional |
|
|
107
|
+
| `output_dir` | string | Directory for generated files |
|
|
108
|
+
| `timeout` | integer | Embeds `timeout=<n>` into every generated HTTP request call |
|
|
109
|
+
|
|
110
|
+
Values given on the command line always take precedence over the config file.
|
|
111
|
+
Without a `timeout`, generated tests place no time limit on requests -
|
|
112
|
+
setting one makes test runs fail fast when the API is unreachable.
|
|
113
|
+
|
|
114
|
+
## Authentication
|
|
115
|
+
|
|
116
|
+
When a specification declares security requirements, SwaggerForge embeds the
|
|
117
|
+
matching authentication headers into the generated tests. Credentials are
|
|
118
|
+
supplied through the config file, never taken from the specification itself.
|
|
119
|
+
|
|
120
|
+
```toml
|
|
121
|
+
# swaggerforge.toml
|
|
122
|
+
[auth.bearer]
|
|
123
|
+
token = "${API_TOKEN}"
|
|
124
|
+
|
|
125
|
+
[auth.api_key]
|
|
126
|
+
value = "${API_KEY}"
|
|
127
|
+
|
|
128
|
+
[auth.basic]
|
|
129
|
+
username = "${API_USER}"
|
|
130
|
+
password = "${API_PASSWORD}"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Three scheme kinds are supported, matching the OpenAPI security scheme types:
|
|
134
|
+
|
|
135
|
+
| Section | Applies to | Generated header |
|
|
136
|
+
|-----------------|---------------------------------------------|-------------------------------------------|
|
|
137
|
+
| `[auth.bearer]` | `http` bearer, and `oauth2` (pre-obtained token) | `Authorization: Bearer <token>` |
|
|
138
|
+
| `[auth.api_key]`| `apiKey` in header | the header named by the specification |
|
|
139
|
+
| `[auth.basic]` | `http` basic | `Authorization: Basic <base64>` |
|
|
140
|
+
|
|
141
|
+
For an API key, the header **name** comes from the specification's scheme
|
|
142
|
+
definition; the config supplies only the secret `value`.
|
|
143
|
+
|
|
144
|
+
**Environment variables.** Any auth value may reference an environment
|
|
145
|
+
variable with `${VAR}` syntax, so secrets stay out of the config file. A
|
|
146
|
+
referenced variable that is not set is an error.
|
|
147
|
+
|
|
148
|
+
**Unconfigured or unsupported schemes.** If a specification requires a scheme
|
|
149
|
+
whose credentials are not configured, SwaggerForge prints a warning and
|
|
150
|
+
generates the tests without that header, rather than failing. Schemes that are
|
|
151
|
+
not yet supported - API keys in a query string or cookie, and OpenID Connect -
|
|
152
|
+
are likewise skipped with a warning.
|
|
153
|
+
|
|
154
|
+
## How it works
|
|
155
|
+
|
|
156
|
+
SwaggerForge runs a six-stage pipeline: the specification is validated, parsed
|
|
157
|
+
into an internal model (with `$ref`s resolved), turned into test scenarios
|
|
158
|
+
based on test-design techniques, rendered into pytest code via templates, and
|
|
159
|
+
written to per-resource files.
|
|
160
|
+
|
|
161
|
+
## Limitations
|
|
162
|
+
|
|
163
|
+
- Targets OpenAPI 3.x with JSON request bodies
|
|
164
|
+
- Authentication covers bearer, API key (header), and basic schemes; API keys
|
|
165
|
+
in query or cookie, full OAuth2 flows, and OpenID Connect are not yet handled
|
|
166
|
+
- Boundary tests require the specification to declare numeric/length constraints
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# SwaggerForge
|
|
2
|
+
|
|
3
|
+
Automatic pytest test generation from OpenAPI (Swagger) specifications.
|
|
4
|
+
|
|
5
|
+
SwaggerForge is a Python library and command-line tool that reads an OpenAPI
|
|
6
|
+
specification and generates ready-to-run pytest test files - one per resource
|
|
7
|
+
covering positive, negative, boundary, and boolean scenarios grounded in
|
|
8
|
+
established test-design techniques.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- Reads OpenAPI 3.x specifications in JSON or YAML
|
|
13
|
+
- Resolves `$ref` references automatically
|
|
14
|
+
- Generates one pytest file per resource tag
|
|
15
|
+
- Produces six scenario types per endpoint where applicable:
|
|
16
|
+
- **Positive** >>> valid request, expects a 2xx response and validates the
|
|
17
|
+
response schema
|
|
18
|
+
- **Missing required field** >>> omits a required field, expects 400
|
|
19
|
+
- **Wrong data type** >>> sends a mistyped field, expects 400/422
|
|
20
|
+
- **Nonexistent resource** >>> requests an unlikely identifier, expects 404
|
|
21
|
+
- **Boundary values** >>> tests values at and just beyond declared
|
|
22
|
+
numeric/length limits (Boundary Value Analysis)
|
|
23
|
+
- **Boolean coverage** >>> exercises both `true` and `false` for boolean fields
|
|
24
|
+
- Deterministic output: the same specification always produces identical tests
|
|
25
|
+
- Generated files use session-scoped pytest fixtures and run with no manual edits
|
|
26
|
+
- Optional `swaggerforge.toml` config file for project-level defaults
|
|
27
|
+
- Authentication support: bearer, API key, and basic schemes, with
|
|
28
|
+
environment-variable substitution for secrets
|
|
29
|
+
|
|
30
|
+
## Requirements
|
|
31
|
+
|
|
32
|
+
- Python 3.10 or newer
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install swaggerforge
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
Generate tests from a specification, pointing at the base URL of the API
|
|
43
|
+
under test:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
swaggerforge generate --spec swagger.json --url http://localhost:8080
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
This reads `swagger.json`, writes one `test_<resource>.py` file per resource
|
|
50
|
+
tag into the output directory (default: `tests_generated/`), and the files can
|
|
51
|
+
be run immediately:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pytest tests_generated
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Options
|
|
58
|
+
|
|
59
|
+
| Option | Description | Default |
|
|
60
|
+
|------------|----------------------------------------------------|-----------------------------------|
|
|
61
|
+
| `--spec` | Path to the OpenAPI specification (JSON or YAML) | *(required)* |
|
|
62
|
+
| `--url` | Base URL of the API under test | *(required unless in config)* |
|
|
63
|
+
| `--output` | Directory for the generated test files | `tests_generated` |
|
|
64
|
+
| `--config` | Path to a configuration file | `./swaggerforge.toml` if present |
|
|
65
|
+
|
|
66
|
+
## Configuration
|
|
67
|
+
|
|
68
|
+
Options that stay the same across runs can be kept in a `swaggerforge.toml`
|
|
69
|
+
file instead of being passed on the command line. The file is picked up
|
|
70
|
+
automatically from the directory where the tool is run, or an explicit path
|
|
71
|
+
can be given with `--config`.
|
|
72
|
+
|
|
73
|
+
```toml
|
|
74
|
+
# swaggerforge.toml
|
|
75
|
+
base_url = "http://localhost:8080"
|
|
76
|
+
output_dir = "tests_generated"
|
|
77
|
+
timeout = 30
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
| Key | Type | Effect |
|
|
81
|
+
|--------------|---------|---------------------------------------------------------------|
|
|
82
|
+
| `base_url` | string | Base URL of the API; makes `--url` optional |
|
|
83
|
+
| `output_dir` | string | Directory for generated files |
|
|
84
|
+
| `timeout` | integer | Embeds `timeout=<n>` into every generated HTTP request call |
|
|
85
|
+
|
|
86
|
+
Values given on the command line always take precedence over the config file.
|
|
87
|
+
Without a `timeout`, generated tests place no time limit on requests -
|
|
88
|
+
setting one makes test runs fail fast when the API is unreachable.
|
|
89
|
+
|
|
90
|
+
## Authentication
|
|
91
|
+
|
|
92
|
+
When a specification declares security requirements, SwaggerForge embeds the
|
|
93
|
+
matching authentication headers into the generated tests. Credentials are
|
|
94
|
+
supplied through the config file, never taken from the specification itself.
|
|
95
|
+
|
|
96
|
+
```toml
|
|
97
|
+
# swaggerforge.toml
|
|
98
|
+
[auth.bearer]
|
|
99
|
+
token = "${API_TOKEN}"
|
|
100
|
+
|
|
101
|
+
[auth.api_key]
|
|
102
|
+
value = "${API_KEY}"
|
|
103
|
+
|
|
104
|
+
[auth.basic]
|
|
105
|
+
username = "${API_USER}"
|
|
106
|
+
password = "${API_PASSWORD}"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Three scheme kinds are supported, matching the OpenAPI security scheme types:
|
|
110
|
+
|
|
111
|
+
| Section | Applies to | Generated header |
|
|
112
|
+
|-----------------|---------------------------------------------|-------------------------------------------|
|
|
113
|
+
| `[auth.bearer]` | `http` bearer, and `oauth2` (pre-obtained token) | `Authorization: Bearer <token>` |
|
|
114
|
+
| `[auth.api_key]`| `apiKey` in header | the header named by the specification |
|
|
115
|
+
| `[auth.basic]` | `http` basic | `Authorization: Basic <base64>` |
|
|
116
|
+
|
|
117
|
+
For an API key, the header **name** comes from the specification's scheme
|
|
118
|
+
definition; the config supplies only the secret `value`.
|
|
119
|
+
|
|
120
|
+
**Environment variables.** Any auth value may reference an environment
|
|
121
|
+
variable with `${VAR}` syntax, so secrets stay out of the config file. A
|
|
122
|
+
referenced variable that is not set is an error.
|
|
123
|
+
|
|
124
|
+
**Unconfigured or unsupported schemes.** If a specification requires a scheme
|
|
125
|
+
whose credentials are not configured, SwaggerForge prints a warning and
|
|
126
|
+
generates the tests without that header, rather than failing. Schemes that are
|
|
127
|
+
not yet supported - API keys in a query string or cookie, and OpenID Connect -
|
|
128
|
+
are likewise skipped with a warning.
|
|
129
|
+
|
|
130
|
+
## How it works
|
|
131
|
+
|
|
132
|
+
SwaggerForge runs a six-stage pipeline: the specification is validated, parsed
|
|
133
|
+
into an internal model (with `$ref`s resolved), turned into test scenarios
|
|
134
|
+
based on test-design techniques, rendered into pytest code via templates, and
|
|
135
|
+
written to per-resource files.
|
|
136
|
+
|
|
137
|
+
## Limitations
|
|
138
|
+
|
|
139
|
+
- Targets OpenAPI 3.x with JSON request bodies
|
|
140
|
+
- Authentication covers bearer, API key (header), and basic schemes; API keys
|
|
141
|
+
in query or cookie, full OAuth2 flows, and OpenID Connect are not yet handled
|
|
142
|
+
- Boundary tests require the specification to declare numeric/length constraints
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "swaggerforge"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Automatic pytest test generation from OpenAPI (Swagger) specifications"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -16,11 +16,11 @@ keywords = ["openapi", "swagger", "pytest", "test generation", "api testing"]
|
|
|
16
16
|
dependencies = [
|
|
17
17
|
"click>=8.0",
|
|
18
18
|
"prance>=23.6.21.0",
|
|
19
|
-
"openapi-core>=0.18",
|
|
20
19
|
"openapi-spec-validator>=0.7",
|
|
21
20
|
"jinja2>=3.0",
|
|
22
21
|
"requests>=2.28",
|
|
23
22
|
"jsonschema>=4.0",
|
|
23
|
+
'tomli>=2.0; python_version < "3.11"',
|
|
24
24
|
]
|
|
25
25
|
|
|
26
26
|
[project.scripts]
|
|
@@ -38,4 +38,4 @@ addopts = "--import-mode=importlib"
|
|
|
38
38
|
testpaths = ["tests"]
|
|
39
39
|
|
|
40
40
|
[project.optional-dependencies]
|
|
41
|
-
dev = ["pytest>=9.0", "flake8>=7.0"]
|
|
41
|
+
dev = ["pytest>=9.0", "flake8>=7.0", "pytest-cov>=7.0", "tox>=4.0"]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Resolution of authentication for generated tests.
|
|
2
|
+
|
|
3
|
+
Given an endpoint's declared security requirement, the specification's
|
|
4
|
+
security schemes, and the user's configured credentials, this module
|
|
5
|
+
decides which authentication headers to inject into the generated tests.
|
|
6
|
+
|
|
7
|
+
Supported schemes (v0.3.0): HTTP bearer, HTTP basic, and API key in a
|
|
8
|
+
header. OAuth2 is treated as bearer (the user supplies a pre-obtained
|
|
9
|
+
token). Other schemes - API key in query or cookie, OpenID Connect,
|
|
10
|
+
and any undefined scheme name - are skipped with a warning.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import base64
|
|
14
|
+
from typing import NamedTuple
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ResolvedAuth(NamedTuple):
|
|
18
|
+
"""The outcome of resolving auth for one endpoint.
|
|
19
|
+
|
|
20
|
+
``headers`` holds the authentication headers to inject (empty when no
|
|
21
|
+
auth applies). ``warning`` is a human-readable message when a declared
|
|
22
|
+
scheme was skipped, or None otherwise.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
headers: dict
|
|
26
|
+
warning: str | None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_NO_AUTH = ResolvedAuth(headers={}, warning=None)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def resolve_auth(endpoint, security_schemes, auth_config):
|
|
33
|
+
"""Resolve the authentication headers for a single endpoint.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
endpoint: The Endpoint whose security requirement is resolved.
|
|
37
|
+
security_schemes: The specification's securitySchemes definitions.
|
|
38
|
+
auth_config: The AuthConfig carrying user-supplied credentials.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
A ResolvedAuth with the headers to inject and an optional warning.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
AuthError: If a supported scheme is required but its credentials
|
|
45
|
+
are not configured.
|
|
46
|
+
"""
|
|
47
|
+
if not endpoint.security:
|
|
48
|
+
return _NO_AUTH
|
|
49
|
+
|
|
50
|
+
scheme_name = endpoint.security[0]
|
|
51
|
+
scheme = security_schemes.get(scheme_name)
|
|
52
|
+
if scheme is None:
|
|
53
|
+
return ResolvedAuth(
|
|
54
|
+
headers={},
|
|
55
|
+
warning=(
|
|
56
|
+
f"Security scheme '{scheme_name}' required by "
|
|
57
|
+
f"{endpoint.method.upper()} {endpoint.path} is not defined in "
|
|
58
|
+
f"the specification; skipping authentication for it."
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
scheme_type = scheme.get("type")
|
|
63
|
+
|
|
64
|
+
if scheme_type == "oauth2":
|
|
65
|
+
return _resolve_bearer(scheme_name, auth_config)
|
|
66
|
+
if scheme_type == "http":
|
|
67
|
+
http_scheme = scheme.get("scheme", "").lower()
|
|
68
|
+
if http_scheme == "bearer":
|
|
69
|
+
return _resolve_bearer(scheme_name, auth_config)
|
|
70
|
+
if http_scheme == "basic":
|
|
71
|
+
return _resolve_basic(scheme_name, auth_config)
|
|
72
|
+
if scheme_type == "apiKey" and scheme.get("in") == "header":
|
|
73
|
+
return _resolve_api_key(scheme_name, scheme, auth_config)
|
|
74
|
+
|
|
75
|
+
return ResolvedAuth(
|
|
76
|
+
headers={},
|
|
77
|
+
warning=(
|
|
78
|
+
f"Security scheme '{scheme_name}' (type '{scheme_type}') is not "
|
|
79
|
+
f"supported in this version; skipping authentication for "
|
|
80
|
+
f"{endpoint.method.upper()} {endpoint.path}."
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _resolve_bearer(scheme_name, auth_config):
|
|
86
|
+
"""Build a bearer Authorization header from configured credentials."""
|
|
87
|
+
bearer = auth_config.bearer
|
|
88
|
+
if bearer is None or not bearer.token:
|
|
89
|
+
return ResolvedAuth(
|
|
90
|
+
headers={},
|
|
91
|
+
warning=(
|
|
92
|
+
f"Scheme '{scheme_name}' requires a bearer token, but none is "
|
|
93
|
+
f"configured; generating without authentication. Add a "
|
|
94
|
+
f"[auth.bearer] section with a 'token' to enable it."
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
return ResolvedAuth(
|
|
98
|
+
headers={"Authorization": f"Bearer {bearer.token}"}, warning=None
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _resolve_basic(scheme_name, auth_config):
|
|
103
|
+
"""Build a basic Authorization header from configured credentials."""
|
|
104
|
+
basic = auth_config.basic
|
|
105
|
+
if basic is None or not basic.username or not basic.password:
|
|
106
|
+
return ResolvedAuth(
|
|
107
|
+
headers={},
|
|
108
|
+
warning=(
|
|
109
|
+
f"Scheme '{scheme_name}' requires basic credentials, but the "
|
|
110
|
+
f"username and password are not both configured; generating "
|
|
111
|
+
f"without authentication. Add a [auth.basic] section with "
|
|
112
|
+
f"'username' and 'password' to enable it."
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
raw = f"{basic.username}:{basic.password}".encode("utf-8")
|
|
116
|
+
encoded = base64.b64encode(raw).decode("ascii")
|
|
117
|
+
return ResolvedAuth(
|
|
118
|
+
headers={"Authorization": f"Basic {encoded}"}, warning=None
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _resolve_api_key(scheme_name, scheme, auth_config):
|
|
123
|
+
"""Build an API-key header, taking the header name from the spec scheme."""
|
|
124
|
+
api_key = auth_config.api_key
|
|
125
|
+
if api_key is None or not api_key.value:
|
|
126
|
+
return ResolvedAuth(
|
|
127
|
+
headers={},
|
|
128
|
+
warning=(
|
|
129
|
+
f"Scheme '{scheme_name}' requires an API key, but none is "
|
|
130
|
+
f"configured; generating without authentication. Add a "
|
|
131
|
+
f"[auth.api_key] section with a 'value' to enable it."
|
|
132
|
+
),
|
|
133
|
+
)
|
|
134
|
+
header_name = scheme.get("name")
|
|
135
|
+
if not header_name:
|
|
136
|
+
return ResolvedAuth(
|
|
137
|
+
headers={},
|
|
138
|
+
warning=(
|
|
139
|
+
f"API-key scheme '{scheme_name}' does not declare a header "
|
|
140
|
+
f"name; skipping authentication for it."
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
return ResolvedAuth(headers={header_name: api_key.value}, warning=None)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Command-line interface for SwaggerForge."""
|
|
2
2
|
|
|
3
3
|
import click
|
|
4
|
+
from swaggerforge.config import load_config, ConfigError
|
|
4
5
|
from swaggerforge.validator import load_and_validate, SpecValidationError
|
|
5
6
|
from swaggerforge.parser import parse_spec, SpecParseError
|
|
6
7
|
from swaggerforge.generator import generate_scenarios
|
|
@@ -23,17 +24,39 @@ def main():
|
|
|
23
24
|
)
|
|
24
25
|
@click.option(
|
|
25
26
|
"--url",
|
|
26
|
-
|
|
27
|
-
help="Base URL of the API to be tested."
|
|
27
|
+
default=None,
|
|
28
|
+
help="Base URL of the API to be tested. "
|
|
29
|
+
"Required unless base_url is set in the config file.",
|
|
28
30
|
)
|
|
29
31
|
@click.option(
|
|
30
32
|
"--output",
|
|
31
|
-
default=
|
|
33
|
+
default=None,
|
|
34
|
+
type=click.Path(),
|
|
35
|
+
help="Directory where generated test files will be written "
|
|
36
|
+
"[default: tests_generated].",
|
|
37
|
+
)
|
|
38
|
+
@click.option(
|
|
39
|
+
"--config",
|
|
40
|
+
"config_path",
|
|
41
|
+
default=None,
|
|
32
42
|
type=click.Path(),
|
|
33
|
-
help="
|
|
43
|
+
help="Path to a swaggerforge.toml config file "
|
|
44
|
+
"[default: ./swaggerforge.toml if present].",
|
|
34
45
|
)
|
|
35
|
-
def generate(spec, url, output):
|
|
46
|
+
def generate(spec, url, output, config_path):
|
|
36
47
|
"""Generate pytest test files from an OpenAPI specification."""
|
|
48
|
+
try:
|
|
49
|
+
config = load_config(config_path)
|
|
50
|
+
except ConfigError as error:
|
|
51
|
+
raise click.ClickException(str(error))
|
|
52
|
+
|
|
53
|
+
url = url or config.base_url
|
|
54
|
+
if url is None:
|
|
55
|
+
raise click.ClickException(
|
|
56
|
+
"No base URL given: pass --url or set base_url in swaggerforge.toml."
|
|
57
|
+
)
|
|
58
|
+
output = output or config.output_dir or "tests_generated"
|
|
59
|
+
|
|
37
60
|
click.echo(f"Reading specification: {spec}")
|
|
38
61
|
|
|
39
62
|
try:
|
|
@@ -45,7 +68,8 @@ def generate(spec, url, output):
|
|
|
45
68
|
click.echo(f"API title: {spec_dict['info']['title']}")
|
|
46
69
|
|
|
47
70
|
try:
|
|
48
|
-
|
|
71
|
+
parsed = parse_spec(spec)
|
|
72
|
+
endpoints = parsed.endpoints
|
|
49
73
|
except SpecParseError as error:
|
|
50
74
|
raise click.ClickException(str(error))
|
|
51
75
|
|
|
@@ -56,12 +80,22 @@ def generate(spec, url, output):
|
|
|
56
80
|
click.echo(f"Generated {len(scenarios)} test scenarios.")
|
|
57
81
|
|
|
58
82
|
try:
|
|
59
|
-
|
|
83
|
+
result = write_test_files(
|
|
84
|
+
scenarios,
|
|
85
|
+
url,
|
|
86
|
+
output,
|
|
87
|
+
config.timeout,
|
|
88
|
+
security_schemes=parsed.security_schemes,
|
|
89
|
+
auth_config=config.auth,
|
|
90
|
+
)
|
|
60
91
|
except OSError as error:
|
|
61
92
|
raise click.ClickException(
|
|
62
93
|
f"Could not write test files to '{output}': {error}"
|
|
63
94
|
)
|
|
64
95
|
|
|
65
|
-
|
|
66
|
-
|
|
96
|
+
for warning in result.warnings:
|
|
97
|
+
click.echo(f"Warning: {warning}")
|
|
98
|
+
|
|
99
|
+
click.echo(f"Wrote {len(result.written)} test file(s) to '{output}':")
|
|
100
|
+
for path in result.written:
|
|
67
101
|
click.echo(f" - {path}")
|