robotframework-openapitools 0.3.0__py3-none-any.whl → 1.0.0__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 +45 -41
- OpenApiDriver/openapi_executors.py +83 -49
- OpenApiDriver/openapi_reader.py +114 -116
- OpenApiDriver/openapidriver.libspec +209 -133
- OpenApiDriver/openapidriver.py +31 -296
- OpenApiLibCore/__init__.py +39 -13
- OpenApiLibCore/annotations.py +10 -0
- OpenApiLibCore/data_generation/__init__.py +10 -0
- OpenApiLibCore/data_generation/body_data_generation.py +250 -0
- OpenApiLibCore/data_generation/data_generation_core.py +233 -0
- OpenApiLibCore/data_invalidation.py +294 -0
- OpenApiLibCore/dto_base.py +75 -129
- OpenApiLibCore/dto_utils.py +125 -85
- OpenApiLibCore/localized_faker.py +88 -0
- OpenApiLibCore/models.py +723 -0
- OpenApiLibCore/oas_cache.py +14 -13
- OpenApiLibCore/openapi_libcore.libspec +363 -322
- OpenApiLibCore/openapi_libcore.py +388 -1903
- OpenApiLibCore/parameter_utils.py +97 -0
- OpenApiLibCore/path_functions.py +215 -0
- OpenApiLibCore/path_invalidation.py +42 -0
- OpenApiLibCore/protocols.py +38 -0
- OpenApiLibCore/request_data.py +246 -0
- OpenApiLibCore/resource_relations.py +55 -0
- OpenApiLibCore/validation.py +380 -0
- OpenApiLibCore/value_utils.py +216 -481
- openapi_libgen/__init__.py +3 -0
- openapi_libgen/command_line.py +75 -0
- openapi_libgen/generator.py +82 -0
- openapi_libgen/parsing_utils.py +30 -0
- openapi_libgen/spec_parser.py +154 -0
- openapi_libgen/templates/__init__.jinja +3 -0
- openapi_libgen/templates/library.jinja +30 -0
- robotframework_openapitools-1.0.0.dist-info/METADATA +249 -0
- robotframework_openapitools-1.0.0.dist-info/RECORD +40 -0
- {robotframework_openapitools-0.3.0.dist-info → robotframework_openapitools-1.0.0.dist-info}/WHEEL +1 -1
- robotframework_openapitools-1.0.0.dist-info/entry_points.txt +3 -0
- roboswag/__init__.py +0 -9
- roboswag/__main__.py +0 -3
- roboswag/auth.py +0 -44
- roboswag/cli.py +0 -80
- roboswag/core.py +0 -85
- roboswag/generate/__init__.py +0 -1
- roboswag/generate/generate.py +0 -121
- roboswag/generate/models/__init__.py +0 -0
- roboswag/generate/models/api.py +0 -219
- roboswag/generate/models/definition.py +0 -28
- roboswag/generate/models/endpoint.py +0 -68
- roboswag/generate/models/parameter.py +0 -25
- roboswag/generate/models/response.py +0 -8
- roboswag/generate/models/tag.py +0 -16
- roboswag/generate/models/utils.py +0 -60
- roboswag/generate/templates/api_init.jinja +0 -15
- roboswag/generate/templates/models.jinja +0 -7
- roboswag/generate/templates/paths.jinja +0 -68
- roboswag/logger.py +0 -33
- roboswag/validate/__init__.py +0 -6
- roboswag/validate/core.py +0 -3
- roboswag/validate/schema.py +0 -21
- roboswag/validate/text_response.py +0 -14
- robotframework_openapitools-0.3.0.dist-info/METADATA +0 -41
- robotframework_openapitools-0.3.0.dist-info/RECORD +0 -41
- {robotframework_openapitools-0.3.0.dist-info → robotframework_openapitools-1.0.0.dist-info}/LICENSE +0 -0
OpenApiDriver/__init__.py
CHANGED
@@ -1,41 +1,45 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
The
|
4
|
-
|
5
|
-
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
from
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
"
|
36
|
-
"
|
37
|
-
"
|
38
|
-
"
|
39
|
-
"
|
40
|
-
"
|
41
|
-
|
1
|
+
# pylint: disable=invalid-name
|
2
|
+
"""
|
3
|
+
The OpenApiDriver package is intended to be used as a Robot Framework library.
|
4
|
+
The following classes and constants are exposed to be used by the library user:
|
5
|
+
- OpenApiDriver: The class to be used as a Library in the *** Settings *** section
|
6
|
+
- IdDependency, IdReference, PathPropertiesConstraint, PropertyValueConstraint,
|
7
|
+
UniquePropertyValueConstraint: Classes to be subclassed by the library user
|
8
|
+
when implementing a custom mapping module (advanced use).
|
9
|
+
- Dto, Relation: Base classes that can be used for type annotations.
|
10
|
+
- IGNORE: A special constant that can be used as a value in the PropertyValueConstraint.
|
11
|
+
"""
|
12
|
+
|
13
|
+
from importlib.metadata import version
|
14
|
+
|
15
|
+
from OpenApiDriver.openapidriver import OpenApiDriver
|
16
|
+
from OpenApiLibCore.dto_base import (
|
17
|
+
Dto,
|
18
|
+
IdDependency,
|
19
|
+
IdReference,
|
20
|
+
PathPropertiesConstraint,
|
21
|
+
PropertyValueConstraint,
|
22
|
+
ResourceRelation,
|
23
|
+
UniquePropertyValueConstraint,
|
24
|
+
)
|
25
|
+
from OpenApiLibCore.validation import ValidationLevel
|
26
|
+
from OpenApiLibCore.value_utils import IGNORE
|
27
|
+
|
28
|
+
try:
|
29
|
+
__version__ = version("robotframework-openapidriver")
|
30
|
+
except Exception: # pragma: no cover pylint: disable=broad-exception-caught
|
31
|
+
pass
|
32
|
+
|
33
|
+
|
34
|
+
__all__ = [
|
35
|
+
"IGNORE",
|
36
|
+
"Dto",
|
37
|
+
"IdDependency",
|
38
|
+
"IdReference",
|
39
|
+
"OpenApiDriver",
|
40
|
+
"PathPropertiesConstraint",
|
41
|
+
"PropertyValueConstraint",
|
42
|
+
"ResourceRelation",
|
43
|
+
"UniquePropertyValueConstraint",
|
44
|
+
"ValidationLevel",
|
45
|
+
]
|
@@ -1,52 +1,70 @@
|
|
1
1
|
"""Module containing the classes to perform automatic OpenAPI contract validation."""
|
2
2
|
|
3
|
-
from
|
3
|
+
from collections.abc import Mapping, MutableMapping
|
4
|
+
from http import HTTPStatus
|
5
|
+
from os import getenv
|
4
6
|
from pathlib import Path
|
5
7
|
from random import choice
|
6
|
-
from
|
8
|
+
from types import MappingProxyType
|
7
9
|
|
8
10
|
from requests import Response
|
9
11
|
from requests.auth import AuthBase
|
10
12
|
from requests.cookies import RequestsCookieJar as CookieJar
|
11
|
-
from robot.api import
|
13
|
+
from robot.api import logger
|
12
14
|
from robot.api.deco import keyword, library
|
15
|
+
from robot.api.exceptions import SkipExecution
|
13
16
|
from robot.libraries.BuiltIn import BuiltIn
|
14
17
|
|
15
|
-
from OpenApiLibCore import
|
18
|
+
from OpenApiLibCore import (
|
19
|
+
KEYWORD_NAMES as LIBCORE_KEYWORD_NAMES,
|
20
|
+
)
|
21
|
+
from OpenApiLibCore import (
|
22
|
+
OpenApiLibCore,
|
23
|
+
RequestData,
|
24
|
+
RequestValues,
|
25
|
+
ValidationLevel,
|
26
|
+
)
|
27
|
+
from OpenApiLibCore.annotations import JSON
|
16
28
|
|
17
29
|
run_keyword = BuiltIn().run_keyword
|
30
|
+
default_str_mapping: Mapping[str, str] = MappingProxyType({})
|
18
31
|
|
19
32
|
|
20
|
-
|
33
|
+
KEYWORD_NAMES = [
|
34
|
+
"test_unauthorized",
|
35
|
+
"test_forbidden",
|
36
|
+
"test_invalid_url",
|
37
|
+
"test_endpoint",
|
38
|
+
]
|
21
39
|
|
22
40
|
|
23
41
|
@library(scope="SUITE", doc_format="ROBOT")
|
24
|
-
class OpenApiExecutors(OpenApiLibCore):
|
42
|
+
class OpenApiExecutors(OpenApiLibCore):
|
25
43
|
"""Main class providing the keywords and core logic to perform endpoint validations."""
|
26
44
|
|
27
|
-
def __init__( # pylint: disable=
|
45
|
+
def __init__( # noqa: PLR0913, pylint: disable=dangerous-default-value
|
28
46
|
self,
|
29
47
|
source: str,
|
30
48
|
origin: str = "",
|
31
49
|
base_path: str = "",
|
32
50
|
response_validation: ValidationLevel = ValidationLevel.WARN,
|
33
51
|
disable_server_validation: bool = True,
|
34
|
-
mappings_path:
|
52
|
+
mappings_path: str | Path = "",
|
35
53
|
invalid_property_default_response: int = 422,
|
36
54
|
default_id_property_name: str = "id",
|
37
|
-
faker_locale:
|
55
|
+
faker_locale: str | list[str] = "",
|
38
56
|
require_body_for_invalid_url: bool = False,
|
39
57
|
recursion_limit: int = 1,
|
40
|
-
recursion_default:
|
58
|
+
recursion_default: JSON = {},
|
41
59
|
username: str = "",
|
42
60
|
password: str = "",
|
43
61
|
security_token: str = "",
|
44
|
-
auth:
|
45
|
-
cert:
|
46
|
-
verify_tls:
|
47
|
-
extra_headers:
|
48
|
-
cookies:
|
49
|
-
proxies:
|
62
|
+
auth: AuthBase | None = None,
|
63
|
+
cert: str | tuple[str, str] = "",
|
64
|
+
verify_tls: bool | str = True,
|
65
|
+
extra_headers: Mapping[str, str] = default_str_mapping,
|
66
|
+
cookies: MutableMapping[str, str] | CookieJar | None = None,
|
67
|
+
proxies: MutableMapping[str, str] | None = None,
|
50
68
|
) -> None:
|
51
69
|
super().__init__(
|
52
70
|
source=source,
|
@@ -84,13 +102,13 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
|
|
84
102
|
> Note: No headers or (json) body are send with the request. For security
|
85
103
|
reasons, the authorization validation should be checked first.
|
86
104
|
"""
|
87
|
-
url: str = run_keyword("get_valid_url", path
|
105
|
+
url: str = run_keyword("get_valid_url", path)
|
88
106
|
response = self.session.request(
|
89
107
|
method=method,
|
90
108
|
url=url,
|
91
109
|
verify=False,
|
92
110
|
)
|
93
|
-
if response.status_code !=
|
111
|
+
if response.status_code != int(HTTPStatus.UNAUTHORIZED):
|
94
112
|
raise AssertionError(f"Response {response.status_code} was not 401.")
|
95
113
|
|
96
114
|
@keyword
|
@@ -105,9 +123,9 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
|
|
105
123
|
> Note: No headers or (json) body are send with the request. For security
|
106
124
|
reasons, the access rights validation should be checked first.
|
107
125
|
"""
|
108
|
-
url: str = run_keyword("get_valid_url", path
|
126
|
+
url: str = run_keyword("get_valid_url", path)
|
109
127
|
response: Response = run_keyword("authorized_request", url, method)
|
110
|
-
if response.status_code !=
|
128
|
+
if response.status_code != int(HTTPStatus.FORBIDDEN):
|
111
129
|
raise AssertionError(f"Response {response.status_code} was not 403.")
|
112
130
|
|
113
131
|
@keyword
|
@@ -118,8 +136,9 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
|
|
118
136
|
Perform a request for the provided 'path' and 'method' where the url for
|
119
137
|
the `path` is invalidated.
|
120
138
|
|
121
|
-
This keyword will be `SKIPPED` if the path contains no parts
|
122
|
-
can be invalidated
|
139
|
+
This keyword will be `SKIPPED` if the path contains no parts
|
140
|
+
that can be invalidated and there is no mapping for a
|
141
|
+
PathPropertiesConstraint for the `expected_status_code`.
|
123
142
|
|
124
143
|
The optional `expected_status_code` parameter (default: 404) can be set to the
|
125
144
|
expected status code for APIs that do not return a 404 on invalid urls.
|
@@ -129,9 +148,17 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
|
|
129
148
|
parameters are send with the request. The `require_body_for_invalid_url`
|
130
149
|
parameter can be set to `True` if needed.
|
131
150
|
"""
|
132
|
-
valid_url: str = run_keyword("get_valid_url", path
|
151
|
+
valid_url: str = run_keyword("get_valid_url", path)
|
152
|
+
|
153
|
+
try:
|
154
|
+
url = run_keyword(
|
155
|
+
"get_invalidated_url", valid_url, path, expected_status_code
|
156
|
+
)
|
157
|
+
except Exception as exception:
|
158
|
+
message = getattr(exception, "message", "")
|
159
|
+
if not message.startswith("ValueError"):
|
160
|
+
raise exception # pragma: no cover
|
133
161
|
|
134
|
-
if not (url := run_keyword("get_invalidated_url", valid_url)):
|
135
162
|
raise SkipExecution(
|
136
163
|
f"Path {path} does not contain resource references that "
|
137
164
|
f"can be invalidated."
|
@@ -139,7 +166,7 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
|
|
139
166
|
|
140
167
|
params, headers, json_data = None, None, None
|
141
168
|
if self.require_body_for_invalid_url:
|
142
|
-
request_data =
|
169
|
+
request_data: RequestData = run_keyword("get_request_data", path, method)
|
143
170
|
params = request_data.params
|
144
171
|
headers = request_data.headers
|
145
172
|
dto = request_data.dto
|
@@ -164,11 +191,11 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
|
|
164
191
|
The keyword calls other keywords to generate the neccesary data to perform
|
165
192
|
the desired operation and validate the response against the openapi document.
|
166
193
|
"""
|
167
|
-
json_data:
|
168
|
-
original_data =
|
194
|
+
json_data: dict[str, JSON] = {}
|
195
|
+
original_data = {}
|
169
196
|
|
170
|
-
url: str = run_keyword("get_valid_url", path
|
171
|
-
request_data: RequestData =
|
197
|
+
url: str = run_keyword("get_valid_url", path)
|
198
|
+
request_data: RequestData = run_keyword("get_request_data", path, method)
|
172
199
|
params = request_data.params
|
173
200
|
headers = request_data.headers
|
174
201
|
if request_data.has_body:
|
@@ -177,10 +204,10 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
|
|
177
204
|
if method == "PATCH":
|
178
205
|
original_data = self.get_original_data(url=url)
|
179
206
|
# in case of a status code indicating an error, ensure the error occurs
|
180
|
-
if status_code >=
|
207
|
+
if status_code >= int(HTTPStatus.BAD_REQUEST):
|
181
208
|
invalidation_keyword_data = {
|
182
|
-
"
|
183
|
-
"
|
209
|
+
"get_invalid_body_data": [
|
210
|
+
"get_invalid_body_data",
|
184
211
|
url,
|
185
212
|
method,
|
186
213
|
status_code,
|
@@ -194,14 +221,14 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
|
|
194
221
|
}
|
195
222
|
invalidation_keywords = []
|
196
223
|
|
197
|
-
if request_data.dto.
|
198
|
-
invalidation_keywords.append("
|
224
|
+
if request_data.dto.get_body_relations_for_error_code(status_code):
|
225
|
+
invalidation_keywords.append("get_invalid_body_data")
|
199
226
|
if request_data.dto.get_parameter_relations_for_error_code(status_code):
|
200
227
|
invalidation_keywords.append("get_invalidated_parameters")
|
201
228
|
if invalidation_keywords:
|
202
229
|
if (
|
203
230
|
invalidation_keyword := choice(invalidation_keywords)
|
204
|
-
) == "
|
231
|
+
) == "get_invalid_body_data":
|
205
232
|
json_data = run_keyword(
|
206
233
|
*invalidation_keyword_data[invalidation_keyword]
|
207
234
|
)
|
@@ -219,13 +246,13 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
|
|
219
246
|
params, headers = run_keyword(
|
220
247
|
*invalidation_keyword_data["get_invalidated_parameters"]
|
221
248
|
)
|
222
|
-
if request_data.
|
249
|
+
if request_data.body_schema:
|
223
250
|
json_data = run_keyword(
|
224
|
-
*invalidation_keyword_data["
|
251
|
+
*invalidation_keyword_data["get_invalid_body_data"]
|
225
252
|
)
|
226
|
-
elif request_data.
|
253
|
+
elif request_data.body_schema:
|
227
254
|
json_data = run_keyword(
|
228
|
-
*invalidation_keyword_data["
|
255
|
+
*invalidation_keyword_data["get_invalid_body_data"]
|
229
256
|
)
|
230
257
|
else:
|
231
258
|
raise SkipExecution(
|
@@ -248,20 +275,20 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
|
|
248
275
|
),
|
249
276
|
original_data,
|
250
277
|
)
|
251
|
-
if status_code <
|
278
|
+
if status_code < int(HTTPStatus.MULTIPLE_CHOICES) and (
|
252
279
|
request_data.has_optional_properties
|
253
280
|
or request_data.has_optional_params
|
254
281
|
or request_data.has_optional_headers
|
255
282
|
):
|
256
283
|
logger.info("Performing request without optional properties and parameters")
|
257
|
-
url = run_keyword("get_valid_url", path
|
258
|
-
request_data =
|
284
|
+
url = run_keyword("get_valid_url", path)
|
285
|
+
request_data = run_keyword("get_request_data", path, method)
|
259
286
|
params = request_data.get_required_params()
|
260
287
|
headers = request_data.get_required_headers()
|
261
288
|
json_data = (
|
262
|
-
request_data.get_minimal_body_dict() if request_data.has_body else
|
289
|
+
request_data.get_minimal_body_dict() if request_data.has_body else {}
|
263
290
|
)
|
264
|
-
original_data =
|
291
|
+
original_data = {}
|
265
292
|
if method == "PATCH":
|
266
293
|
original_data = self.get_original_data(url=url)
|
267
294
|
run_keyword(
|
@@ -278,15 +305,15 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
|
|
278
305
|
original_data,
|
279
306
|
)
|
280
307
|
|
281
|
-
def get_original_data(self, url: str) ->
|
308
|
+
def get_original_data(self, url: str) -> dict[str, JSON]:
|
282
309
|
"""
|
283
310
|
Attempt to GET the current data for the given url and return it.
|
284
311
|
|
285
|
-
If the GET request fails,
|
312
|
+
If the GET request fails, an empty dict is returned.
|
286
313
|
"""
|
287
|
-
original_data =
|
288
|
-
path = self.
|
289
|
-
get_request_data =
|
314
|
+
original_data = {}
|
315
|
+
path = self.get_parameterized_path_from_url(url)
|
316
|
+
get_request_data: RequestData = run_keyword("get_request_data", path, "GET")
|
290
317
|
get_params = get_request_data.params
|
291
318
|
get_headers = get_request_data.headers
|
292
319
|
response: Response = run_keyword(
|
@@ -295,3 +322,10 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
|
|
295
322
|
if response.ok:
|
296
323
|
original_data = response.json()
|
297
324
|
return original_data
|
325
|
+
|
326
|
+
@staticmethod
|
327
|
+
def get_keyword_names() -> list[str]:
|
328
|
+
"""Curated keywords for libdoc and libspec."""
|
329
|
+
if getenv("HIDE_INHERITED_KEYWORDS") == "true":
|
330
|
+
return KEYWORD_NAMES
|
331
|
+
return KEYWORD_NAMES + LIBCORE_KEYWORD_NAMES
|
OpenApiDriver/openapi_reader.py
CHANGED
@@ -1,116 +1,114 @@
|
|
1
|
-
"""Module holding the OpenApiReader reader_class implementation."""
|
2
|
-
|
3
|
-
from typing import
|
4
|
-
|
5
|
-
from DataDriver.AbstractReaderClass import AbstractReaderClass
|
6
|
-
from DataDriver.ReaderConfig import TestCaseData
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
self.
|
18
|
-
self.
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
and self.
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
response
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
"${
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
if
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
def _get_tag_list(tags: List[str], method: str, response: str) -> List[str]:
|
116
|
-
return [*tags, f"Method: {method.upper()}", f"Response: {response}"]
|
1
|
+
"""Module holding the OpenApiReader reader_class implementation."""
|
2
|
+
|
3
|
+
from typing import Sequence
|
4
|
+
|
5
|
+
from DataDriver.AbstractReaderClass import AbstractReaderClass
|
6
|
+
from DataDriver.ReaderConfig import TestCaseData
|
7
|
+
|
8
|
+
from OpenApiLibCore.models import PathItemObject
|
9
|
+
|
10
|
+
|
11
|
+
class Test:
|
12
|
+
"""
|
13
|
+
Helper class to support ignoring endpoint responses when generating the test cases.
|
14
|
+
"""
|
15
|
+
|
16
|
+
def __init__(self, path: str, method: str, response: str | int) -> None:
|
17
|
+
self.path = path
|
18
|
+
self.method = method.lower()
|
19
|
+
self.response = str(response)
|
20
|
+
|
21
|
+
def __eq__(self, other: object) -> bool:
|
22
|
+
if not isinstance(other, type(self)):
|
23
|
+
return False
|
24
|
+
return (
|
25
|
+
self.path == other.path
|
26
|
+
and self.method == other.method
|
27
|
+
and self.response == other.response
|
28
|
+
)
|
29
|
+
|
30
|
+
|
31
|
+
class OpenApiReader(AbstractReaderClass):
|
32
|
+
"""Implementation of the reader_class used by DataDriver."""
|
33
|
+
|
34
|
+
def get_data_from_source(self) -> list[TestCaseData]:
|
35
|
+
test_data: list[TestCaseData] = []
|
36
|
+
|
37
|
+
read_paths_method = getattr(self, "read_paths_method")
|
38
|
+
paths: dict[str, PathItemObject] = read_paths_method()
|
39
|
+
self._filter_paths(paths)
|
40
|
+
|
41
|
+
ignored_responses_ = [
|
42
|
+
str(response) for response in getattr(self, "ignored_responses", [])
|
43
|
+
]
|
44
|
+
|
45
|
+
ignored_tests = [Test(*test) for test in getattr(self, "ignored_testcases", [])]
|
46
|
+
|
47
|
+
for path, path_item in paths.items():
|
48
|
+
path_operations = path_item.get_operations()
|
49
|
+
|
50
|
+
# by reseversing the items, post/put operations come before get and delete
|
51
|
+
for method, operation_data in reversed(path_operations.items()):
|
52
|
+
tags_from_spec = operation_data.tags
|
53
|
+
for response in operation_data.responses.keys():
|
54
|
+
# 'default' applies to all status codes that are not specified, in
|
55
|
+
# which case we don't know what to expect and therefore can't verify
|
56
|
+
if (
|
57
|
+
response == "default"
|
58
|
+
or response in ignored_responses_
|
59
|
+
or Test(path, method, response) in ignored_tests
|
60
|
+
):
|
61
|
+
continue
|
62
|
+
|
63
|
+
tag_list = _get_tag_list(
|
64
|
+
tags=tags_from_spec, method=method, response=response
|
65
|
+
)
|
66
|
+
test_data.append(
|
67
|
+
TestCaseData(
|
68
|
+
arguments={
|
69
|
+
"${path}": path,
|
70
|
+
"${method}": method.upper(),
|
71
|
+
"${status_code}": response,
|
72
|
+
},
|
73
|
+
tags=tag_list,
|
74
|
+
),
|
75
|
+
)
|
76
|
+
return test_data
|
77
|
+
|
78
|
+
def _filter_paths(self, paths: dict[str, PathItemObject]) -> None:
|
79
|
+
def matches_include_pattern(path: str) -> bool:
|
80
|
+
for included_path in included_paths:
|
81
|
+
if path == included_path:
|
82
|
+
return True
|
83
|
+
if included_path.endswith("*"):
|
84
|
+
wildcard_include, _, _ = included_path.partition("*")
|
85
|
+
if path.startswith(wildcard_include):
|
86
|
+
return True
|
87
|
+
return False
|
88
|
+
|
89
|
+
def matches_ignore_pattern(path: str) -> bool:
|
90
|
+
for ignored_path in ignored_paths:
|
91
|
+
if path == ignored_path:
|
92
|
+
return True
|
93
|
+
|
94
|
+
if ignored_path.endswith("*"):
|
95
|
+
wildcard_ignore, _, _ = ignored_path.partition("*")
|
96
|
+
if path.startswith(wildcard_ignore):
|
97
|
+
return True
|
98
|
+
return False
|
99
|
+
|
100
|
+
if included_paths := getattr(self, "included_paths", ()):
|
101
|
+
path_list = list(paths.keys())
|
102
|
+
for path in path_list:
|
103
|
+
if not matches_include_pattern(path):
|
104
|
+
paths.pop(path)
|
105
|
+
|
106
|
+
if ignored_paths := getattr(self, "ignored_paths", ()):
|
107
|
+
path_list = list(paths.keys())
|
108
|
+
for path in path_list:
|
109
|
+
if matches_ignore_pattern(path):
|
110
|
+
paths.pop(path)
|
111
|
+
|
112
|
+
|
113
|
+
def _get_tag_list(tags: Sequence[str], method: str, response: str) -> list[str]:
|
114
|
+
return [*tags, f"Method: {method.upper()}", f"Response: {response}"]
|