robotframework-openapitools 1.0.0b4__py3-none-any.whl → 1.0.1__py3-none-any.whl
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.
- OpenApiDriver/__init__.py +1 -0
- OpenApiDriver/openapi_executors.py +32 -4
- OpenApiDriver/openapidriver.libspec +183 -80
- OpenApiDriver/openapidriver.py +8 -279
- OpenApiLibCore/__init__.py +26 -0
- OpenApiLibCore/data_generation/__init__.py +0 -0
- OpenApiLibCore/data_generation/body_data_generation.py +4 -4
- OpenApiLibCore/data_generation/data_generation_core.py +18 -45
- OpenApiLibCore/data_invalidation.py +1 -5
- OpenApiLibCore/dto_base.py +31 -22
- OpenApiLibCore/dto_utils.py +31 -3
- OpenApiLibCore/localized_faker.py +0 -0
- OpenApiLibCore/models.py +26 -18
- OpenApiLibCore/openapi_libcore.libspec +229 -121
- OpenApiLibCore/openapi_libcore.py +45 -274
- OpenApiLibCore/parameter_utils.py +3 -3
- OpenApiLibCore/path_functions.py +5 -6
- OpenApiLibCore/path_invalidation.py +5 -7
- OpenApiLibCore/protocols.py +6 -0
- OpenApiLibCore/request_data.py +0 -0
- OpenApiLibCore/resource_relations.py +4 -2
- OpenApiLibCore/validation.py +4 -9
- OpenApiLibCore/value_utils.py +1 -1
- openapi_libgen/__init__.py +3 -0
- openapi_libgen/generator.py +2 -4
- openapi_libgen/parsing_utils.py +9 -5
- openapi_libgen/spec_parser.py +4 -4
- openapitools_docs/__init__.py +0 -0
- openapitools_docs/docstrings.py +891 -0
- openapitools_docs/documentation_generator.py +35 -0
- openapitools_docs/templates/documentation.jinja +209 -0
- {robotframework_openapitools-1.0.0b4.dist-info → robotframework_openapitools-1.0.1.dist-info}/METADATA +16 -98
- robotframework_openapitools-1.0.1.dist-info/RECORD +44 -0
- robotframework_openapitools-1.0.0b4.dist-info/RECORD +0 -40
- {robotframework_openapitools-1.0.0b4.dist-info → robotframework_openapitools-1.0.1.dist-info}/LICENSE +0 -0
- {robotframework_openapitools-1.0.0b4.dist-info → robotframework_openapitools-1.0.1.dist-info}/WHEEL +0 -0
- {robotframework_openapitools-1.0.0b4.dist-info → robotframework_openapitools-1.0.1.dist-info}/entry_points.txt +0 -0
OpenApiDriver/openapidriver.py
CHANGED
@@ -1,127 +1,3 @@
|
|
1
|
-
"""
|
2
|
-
# OpenApiDriver for Robot Framework®
|
3
|
-
|
4
|
-
OpenApiDriver is an extension of the Robot Framework® DataDriver library that allows
|
5
|
-
for generation and execution of test cases based on the information in an OpenAPI
|
6
|
-
document (also known as Swagger document).
|
7
|
-
This document explains how to use the OpenApiDriver library.
|
8
|
-
|
9
|
-
For more information about Robot Framework®, see http://robotframework.org.
|
10
|
-
|
11
|
-
For more information about the DataDriver library, see
|
12
|
-
https://github.com/Snooz82/robotframework-datadriver.
|
13
|
-
|
14
|
-
---
|
15
|
-
|
16
|
-
> Note: OpenApiDriver is still under development so there are currently
|
17
|
-
restrictions / limitations that you may encounter when using this library to run
|
18
|
-
tests against an API. See [Limitations](#limitations) for details.
|
19
|
-
|
20
|
-
---
|
21
|
-
|
22
|
-
## Installation
|
23
|
-
|
24
|
-
If you already have Python >= 3.8 with pip installed, you can simply run:
|
25
|
-
|
26
|
-
`pip install --upgrade robotframework-openapidriver`
|
27
|
-
|
28
|
-
---
|
29
|
-
|
30
|
-
## OpenAPI (aka Swagger)
|
31
|
-
|
32
|
-
The OpenAPI Specification (OAS) defines a standard, language-agnostic interface
|
33
|
-
to RESTful APIs, see https://swagger.io/specification/
|
34
|
-
|
35
|
-
The OpenApiDriver module implements a reader class that generates a test case for
|
36
|
-
each path, method and response (i.e. every response for each endpoint) that is defined
|
37
|
-
in an OpenAPI document, typically an openapi.json or openapi.yaml file.
|
38
|
-
|
39
|
-
> Note: OpenApiDriver is designed for APIs based on the OAS v3
|
40
|
-
The library has not been tested for APIs based on the OAS v2.
|
41
|
-
|
42
|
-
---
|
43
|
-
|
44
|
-
## Getting started
|
45
|
-
|
46
|
-
Before trying to use OpenApiDriver to run automatic validations on the target API
|
47
|
-
it's recommended to first ensure that the openapi document for the API is valid
|
48
|
-
under the OpenAPI Specification.
|
49
|
-
|
50
|
-
This can be done using the command line interface of a package that is installed as
|
51
|
-
a prerequisite for OpenApiDriver.
|
52
|
-
Both a local openapi.json or openapi.yaml file or one hosted by the API server
|
53
|
-
can be checked using the `prance validate <reference_to_file>` shell command:
|
54
|
-
|
55
|
-
```shell
|
56
|
-
prance validate --backend=openapi-spec-validator http://localhost:8000/openapi.json
|
57
|
-
Processing "http://localhost:8000/openapi.json"...
|
58
|
-
-> Resolving external references.
|
59
|
-
Validates OK as OpenAPI 3.0.2!
|
60
|
-
|
61
|
-
prance validate --backend=openapi-spec-validator /tests/files/petstore_openapi.yaml
|
62
|
-
Processing "/tests/files/petstore_openapi.yaml"...
|
63
|
-
-> Resolving external references.
|
64
|
-
Validates OK as OpenAPI 3.0.2!
|
65
|
-
```
|
66
|
-
|
67
|
-
You'll have to change the url or file reference to the location of the openapi
|
68
|
-
document for your API.
|
69
|
-
|
70
|
-
> Note: Although recursion is technically allowed under the OAS, tool support is limited
|
71
|
-
and changing the OAS to not use recursion is recommended.
|
72
|
-
OpenApiDriver has limited support for parsing OpenAPI documents with
|
73
|
-
recursion in them. See the `recursion_limit` and `recursion_default` parameters.
|
74
|
-
|
75
|
-
If the openapi document passes this validation, the next step is trying to do a test
|
76
|
-
run with a minimal test suite.
|
77
|
-
The example below can be used, with `source` and `origin` altered to fit your situation.
|
78
|
-
|
79
|
-
``` robotframework
|
80
|
-
*** Settings ***
|
81
|
-
Library OpenApiDriver
|
82
|
-
... source=http://localhost:8000/openapi.json
|
83
|
-
... origin=http://localhost:8000
|
84
|
-
Test Template Validate Using Test Endpoint Keyword
|
85
|
-
|
86
|
-
*** Test Cases ***
|
87
|
-
Test Endpoint for ${method} on ${path} where ${status_code} is expected
|
88
|
-
|
89
|
-
*** Keywords ***
|
90
|
-
Validate Using Test Endpoint Keyword
|
91
|
-
[Arguments] ${path} ${method} ${status_code}
|
92
|
-
Test Endpoint
|
93
|
-
... path=${path} method=${method} status_code=${status_code}
|
94
|
-
|
95
|
-
```
|
96
|
-
|
97
|
-
Running the above suite for the first time is likely to result in some
|
98
|
-
errors / failed tests.
|
99
|
-
You should look at the Robot Framework `log.html` to determine the reasons
|
100
|
-
for the failing tests.
|
101
|
-
Depending on the reasons for the failures, different solutions are possible.
|
102
|
-
|
103
|
-
Details about the OpenApiDriver library parameters that you may need can be found
|
104
|
-
[here](https://marketsquare.github.io/robotframework-openapidriver/openapidriver.html).
|
105
|
-
|
106
|
-
The OpenApiDriver also support handling of relations between resources within the scope
|
107
|
-
of the API being validated as well as handling dependencies on resources outside the
|
108
|
-
scope of the API. In addition there is support for handling restrictions on the values
|
109
|
-
of parameters and properties.
|
110
|
-
|
111
|
-
Details about the `mappings_path` variable usage can be found
|
112
|
-
[here](https://marketsquare.github.io/robotframework-openapi-libcore/advanced_use.html).
|
113
|
-
|
114
|
-
---
|
115
|
-
|
116
|
-
## Limitations
|
117
|
-
|
118
|
-
There are currently a number of limitations to supported API structures, supported
|
119
|
-
data types and properties. The following list details the most important ones:
|
120
|
-
- Only JSON request and response bodies are supported.
|
121
|
-
- No support for per-path authorization levels (only simple 401 / 403 validation).
|
122
|
-
|
123
|
-
"""
|
124
|
-
|
125
1
|
from collections.abc import Mapping, MutableMapping
|
126
2
|
from pathlib import Path
|
127
3
|
from types import MappingProxyType
|
@@ -136,17 +12,16 @@ from OpenApiDriver.openapi_executors import OpenApiExecutors
|
|
136
12
|
from OpenApiDriver.openapi_reader import OpenApiReader
|
137
13
|
from OpenApiLibCore import ValidationLevel
|
138
14
|
from OpenApiLibCore.annotations import JSON
|
15
|
+
from openapitools_docs.docstrings import (
|
16
|
+
OPENAPIDRIVER_INIT_DOCSTRING,
|
17
|
+
OPENAPIDRIVER_LIBRARY_DOCSTRING,
|
18
|
+
)
|
139
19
|
|
140
20
|
default_str_mapping: Mapping[str, str] = MappingProxyType({})
|
141
21
|
|
142
22
|
|
143
|
-
@library(scope="SUITE", doc_format="
|
23
|
+
@library(scope="SUITE", doc_format="HTML")
|
144
24
|
class OpenApiDriver(OpenApiExecutors, DataDriver):
|
145
|
-
"""
|
146
|
-
Visit the [https://github.com/MarketSquare/robotframework-openapidriver | library page]
|
147
|
-
for an introduction and examples.
|
148
|
-
"""
|
149
|
-
|
150
25
|
def __init__( # noqa: PLR0913, pylint: disable=dangerous-default-value
|
151
26
|
self,
|
152
27
|
source: str,
|
@@ -174,144 +49,7 @@ class OpenApiDriver(OpenApiExecutors, DataDriver):
|
|
174
49
|
extra_headers: Mapping[str, str] = default_str_mapping,
|
175
50
|
cookies: MutableMapping[str, str] | CookieJar | None = None,
|
176
51
|
proxies: MutableMapping[str, str] | None = None,
|
177
|
-
):
|
178
|
-
"""
|
179
|
-
== Base parameters ==
|
180
|
-
|
181
|
-
=== source ===
|
182
|
-
An absolute path to an openapi.json or openapi.yaml file or an url to such a file.
|
183
|
-
|
184
|
-
=== origin ===
|
185
|
-
The server (and port) of the target server. E.g. ``https://localhost:8000``
|
186
|
-
|
187
|
-
=== base_path ===
|
188
|
-
The routing between ``origin`` and the endpoints as found in the ``paths``
|
189
|
-
section in the openapi document.
|
190
|
-
E.g. ``/petshop/v2``.
|
191
|
-
|
192
|
-
== Test case generation and execution ==
|
193
|
-
|
194
|
-
=== included_paths ===
|
195
|
-
A list of paths that will be included when generating the test cases.
|
196
|
-
The ``*`` character can be used at the end of a partial path to include all paths
|
197
|
-
starting with the partial path (wildcard include).
|
198
|
-
|
199
|
-
=== ignored_paths ===
|
200
|
-
A list of paths that will be ignored when generating the test cases.
|
201
|
-
The ``*`` character can be used at the end of a partial path to ignore all paths
|
202
|
-
starting with the partial path (wildcard ignore).
|
203
|
-
|
204
|
-
=== ignored_responses ===
|
205
|
-
A list of responses that will be ignored when generating the test cases.
|
206
|
-
|
207
|
-
=== ignored_testcases ===
|
208
|
-
A list of specific test cases that, if it would be generated, will be ignored.
|
209
|
-
Specific test cases to ignore must be specified as a ``tuple`` or ``list``
|
210
|
-
of ``path``, ``method`` and ``response``.
|
211
|
-
|
212
|
-
=== response_validation ===
|
213
|
-
By default, a ``WARN`` is logged when the Response received after a Request does not
|
214
|
-
comply with the schema as defined in the openapi document for the given operation. The
|
215
|
-
following values are supported:
|
216
|
-
|
217
|
-
- ``DISABLED``: All Response validation errors will be ignored
|
218
|
-
- ``INFO``: Any Response validation erros will be logged at ``INFO`` level
|
219
|
-
- ``WARN``: Any Response validation erros will be logged at ``WARN`` level
|
220
|
-
- ``STRICT``: The Test Case will fail on any Response validation errors
|
221
|
-
|
222
|
-
=== disable_server_validation ===
|
223
|
-
If enabled by setting this parameter to ``True``, the Response validation will also
|
224
|
-
include possible errors for Requests made to a server address that is not defined in
|
225
|
-
the list of servers in the openapi document. This generally means that if there is a
|
226
|
-
mismatch, every Test Case will raise this error. Note that ``localhost`` and
|
227
|
-
``127.0.0.1`` are not considered the same by Response validation.
|
228
|
-
|
229
|
-
== API-specific configurations ==
|
230
|
-
|
231
|
-
=== mappings_path ===
|
232
|
-
See [https://marketsquare.github.io/robotframework-openapi-libcore/advanced_use.html | this page]
|
233
|
-
for an in-depth explanation.
|
234
|
-
|
235
|
-
=== invalid_property_default_response ===
|
236
|
-
The default response code for requests with a JSON body that does not comply
|
237
|
-
with the schema.
|
238
|
-
Example: a value outside the specified range or a string value
|
239
|
-
for a property defined as integer in the schema.
|
240
|
-
|
241
|
-
=== default_id_property_name ===
|
242
|
-
The default name for the property that identifies a resource (i.e. a unique
|
243
|
-
entity) within the API.
|
244
|
-
The default value for this property name is ``id``.
|
245
|
-
If the target API uses a different name for all the resources within the API,
|
246
|
-
you can configure it globally using this property.
|
247
|
-
|
248
|
-
If different property names are used for the unique identifier for different
|
249
|
-
types of resources, an ``ID_MAPPING`` can be implemented using the ``mappings_path``.
|
250
|
-
|
251
|
-
=== faker_locale ===
|
252
|
-
A locale string or list of locale strings to pass to the Faker library to be
|
253
|
-
used in generation of string data for supported format types.
|
254
|
-
|
255
|
-
=== require_body_for_invalid_url ===
|
256
|
-
When a request is made against an invalid url, this usually is because of a "404" request;
|
257
|
-
a request for a resource that does not exist. Depending on API implementation, when a
|
258
|
-
request with a missing or invalid request body is made on a non-existent resource,
|
259
|
-
either a 404 or a 422 or 400 Response is normally returned. If the API being tested
|
260
|
-
processes the request body before checking if the requested resource exists, set
|
261
|
-
this parameter to True.
|
262
|
-
|
263
|
-
== Parsing parameters ==
|
264
|
-
|
265
|
-
=== recursion_limit ===
|
266
|
-
The recursion depth to which to fully parse recursive references before the
|
267
|
-
`recursion_default` is used to end the recursion.
|
268
|
-
|
269
|
-
=== recursion_default ===
|
270
|
-
The value that is used instead of the referenced schema when the
|
271
|
-
`recursion_limit` has been reached.
|
272
|
-
The default `{}` represents an empty object in JSON.
|
273
|
-
Depending on schema definitions, this may cause schema validation errors.
|
274
|
-
If this is the case, 'None' (``${NONE}`` in Robot Framework) or an empty list
|
275
|
-
can be tried as an alternative.
|
276
|
-
|
277
|
-
== Security-related parameters ==
|
278
|
-
_Note: these parameters are equivalent to those in the ``requests`` library._
|
279
|
-
|
280
|
-
=== username ===
|
281
|
-
The username to be used for Basic Authentication.
|
282
|
-
|
283
|
-
=== password ===
|
284
|
-
The password to be used for Basic Authentication.
|
285
|
-
|
286
|
-
=== security_token ===
|
287
|
-
The token to be used for token based security using the ``Authorization`` header.
|
288
|
-
|
289
|
-
=== auth ===
|
290
|
-
A [https://requests.readthedocs.io/en/latest/api/#authentication | requests ``AuthBase`` instance]
|
291
|
-
to be used for authentication instead of the ``username`` and ``password``.
|
292
|
-
|
293
|
-
=== cert ===
|
294
|
-
The SSL certificate to use with all requests.
|
295
|
-
If string: the path to ssl client cert file (.pem).
|
296
|
-
If tuple: the ('cert', 'key') pair.
|
297
|
-
|
298
|
-
=== verify_tls ===
|
299
|
-
Whether or not to verify the TLS / SSL certificate of the server.
|
300
|
-
If boolean: whether or not to verify the server TLS certificate.
|
301
|
-
If string: path to a CA bundle to use for verification.
|
302
|
-
|
303
|
-
=== extra_headers ===
|
304
|
-
A dictionary with extra / custom headers that will be send with every request.
|
305
|
-
This parameter can be used to send headers that are not documented in the
|
306
|
-
openapi document or to provide an API-key.
|
307
|
-
|
308
|
-
=== cookies ===
|
309
|
-
A dictionary or [https://docs.python.org/3/library/http.cookiejar.html#http.cookiejar.CookieJar | CookieJar object]
|
310
|
-
to send with all requests.
|
311
|
-
|
312
|
-
=== proxies ===
|
313
|
-
A dictionary of 'protocol': 'proxy url' to use for all requests.
|
314
|
-
"""
|
52
|
+
) -> None:
|
315
53
|
included_paths = included_paths if included_paths else ()
|
316
54
|
ignored_paths = ignored_paths if ignored_paths else ()
|
317
55
|
ignored_responses = ignored_responses if ignored_responses else ()
|
@@ -354,16 +92,7 @@ class OpenApiDriver(OpenApiExecutors, DataDriver):
|
|
354
92
|
ignored_testcases=ignored_testcases,
|
355
93
|
)
|
356
94
|
|
95
|
+
__init__.__doc__ = OPENAPIDRIVER_INIT_DOCSTRING
|
357
96
|
|
358
|
-
class DocumentationGenerator(OpenApiDriver):
|
359
|
-
__doc__ = OpenApiDriver.__doc__
|
360
97
|
|
361
|
-
|
362
|
-
def get_keyword_names() -> list[str]:
|
363
|
-
"""Curated keywords for libdoc and libspec."""
|
364
|
-
return [
|
365
|
-
"test_unauthorized",
|
366
|
-
"test_forbidden",
|
367
|
-
"test_invalid_url",
|
368
|
-
"test_endpoint",
|
369
|
-
] # pragma: no cover
|
98
|
+
OpenApiDriver.__doc__ = OPENAPIDRIVER_LIBRARY_DOCSTRING
|
OpenApiLibCore/__init__.py
CHANGED
@@ -36,6 +36,32 @@ except Exception: # pragma: no cover pylint: disable=broad-exception-caught
|
|
36
36
|
pass
|
37
37
|
|
38
38
|
|
39
|
+
KEYWORD_NAMES = [
|
40
|
+
"set_origin",
|
41
|
+
"set_security_token",
|
42
|
+
"set_basic_auth",
|
43
|
+
"set_auth",
|
44
|
+
"set_extra_headers",
|
45
|
+
"get_request_values",
|
46
|
+
"get_request_data",
|
47
|
+
"get_invalid_body_data",
|
48
|
+
"get_invalidated_parameters",
|
49
|
+
"get_json_data_with_conflict",
|
50
|
+
"get_valid_url",
|
51
|
+
"get_valid_id_for_path",
|
52
|
+
"get_parameterized_path_from_url",
|
53
|
+
"get_ids_from_url",
|
54
|
+
"get_invalidated_url",
|
55
|
+
"ensure_in_use",
|
56
|
+
"authorized_request",
|
57
|
+
"perform_validated_request",
|
58
|
+
"validate_response_using_validator",
|
59
|
+
"assert_href_to_resource_is_valid",
|
60
|
+
"validate_response",
|
61
|
+
"validate_send_response",
|
62
|
+
]
|
63
|
+
|
64
|
+
|
39
65
|
__all__ = [
|
40
66
|
"IGNORE",
|
41
67
|
"UNSET",
|
File without changes
|
@@ -8,7 +8,7 @@ from typing import Any
|
|
8
8
|
|
9
9
|
from robot.api import logger
|
10
10
|
|
11
|
-
import OpenApiLibCore.path_functions as
|
11
|
+
import OpenApiLibCore.path_functions as _path_functions
|
12
12
|
from OpenApiLibCore.annotations import JSON
|
13
13
|
from OpenApiLibCore.dto_base import (
|
14
14
|
Dto,
|
@@ -72,7 +72,7 @@ def get_dict_data_for_dto_class(
|
|
72
72
|
property_names = get_property_names_to_process(schema=schema, dto_class=dto_class)
|
73
73
|
|
74
74
|
for property_name in property_names:
|
75
|
-
property_schema = schema.properties.root[property_name]
|
75
|
+
property_schema = schema.properties.root[property_name] # type: ignore[union-attr]
|
76
76
|
if property_schema.readOnly:
|
77
77
|
continue
|
78
78
|
|
@@ -178,7 +178,7 @@ def get_property_names_to_process(
|
|
178
178
|
) -> list[str]:
|
179
179
|
property_names = []
|
180
180
|
|
181
|
-
for property_name in schema.properties.root:
|
181
|
+
for property_name in schema.properties.root: # type: ignore[union-attr]
|
182
182
|
# register the oas_name
|
183
183
|
_ = get_safe_name_for_oas_name(property_name)
|
184
184
|
if constrained_values := get_constrained_values(
|
@@ -243,7 +243,7 @@ def get_dependent_id(
|
|
243
243
|
except ValueError:
|
244
244
|
return None
|
245
245
|
|
246
|
-
valid_id =
|
246
|
+
valid_id = _path_functions.get_valid_id_for_path(
|
247
247
|
path=id_get_path, get_id_property_name=get_id_property_name
|
248
248
|
)
|
249
249
|
logger.debug(f"get_dependent_id for {id_get_path} returned {valid_id}")
|
@@ -3,14 +3,13 @@ Module holding the main functions related to data generation
|
|
3
3
|
for the requests made as part of keyword exection.
|
4
4
|
"""
|
5
5
|
|
6
|
-
import re
|
7
6
|
from dataclasses import Field, field, make_dataclass
|
8
7
|
from random import choice
|
9
8
|
from typing import Any, cast
|
10
9
|
|
11
10
|
from robot.api import logger
|
12
11
|
|
13
|
-
import OpenApiLibCore.path_functions as
|
12
|
+
import OpenApiLibCore.path_functions as _path_functions
|
14
13
|
from OpenApiLibCore.annotations import JSON
|
15
14
|
from OpenApiLibCore.dto_base import (
|
16
15
|
Dto,
|
@@ -23,7 +22,6 @@ from OpenApiLibCore.models import (
|
|
23
22
|
OpenApiObject,
|
24
23
|
OperationObject,
|
25
24
|
ParameterObject,
|
26
|
-
RequestBodyObject,
|
27
25
|
UnionTypeSchema,
|
28
26
|
)
|
29
27
|
from OpenApiLibCore.parameter_utils import get_safe_name_for_oas_name
|
@@ -47,11 +45,13 @@ def get_request_data(
|
|
47
45
|
dto_cls_name = get_dto_cls_name(path=path, method=method)
|
48
46
|
# The path can contain already resolved Ids that have to be matched
|
49
47
|
# against the parametrized paths in the paths section.
|
50
|
-
spec_path =
|
48
|
+
spec_path = _path_functions.get_parametrized_path(
|
49
|
+
path=path, openapi_spec=openapi_spec
|
50
|
+
)
|
51
51
|
dto_class = get_dto_class(path=spec_path, method=method)
|
52
52
|
try:
|
53
53
|
path_item = openapi_spec.paths[spec_path]
|
54
|
-
operation_spec = getattr(path_item, method)
|
54
|
+
operation_spec: OperationObject | None = getattr(path_item, method)
|
55
55
|
if operation_spec is None:
|
56
56
|
raise AttributeError
|
57
57
|
except AttributeError:
|
@@ -77,38 +77,30 @@ def get_request_data(
|
|
77
77
|
has_body=False,
|
78
78
|
)
|
79
79
|
|
80
|
-
|
81
|
-
|
82
|
-
body_schema = operation_spec.requestBody
|
83
|
-
media_type_dict = body_schema.content
|
84
|
-
supported_types = [v for k, v in media_type_dict.items() if "json" in k]
|
85
|
-
supported_schemas = [t.schema_ for t in supported_types if t.schema_ is not None]
|
80
|
+
body_schema = operation_spec.requestBody.schema_
|
86
81
|
|
87
|
-
if not
|
88
|
-
raise ValueError(
|
89
|
-
|
90
|
-
if len(supported_schemas) > 1:
|
91
|
-
logger.warn(
|
92
|
-
f"Multiple JSON media types defined for requestBody, using the first candidate {media_type_dict}"
|
82
|
+
if not body_schema:
|
83
|
+
raise ValueError(
|
84
|
+
f"No supported content schema found: {operation_spec.requestBody.content}"
|
93
85
|
)
|
94
86
|
|
95
|
-
|
87
|
+
headers.update({"content-type": operation_spec.requestBody.mime_type})
|
96
88
|
|
97
|
-
if isinstance(
|
98
|
-
resolved_schemas =
|
99
|
-
|
89
|
+
if isinstance(body_schema, UnionTypeSchema):
|
90
|
+
resolved_schemas = body_schema.resolved_schemas
|
91
|
+
body_schema = choice(resolved_schemas)
|
100
92
|
|
101
|
-
if not isinstance(
|
102
|
-
raise ValueError(f"Selected schema is not an object schema: {
|
93
|
+
if not isinstance(body_schema, ObjectSchema):
|
94
|
+
raise ValueError(f"Selected schema is not an object schema: {body_schema}")
|
103
95
|
|
104
96
|
dto_data = _get_json_data_for_dto_class(
|
105
|
-
schema=
|
97
|
+
schema=body_schema,
|
106
98
|
dto_class=dto_class,
|
107
99
|
get_id_property_name=get_id_property_name,
|
108
100
|
operation_id=operation_spec.operationId,
|
109
101
|
)
|
110
102
|
dto_instance = _get_dto_instance_from_dto_data(
|
111
|
-
object_schema=
|
103
|
+
object_schema=body_schema,
|
112
104
|
dto_class=dto_class,
|
113
105
|
dto_data=dto_data,
|
114
106
|
method_spec=operation_spec,
|
@@ -116,7 +108,7 @@ def get_request_data(
|
|
116
108
|
)
|
117
109
|
return RequestData(
|
118
110
|
dto=dto_instance,
|
119
|
-
body_schema=
|
111
|
+
body_schema=body_schema,
|
120
112
|
parameters=parameters,
|
121
113
|
params=params,
|
122
114
|
headers=headers,
|
@@ -198,25 +190,6 @@ def get_dto_cls_name(path: str, method: str) -> str:
|
|
198
190
|
return result
|
199
191
|
|
200
192
|
|
201
|
-
def get_content_type(body_spec: RequestBodyObject) -> str:
|
202
|
-
"""Get and validate the first supported content type from the requested body spec
|
203
|
-
|
204
|
-
Should be application/json like content type,
|
205
|
-
e.g "application/json;charset=utf-8" or "application/merge-patch+json"
|
206
|
-
"""
|
207
|
-
content_types: list[str] = list(body_spec.content.keys())
|
208
|
-
json_regex = r"application/([a-z\-]+\+)?json(;\s?charset=(.+))?"
|
209
|
-
for content_type in content_types:
|
210
|
-
if re.search(json_regex, content_type):
|
211
|
-
return content_type
|
212
|
-
|
213
|
-
# At present no supported for other types.
|
214
|
-
raise NotImplementedError(
|
215
|
-
f"Only content types like 'application/json' are supported. "
|
216
|
-
f"Content types definded in the spec are '{content_types}'."
|
217
|
-
)
|
218
|
-
|
219
|
-
|
220
193
|
def get_request_parameters(
|
221
194
|
dto_class: Dto | type[Dto], method_spec: OperationObject
|
222
195
|
) -> tuple[list[ParameterObject], dict[str, Any], dict[str, str]]:
|
@@ -16,7 +16,6 @@ from OpenApiLibCore.dto_base import (
|
|
16
16
|
NOT_SET,
|
17
17
|
Dto,
|
18
18
|
IdReference,
|
19
|
-
PathPropertiesConstraint,
|
20
19
|
PropertyValueConstraint,
|
21
20
|
UniquePropertyValueConstraint,
|
22
21
|
)
|
@@ -35,10 +34,7 @@ def get_invalid_body_data(
|
|
35
34
|
invalid_property_default_response: int,
|
36
35
|
) -> dict[str, Any]:
|
37
36
|
method = method.lower()
|
38
|
-
data_relations = request_data.dto.
|
39
|
-
data_relations = [
|
40
|
-
r for r in data_relations if not isinstance(r, PathPropertiesConstraint)
|
41
|
-
]
|
37
|
+
data_relations = request_data.dto.get_body_relations_for_error_code(status_code)
|
42
38
|
if not data_relations:
|
43
39
|
if request_data.body_schema is None:
|
44
40
|
raise ValueError(
|
OpenApiLibCore/dto_base.py
CHANGED
@@ -83,17 +83,17 @@ class Dto(ABC):
|
|
83
83
|
"""Base class for the Dto class."""
|
84
84
|
|
85
85
|
@staticmethod
|
86
|
-
def
|
86
|
+
def get_path_relations() -> list[PathPropertiesConstraint]:
|
87
87
|
"""Return the list of Relations for the header and query parameters."""
|
88
88
|
return []
|
89
89
|
|
90
|
-
def
|
90
|
+
def get_path_relations_for_error_code(
|
91
91
|
self, error_code: int
|
92
|
-
) -> list[
|
92
|
+
) -> list[PathPropertiesConstraint]:
|
93
93
|
"""Return the list of Relations associated with the given error_code."""
|
94
|
-
relations: list[
|
94
|
+
relations: list[PathPropertiesConstraint] = [
|
95
95
|
r
|
96
|
-
for r in self.
|
96
|
+
for r in self.get_path_relations()
|
97
97
|
if r.error_code == error_code
|
98
98
|
or (
|
99
99
|
getattr(r, "invalid_value_error_code", None) == error_code
|
@@ -103,15 +103,17 @@ class Dto(ABC):
|
|
103
103
|
return relations
|
104
104
|
|
105
105
|
@staticmethod
|
106
|
-
def
|
107
|
-
"""Return the list of Relations for the
|
106
|
+
def get_parameter_relations() -> list[ResourceRelation]:
|
107
|
+
"""Return the list of Relations for the header and query parameters."""
|
108
108
|
return []
|
109
109
|
|
110
|
-
def
|
110
|
+
def get_parameter_relations_for_error_code(
|
111
|
+
self, error_code: int
|
112
|
+
) -> list[ResourceRelation]:
|
111
113
|
"""Return the list of Relations associated with the given error_code."""
|
112
114
|
relations: list[ResourceRelation] = [
|
113
115
|
r
|
114
|
-
for r in self.
|
116
|
+
for r in self.get_parameter_relations()
|
115
117
|
if r.error_code == error_code
|
116
118
|
or (
|
117
119
|
getattr(r, "invalid_value_error_code", None) == error_code
|
@@ -120,6 +122,11 @@ class Dto(ABC):
|
|
120
122
|
]
|
121
123
|
return relations
|
122
124
|
|
125
|
+
@staticmethod
|
126
|
+
def get_relations() -> list[ResourceRelation]:
|
127
|
+
"""Return the list of Relations for the (json) body."""
|
128
|
+
return []
|
129
|
+
|
123
130
|
def get_body_relations_for_error_code(
|
124
131
|
self, error_code: int
|
125
132
|
) -> list[ResourceRelation]:
|
@@ -127,8 +134,16 @@ class Dto(ABC):
|
|
127
134
|
Return the list of Relations associated with the given error_code that are
|
128
135
|
applicable to the body / payload of the request.
|
129
136
|
"""
|
130
|
-
|
131
|
-
|
137
|
+
relations: list[ResourceRelation] = [
|
138
|
+
r
|
139
|
+
for r in self.get_relations()
|
140
|
+
if r.error_code == error_code
|
141
|
+
or (
|
142
|
+
getattr(r, "invalid_value_error_code", None) == error_code
|
143
|
+
and getattr(r, "invalid_value", None) != NOT_SET
|
144
|
+
)
|
145
|
+
]
|
146
|
+
return relations
|
132
147
|
|
133
148
|
def get_invalidated_data(
|
134
149
|
self,
|
@@ -139,17 +154,11 @@ class Dto(ABC):
|
|
139
154
|
"""Return a data set with one of the properties set to an invalid value or type."""
|
140
155
|
properties: dict[str, Any] = self.as_dict()
|
141
156
|
|
142
|
-
|
143
|
-
|
144
|
-
relations = self.get_relations_for_error_code(error_code=status_code)
|
145
|
-
# filter PathProperyConstraints since in that case no data can be invalidated
|
146
|
-
relations = [
|
147
|
-
r for r in relations if not isinstance(r, PathPropertiesConstraint)
|
148
|
-
]
|
157
|
+
relations = self.get_body_relations_for_error_code(error_code=status_code)
|
149
158
|
property_names = [r.property_name for r in relations]
|
150
159
|
if status_code == invalid_property_default_code:
|
151
160
|
# add all properties defined in the schema, including optional properties
|
152
|
-
property_names.extend((schema.properties.root.keys()))
|
161
|
+
property_names.extend((schema.properties.root.keys())) # type: ignore[union-attr]
|
153
162
|
if not property_names:
|
154
163
|
raise ValueError(
|
155
164
|
f"No property can be invalidated to cause status_code {status_code}"
|
@@ -167,8 +176,8 @@ class Dto(ABC):
|
|
167
176
|
if id_dependencies:
|
168
177
|
invalid_id = uuid4().hex
|
169
178
|
logger.debug(
|
170
|
-
f"Breaking IdDependency for status_code {status_code}:
|
171
|
-
f"{
|
179
|
+
f"Breaking IdDependency for status_code {status_code}: setting "
|
180
|
+
f"{property_name} to {invalid_id}"
|
172
181
|
)
|
173
182
|
properties[property_name] = invalid_id
|
174
183
|
return properties
|
@@ -191,7 +200,7 @@ class Dto(ABC):
|
|
191
200
|
)
|
192
201
|
return properties
|
193
202
|
|
194
|
-
value_schema = schema.properties.root[property_name]
|
203
|
+
value_schema = schema.properties.root[property_name] # type: ignore[union-attr]
|
195
204
|
if isinstance(value_schema, UnionTypeSchema):
|
196
205
|
# Filter "type": "null" from the possible types since this indicates an
|
197
206
|
# optional / nullable property that can only be invalidated by sending
|