robotframework-openapitools 0.3.0__py3-none-any.whl → 1.0.0b1__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 +44 -41
- OpenApiDriver/openapi_executors.py +48 -42
- OpenApiDriver/openapi_reader.py +115 -116
- OpenApiDriver/openapidriver.libspec +72 -62
- OpenApiDriver/openapidriver.py +25 -19
- OpenApiLibCore/__init__.py +13 -11
- OpenApiLibCore/annotations.py +3 -0
- OpenApiLibCore/data_generation/__init__.py +12 -0
- OpenApiLibCore/data_generation/body_data_generation.py +269 -0
- OpenApiLibCore/data_generation/data_generation_core.py +240 -0
- OpenApiLibCore/data_invalidation.py +281 -0
- OpenApiLibCore/dto_base.py +43 -40
- OpenApiLibCore/dto_utils.py +97 -85
- OpenApiLibCore/oas_cache.py +14 -13
- OpenApiLibCore/openapi_libcore.libspec +361 -188
- OpenApiLibCore/openapi_libcore.py +392 -1645
- OpenApiLibCore/parameter_utils.py +89 -0
- OpenApiLibCore/path_functions.py +215 -0
- OpenApiLibCore/path_invalidation.py +44 -0
- OpenApiLibCore/protocols.py +30 -0
- OpenApiLibCore/request_data.py +275 -0
- OpenApiLibCore/resource_relations.py +54 -0
- OpenApiLibCore/validation.py +497 -0
- OpenApiLibCore/value_utils.py +528 -481
- openapi_libgen/__init__.py +46 -0
- openapi_libgen/command_line.py +87 -0
- openapi_libgen/parsing_utils.py +26 -0
- openapi_libgen/spec_parser.py +212 -0
- openapi_libgen/templates/__init__.jinja +3 -0
- openapi_libgen/templates/library.jinja +30 -0
- robotframework_openapitools-1.0.0b1.dist-info/METADATA +237 -0
- robotframework_openapitools-1.0.0b1.dist-info/RECORD +37 -0
- {robotframework_openapitools-0.3.0.dist-info → robotframework_openapitools-1.0.0b1.dist-info}/WHEEL +1 -1
- robotframework_openapitools-1.0.0b1.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.0b1.dist-info}/LICENSE +0 -0
@@ -0,0 +1,281 @@
|
|
1
|
+
"""
|
2
|
+
Module holding the functions related to invalidation of valid data (generated
|
3
|
+
to make 2xx requests) to support testing for 4xx responses.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from copy import deepcopy
|
7
|
+
from random import choice
|
8
|
+
from typing import Any
|
9
|
+
|
10
|
+
from requests import Response
|
11
|
+
from robot.api import logger
|
12
|
+
from robot.libraries.BuiltIn import BuiltIn
|
13
|
+
|
14
|
+
from OpenApiLibCore.dto_base import (
|
15
|
+
NOT_SET,
|
16
|
+
Dto,
|
17
|
+
IdReference,
|
18
|
+
PathPropertiesConstraint,
|
19
|
+
PropertyValueConstraint,
|
20
|
+
UniquePropertyValueConstraint,
|
21
|
+
resolve_schema,
|
22
|
+
)
|
23
|
+
from OpenApiLibCore.request_data import RequestData
|
24
|
+
from OpenApiLibCore.value_utils import IGNORE, get_invalid_value, get_valid_value
|
25
|
+
|
26
|
+
run_keyword = BuiltIn().run_keyword
|
27
|
+
|
28
|
+
|
29
|
+
def get_invalid_json_data(
|
30
|
+
url: str,
|
31
|
+
method: str,
|
32
|
+
status_code: int,
|
33
|
+
request_data: RequestData,
|
34
|
+
invalid_property_default_response: int,
|
35
|
+
) -> dict[str, Any]:
|
36
|
+
method = method.lower()
|
37
|
+
data_relations = request_data.dto.get_relations_for_error_code(status_code)
|
38
|
+
data_relations = [
|
39
|
+
r for r in data_relations if not isinstance(r, PathPropertiesConstraint)
|
40
|
+
]
|
41
|
+
if not data_relations:
|
42
|
+
if not request_data.dto_schema:
|
43
|
+
raise ValueError(
|
44
|
+
"Failed to invalidate: no data_relations and empty schema."
|
45
|
+
)
|
46
|
+
json_data = request_data.dto.get_invalidated_data(
|
47
|
+
schema=request_data.dto_schema,
|
48
|
+
status_code=status_code,
|
49
|
+
invalid_property_default_code=invalid_property_default_response,
|
50
|
+
)
|
51
|
+
return json_data
|
52
|
+
resource_relation = choice(data_relations)
|
53
|
+
if isinstance(resource_relation, UniquePropertyValueConstraint):
|
54
|
+
json_data = run_keyword(
|
55
|
+
"get_json_data_with_conflict",
|
56
|
+
url,
|
57
|
+
method,
|
58
|
+
request_data.dto,
|
59
|
+
status_code,
|
60
|
+
)
|
61
|
+
elif isinstance(resource_relation, IdReference):
|
62
|
+
run_keyword("ensure_in_use", url, resource_relation)
|
63
|
+
json_data = request_data.dto.as_dict()
|
64
|
+
else:
|
65
|
+
json_data = request_data.dto.get_invalidated_data(
|
66
|
+
schema=request_data.dto_schema,
|
67
|
+
status_code=status_code,
|
68
|
+
invalid_property_default_code=invalid_property_default_response,
|
69
|
+
)
|
70
|
+
return json_data
|
71
|
+
|
72
|
+
|
73
|
+
def get_invalidated_parameters(
|
74
|
+
status_code: int, request_data: RequestData, invalid_property_default_response: int
|
75
|
+
) -> tuple[dict[str, Any], dict[str, str]]:
|
76
|
+
if not request_data.parameters:
|
77
|
+
raise ValueError("No params or headers to invalidate.")
|
78
|
+
|
79
|
+
# ensure the status_code can be triggered
|
80
|
+
relations = request_data.dto.get_parameter_relations_for_error_code(status_code)
|
81
|
+
relations_for_status_code = [
|
82
|
+
r
|
83
|
+
for r in relations
|
84
|
+
if isinstance(r, PropertyValueConstraint)
|
85
|
+
and (status_code in (r.error_code, r.invalid_value_error_code))
|
86
|
+
]
|
87
|
+
parameters_to_ignore = {
|
88
|
+
r.property_name
|
89
|
+
for r in relations_for_status_code
|
90
|
+
if r.invalid_value_error_code == status_code and r.invalid_value == IGNORE
|
91
|
+
}
|
92
|
+
relation_property_names = {r.property_name for r in relations_for_status_code}
|
93
|
+
if not relation_property_names:
|
94
|
+
if status_code != invalid_property_default_response:
|
95
|
+
raise ValueError(f"No relations to cause status_code {status_code} found.")
|
96
|
+
|
97
|
+
# ensure we're not modifying mutable properties
|
98
|
+
params = deepcopy(request_data.params)
|
99
|
+
headers = deepcopy(request_data.headers)
|
100
|
+
|
101
|
+
if status_code == invalid_property_default_response:
|
102
|
+
# take the params and headers that can be invalidated based on data type
|
103
|
+
# and expand the set with properties that can be invalided by relations
|
104
|
+
parameter_names = set(request_data.params_that_can_be_invalidated).union(
|
105
|
+
request_data.headers_that_can_be_invalidated
|
106
|
+
)
|
107
|
+
parameter_names.update(relation_property_names)
|
108
|
+
if not parameter_names:
|
109
|
+
raise ValueError(
|
110
|
+
"None of the query parameters and headers can be invalidated."
|
111
|
+
)
|
112
|
+
else:
|
113
|
+
# non-default status_codes can only be the result of a Relation
|
114
|
+
parameter_names = relation_property_names
|
115
|
+
|
116
|
+
# Dto mappings may contain generic mappings for properties that are not present
|
117
|
+
# in this specific schema
|
118
|
+
request_data_parameter_names = [p.get("name") for p in request_data.parameters]
|
119
|
+
additional_relation_property_names = {
|
120
|
+
n for n in relation_property_names if n not in request_data_parameter_names
|
121
|
+
}
|
122
|
+
if additional_relation_property_names:
|
123
|
+
logger.warn(
|
124
|
+
f"get_parameter_relations_for_error_code yielded properties that are "
|
125
|
+
f"not defined in the schema: {additional_relation_property_names}\n"
|
126
|
+
f"These properties will be ignored for parameter invalidation."
|
127
|
+
)
|
128
|
+
parameter_names = parameter_names - additional_relation_property_names
|
129
|
+
|
130
|
+
if not parameter_names:
|
131
|
+
raise ValueError(
|
132
|
+
f"No parameter can be changed to cause status_code {status_code}."
|
133
|
+
)
|
134
|
+
|
135
|
+
parameter_names = parameter_names - parameters_to_ignore
|
136
|
+
parameter_to_invalidate = choice(tuple(parameter_names))
|
137
|
+
|
138
|
+
# check for invalid parameters in the provided request_data
|
139
|
+
try:
|
140
|
+
[parameter_data] = [
|
141
|
+
data
|
142
|
+
for data in request_data.parameters
|
143
|
+
if data["name"] == parameter_to_invalidate
|
144
|
+
]
|
145
|
+
except Exception:
|
146
|
+
raise ValueError(
|
147
|
+
f"{parameter_to_invalidate} not found in provided parameters."
|
148
|
+
) from None
|
149
|
+
|
150
|
+
# get the invalid_value for the chosen parameter
|
151
|
+
try:
|
152
|
+
[invalid_value_for_error_code] = [
|
153
|
+
r.invalid_value
|
154
|
+
for r in relations_for_status_code
|
155
|
+
if r.property_name == parameter_to_invalidate
|
156
|
+
and r.invalid_value_error_code == status_code
|
157
|
+
]
|
158
|
+
except ValueError:
|
159
|
+
invalid_value_for_error_code = NOT_SET
|
160
|
+
|
161
|
+
# get the constraint values if available for the chosen parameter
|
162
|
+
try:
|
163
|
+
[values_from_constraint] = [
|
164
|
+
r.values
|
165
|
+
for r in relations_for_status_code
|
166
|
+
if r.property_name == parameter_to_invalidate
|
167
|
+
]
|
168
|
+
except ValueError:
|
169
|
+
values_from_constraint = []
|
170
|
+
|
171
|
+
# if the parameter was not provided, add it to params / headers
|
172
|
+
params, headers = ensure_parameter_in_parameters(
|
173
|
+
parameter_to_invalidate=parameter_to_invalidate,
|
174
|
+
params=params,
|
175
|
+
headers=headers,
|
176
|
+
parameter_data=parameter_data,
|
177
|
+
values_from_constraint=values_from_constraint,
|
178
|
+
)
|
179
|
+
|
180
|
+
# determine the invalid_value
|
181
|
+
if invalid_value_for_error_code != NOT_SET:
|
182
|
+
invalid_value = invalid_value_for_error_code
|
183
|
+
else:
|
184
|
+
if parameter_to_invalidate in params.keys():
|
185
|
+
valid_value = params[parameter_to_invalidate]
|
186
|
+
else:
|
187
|
+
valid_value = headers[parameter_to_invalidate]
|
188
|
+
|
189
|
+
value_schema = resolve_schema(parameter_data["schema"])
|
190
|
+
invalid_value = get_invalid_value(
|
191
|
+
value_schema=value_schema,
|
192
|
+
current_value=valid_value,
|
193
|
+
values_from_constraint=values_from_constraint,
|
194
|
+
)
|
195
|
+
logger.debug(f"{parameter_to_invalidate} changed to {invalid_value}")
|
196
|
+
|
197
|
+
# update the params / headers and return
|
198
|
+
if parameter_to_invalidate in params.keys():
|
199
|
+
params[parameter_to_invalidate] = invalid_value
|
200
|
+
else:
|
201
|
+
headers[parameter_to_invalidate] = str(invalid_value)
|
202
|
+
return params, headers
|
203
|
+
|
204
|
+
|
205
|
+
def ensure_parameter_in_parameters(
|
206
|
+
parameter_to_invalidate: str,
|
207
|
+
params: dict[str, Any],
|
208
|
+
headers: dict[str, str],
|
209
|
+
parameter_data: dict[str, Any],
|
210
|
+
values_from_constraint: list[Any],
|
211
|
+
) -> tuple[dict[str, Any], dict[str, str]]:
|
212
|
+
"""
|
213
|
+
Returns the params, headers tuple with parameter_to_invalidate with a valid
|
214
|
+
value to params or headers if not originally present.
|
215
|
+
"""
|
216
|
+
if (
|
217
|
+
parameter_to_invalidate not in params.keys()
|
218
|
+
and parameter_to_invalidate not in headers.keys()
|
219
|
+
):
|
220
|
+
if values_from_constraint:
|
221
|
+
valid_value = choice(values_from_constraint)
|
222
|
+
else:
|
223
|
+
parameter_schema = resolve_schema(parameter_data["schema"])
|
224
|
+
valid_value = get_valid_value(parameter_schema)
|
225
|
+
if (
|
226
|
+
parameter_data["in"] == "query"
|
227
|
+
and parameter_to_invalidate not in params.keys()
|
228
|
+
):
|
229
|
+
params[parameter_to_invalidate] = valid_value
|
230
|
+
if (
|
231
|
+
parameter_data["in"] == "header"
|
232
|
+
and parameter_to_invalidate not in headers.keys()
|
233
|
+
):
|
234
|
+
headers[parameter_to_invalidate] = str(valid_value)
|
235
|
+
return params, headers
|
236
|
+
|
237
|
+
|
238
|
+
def get_json_data_with_conflict(
|
239
|
+
url: str, base_url: str, method: str, dto: Dto, conflict_status_code: int
|
240
|
+
) -> dict[str, Any]:
|
241
|
+
method = method.lower()
|
242
|
+
json_data = dto.as_dict()
|
243
|
+
unique_property_value_constraints = [
|
244
|
+
r for r in dto.get_relations() if isinstance(r, UniquePropertyValueConstraint)
|
245
|
+
]
|
246
|
+
for relation in unique_property_value_constraints:
|
247
|
+
json_data[relation.property_name] = relation.value
|
248
|
+
# create a new resource that the original request will conflict with
|
249
|
+
if method in ["patch", "put"]:
|
250
|
+
post_url_parts = url.split("/")[:-1]
|
251
|
+
post_url = "/".join(post_url_parts)
|
252
|
+
# the PATCH or PUT may use a different dto than required for POST
|
253
|
+
# so a valid POST dto must be constructed
|
254
|
+
path = post_url.replace(base_url, "")
|
255
|
+
request_data: RequestData = run_keyword("get_request_data", path, "post")
|
256
|
+
post_json = request_data.dto.as_dict()
|
257
|
+
for key in post_json.keys():
|
258
|
+
if key in json_data:
|
259
|
+
post_json[key] = json_data.get(key)
|
260
|
+
else:
|
261
|
+
post_url = url
|
262
|
+
post_json = json_data
|
263
|
+
path = post_url.replace(base_url, "")
|
264
|
+
request_data = run_keyword("get_request_data", path, "post")
|
265
|
+
|
266
|
+
response: Response = run_keyword(
|
267
|
+
"authorized_request",
|
268
|
+
post_url,
|
269
|
+
"post",
|
270
|
+
request_data.params,
|
271
|
+
request_data.headers,
|
272
|
+
post_json,
|
273
|
+
)
|
274
|
+
# conflicting resource may already exist
|
275
|
+
assert response.ok or response.status_code == conflict_status_code, (
|
276
|
+
f"get_json_data_with_conflict received {response.status_code}: {response.json()}"
|
277
|
+
)
|
278
|
+
return json_data
|
279
|
+
raise ValueError(
|
280
|
+
f"No UniquePropertyValueConstraint in the get_relations list on dto {dto}."
|
281
|
+
)
|
OpenApiLibCore/dto_base.py
CHANGED
@@ -7,20 +7,19 @@ test and constraints / restrictions on properties of the resources.
|
|
7
7
|
from abc import ABC
|
8
8
|
from copy import deepcopy
|
9
9
|
from dataclasses import dataclass, fields
|
10
|
-
from logging import getLogger
|
11
10
|
from random import choice, shuffle
|
12
|
-
from typing import Any
|
11
|
+
from typing import Any
|
13
12
|
from uuid import uuid4
|
14
13
|
|
15
|
-
from
|
14
|
+
from robot.api import logger
|
16
15
|
|
17
|
-
|
16
|
+
from OpenApiLibCore import value_utils
|
18
17
|
|
19
18
|
NOT_SET = object()
|
20
19
|
SENTINEL = object()
|
21
20
|
|
22
21
|
|
23
|
-
def resolve_schema(schema:
|
22
|
+
def resolve_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
24
23
|
"""
|
25
24
|
Helper function to resolve allOf, anyOf and oneOf instances in a schema.
|
26
25
|
|
@@ -64,7 +63,7 @@ def resolve_schema(schema: Dict[str, Any]) -> Dict[str, Any]:
|
|
64
63
|
return resolved_schema
|
65
64
|
|
66
65
|
|
67
|
-
def merge_schemas(first:
|
66
|
+
def merge_schemas(first: dict[str, Any], second: dict[str, Any]) -> dict[str, Any]:
|
68
67
|
"""Helper method to merge two schemas, recursively."""
|
69
68
|
merged_schema = deepcopy(first)
|
70
69
|
for key, value in second.items():
|
@@ -86,7 +85,7 @@ def merge_schemas(first: Dict[str, Any], second: Dict[str, Any]) -> Dict[str, An
|
|
86
85
|
return merged_schema
|
87
86
|
|
88
87
|
|
89
|
-
class ResourceRelation(ABC):
|
88
|
+
class ResourceRelation(ABC):
|
90
89
|
"""ABC for all resource relations or restrictions within the API."""
|
91
90
|
|
92
91
|
property_name: str
|
@@ -95,10 +94,12 @@ class ResourceRelation(ABC): # pylint: disable=too-few-public-methods
|
|
95
94
|
|
96
95
|
@dataclass
|
97
96
|
class PathPropertiesConstraint(ResourceRelation):
|
98
|
-
"""The
|
97
|
+
"""The value to be used as the ``path`` for related requests."""
|
99
98
|
|
100
99
|
path: str
|
101
100
|
property_name: str = "id"
|
101
|
+
invalid_value: Any = NOT_SET
|
102
|
+
invalid_value_error_code: int = 422
|
102
103
|
error_code: int = 404
|
103
104
|
|
104
105
|
|
@@ -107,10 +108,11 @@ class PropertyValueConstraint(ResourceRelation):
|
|
107
108
|
"""The allowed values for property_name."""
|
108
109
|
|
109
110
|
property_name: str
|
110
|
-
values:
|
111
|
+
values: list[Any]
|
111
112
|
invalid_value: Any = NOT_SET
|
112
113
|
invalid_value_error_code: int = 422
|
113
114
|
error_code: int = 422
|
115
|
+
treat_as_mandatory: bool = False
|
114
116
|
|
115
117
|
|
116
118
|
@dataclass
|
@@ -119,7 +121,7 @@ class IdDependency(ResourceRelation):
|
|
119
121
|
|
120
122
|
property_name: str
|
121
123
|
get_path: str
|
122
|
-
operation_id:
|
124
|
+
operation_id: str = ""
|
123
125
|
error_code: int = 422
|
124
126
|
|
125
127
|
|
@@ -141,27 +143,20 @@ class UniquePropertyValueConstraint(ResourceRelation):
|
|
141
143
|
error_code: int = 422
|
142
144
|
|
143
145
|
|
144
|
-
Relation = Union[
|
145
|
-
IdDependency,
|
146
|
-
IdReference,
|
147
|
-
PathPropertiesConstraint,
|
148
|
-
PropertyValueConstraint,
|
149
|
-
UniquePropertyValueConstraint,
|
150
|
-
]
|
151
|
-
|
152
|
-
|
153
146
|
@dataclass
|
154
147
|
class Dto(ABC):
|
155
148
|
"""Base class for the Dto class."""
|
156
149
|
|
157
150
|
@staticmethod
|
158
|
-
def get_parameter_relations() ->
|
151
|
+
def get_parameter_relations() -> list[ResourceRelation]:
|
159
152
|
"""Return the list of Relations for the header and query parameters."""
|
160
153
|
return []
|
161
154
|
|
162
|
-
def get_parameter_relations_for_error_code(
|
155
|
+
def get_parameter_relations_for_error_code(
|
156
|
+
self, error_code: int
|
157
|
+
) -> list[ResourceRelation]:
|
163
158
|
"""Return the list of Relations associated with the given error_code."""
|
164
|
-
relations:
|
159
|
+
relations: list[ResourceRelation] = [
|
165
160
|
r
|
166
161
|
for r in self.get_parameter_relations()
|
167
162
|
if r.error_code == error_code
|
@@ -173,13 +168,13 @@ class Dto(ABC):
|
|
173
168
|
return relations
|
174
169
|
|
175
170
|
@staticmethod
|
176
|
-
def get_relations() ->
|
171
|
+
def get_relations() -> list[ResourceRelation]:
|
177
172
|
"""Return the list of Relations for the (json) body."""
|
178
173
|
return []
|
179
174
|
|
180
|
-
def get_relations_for_error_code(self, error_code: int) ->
|
175
|
+
def get_relations_for_error_code(self, error_code: int) -> list[ResourceRelation]:
|
181
176
|
"""Return the list of Relations associated with the given error_code."""
|
182
|
-
relations:
|
177
|
+
relations: list[ResourceRelation] = [
|
183
178
|
r
|
184
179
|
for r in self.get_relations()
|
185
180
|
if r.error_code == error_code
|
@@ -190,14 +185,24 @@ class Dto(ABC):
|
|
190
185
|
]
|
191
186
|
return relations
|
192
187
|
|
188
|
+
def get_body_relations_for_error_code(
|
189
|
+
self, error_code: int
|
190
|
+
) -> list[ResourceRelation]:
|
191
|
+
"""
|
192
|
+
Return the list of Relations associated with the given error_code that are
|
193
|
+
applicable to the body / payload of the request.
|
194
|
+
"""
|
195
|
+
all_relations = self.get_relations_for_error_code(error_code=error_code)
|
196
|
+
return [r for r in all_relations if not isinstance(r, PathPropertiesConstraint)]
|
197
|
+
|
193
198
|
def get_invalidated_data(
|
194
199
|
self,
|
195
|
-
schema:
|
200
|
+
schema: dict[str, Any],
|
196
201
|
status_code: int,
|
197
202
|
invalid_property_default_code: int,
|
198
|
-
) ->
|
203
|
+
) -> dict[str, Any]:
|
199
204
|
"""Return a data set with one of the properties set to an invalid value or type."""
|
200
|
-
properties:
|
205
|
+
properties: dict[str, Any] = self.as_dict()
|
201
206
|
|
202
207
|
schema = resolve_schema(schema)
|
203
208
|
|
@@ -207,18 +212,16 @@ class Dto(ABC):
|
|
207
212
|
r for r in relations if not isinstance(r, PathPropertiesConstraint)
|
208
213
|
]
|
209
214
|
property_names = [r.property_name for r in relations]
|
210
|
-
if status_code == invalid_property_default_code:
|
215
|
+
if status_code == invalid_property_default_code and schema.get("properties"):
|
211
216
|
# add all properties defined in the schema, including optional properties
|
212
217
|
property_names.extend((schema["properties"].keys()))
|
213
|
-
# remove duplicates
|
214
|
-
property_names = list(set(property_names))
|
215
218
|
if not property_names:
|
216
219
|
raise ValueError(
|
217
220
|
f"No property can be invalidated to cause status_code {status_code}"
|
218
221
|
)
|
219
|
-
# shuffle the property_names so different properties on
|
220
|
-
# when rerunning the test
|
221
|
-
shuffle(property_names)
|
222
|
+
# Remove duplicates, then shuffle the property_names so different properties on
|
223
|
+
# the Dto are invalidated when rerunning the test.
|
224
|
+
shuffle(list(set(property_names)))
|
222
225
|
for property_name in property_names:
|
223
226
|
# if possible, invalidate a constraint but send otherwise valid data
|
224
227
|
id_dependencies = [
|
@@ -227,12 +230,12 @@ class Dto(ABC):
|
|
227
230
|
if isinstance(r, IdDependency) and r.property_name == property_name
|
228
231
|
]
|
229
232
|
if id_dependencies:
|
230
|
-
|
233
|
+
invalid_id = uuid4().hex
|
231
234
|
logger.debug(
|
232
235
|
f"Breaking IdDependency for status_code {status_code}: replacing "
|
233
|
-
f"{properties[property_name]} with {
|
236
|
+
f"{properties[property_name]} with {invalid_id}"
|
234
237
|
)
|
235
|
-
properties[property_name] =
|
238
|
+
properties[property_name] = invalid_id
|
236
239
|
return properties
|
237
240
|
|
238
241
|
invalid_value_from_constraint = [
|
@@ -293,14 +296,14 @@ class Dto(ABC):
|
|
293
296
|
)
|
294
297
|
properties[property_name] = invalid_value
|
295
298
|
logger.debug(
|
296
|
-
f"Property {property_name} changed to {invalid_value} (received from "
|
299
|
+
f"Property {property_name} changed to {invalid_value!r} (received from "
|
297
300
|
f"get_invalid_value)"
|
298
301
|
)
|
299
302
|
return properties
|
300
|
-
logger.
|
303
|
+
logger.warn("get_invalidated_data returned unchanged properties")
|
301
304
|
return properties # pragma: no cover
|
302
305
|
|
303
|
-
def as_dict(self) ->
|
306
|
+
def as_dict(self) -> dict[Any, Any]:
|
304
307
|
"""Return the dict representation of the Dto."""
|
305
308
|
result = {}
|
306
309
|
|
OpenApiLibCore/dto_utils.py
CHANGED
@@ -1,85 +1,97 @@
|
|
1
|
-
"""Module for helper methods and classes used by the openapi_executors module."""
|
2
|
-
|
3
|
-
from dataclasses import dataclass
|
4
|
-
from importlib import import_module
|
5
|
-
from
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
@dataclass
|
14
|
-
class _DefaultIdPropertyName:
|
15
|
-
id_property_name: str = "id"
|
16
|
-
|
17
|
-
|
18
|
-
DEFAULT_ID_PROPERTY_NAME = _DefaultIdPropertyName()
|
19
|
-
|
20
|
-
|
21
|
-
@dataclass
|
22
|
-
class DefaultDto(Dto):
|
23
|
-
"""A default Dto that can be instantiated."""
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
)
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
return
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
)
|
85
|
-
return default_id_name
|
1
|
+
"""Module for helper methods and classes used by the openapi_executors module."""
|
2
|
+
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from importlib import import_module
|
5
|
+
from typing import Any, Callable, Type, overload
|
6
|
+
|
7
|
+
from robot.api import logger
|
8
|
+
|
9
|
+
from OpenApiLibCore.dto_base import Dto
|
10
|
+
from OpenApiLibCore.protocols import GetDtoClassType, GetIdPropertyNameType
|
11
|
+
|
12
|
+
|
13
|
+
@dataclass
|
14
|
+
class _DefaultIdPropertyName:
|
15
|
+
id_property_name: str = "id"
|
16
|
+
|
17
|
+
|
18
|
+
DEFAULT_ID_PROPERTY_NAME = _DefaultIdPropertyName()
|
19
|
+
|
20
|
+
|
21
|
+
@dataclass
|
22
|
+
class DefaultDto(Dto):
|
23
|
+
"""A default Dto that can be instantiated."""
|
24
|
+
|
25
|
+
|
26
|
+
def get_dto_class(mappings_module_name: str) -> GetDtoClassType:
|
27
|
+
return GetDtoClass(mappings_module_name=mappings_module_name)
|
28
|
+
|
29
|
+
|
30
|
+
class GetDtoClass:
|
31
|
+
"""Callable class to return Dtos from user-implemented mappings file."""
|
32
|
+
|
33
|
+
def __init__(self, mappings_module_name: str) -> None:
|
34
|
+
try:
|
35
|
+
mappings_module = import_module(mappings_module_name)
|
36
|
+
self.dto_mapping: dict[tuple[str, str], Type[Dto]] = (
|
37
|
+
mappings_module.DTO_MAPPING
|
38
|
+
)
|
39
|
+
except (ImportError, AttributeError, ValueError) as exception:
|
40
|
+
if mappings_module_name != "no mapping":
|
41
|
+
logger.error(f"DTO_MAPPING was not imported: {exception}")
|
42
|
+
self.dto_mapping = {}
|
43
|
+
|
44
|
+
def __call__(self, path: str, method: str) -> Type[Dto]:
|
45
|
+
try:
|
46
|
+
return self.dto_mapping[(path, method.lower())]
|
47
|
+
except KeyError:
|
48
|
+
logger.debug(f"No Dto mapping for {path} {method}.")
|
49
|
+
return DefaultDto
|
50
|
+
|
51
|
+
|
52
|
+
def get_id_property_name(mappings_module_name: str) -> GetIdPropertyNameType:
|
53
|
+
return GetIdPropertyName(mappings_module_name=mappings_module_name)
|
54
|
+
|
55
|
+
|
56
|
+
class GetIdPropertyName:
|
57
|
+
"""
|
58
|
+
Callable class to return the name of the property that uniquely identifies
|
59
|
+
the resource from user-implemented mappings file.
|
60
|
+
"""
|
61
|
+
|
62
|
+
def __init__(self, mappings_module_name: str) -> None:
|
63
|
+
try:
|
64
|
+
mappings_module = import_module(mappings_module_name)
|
65
|
+
self.id_mapping: dict[
|
66
|
+
str,
|
67
|
+
str | tuple[str, Callable[[str], str] | Callable[[int], int]],
|
68
|
+
] = mappings_module.ID_MAPPING
|
69
|
+
except (ImportError, AttributeError, ValueError) as exception:
|
70
|
+
if mappings_module_name != "no mapping":
|
71
|
+
logger.error(f"ID_MAPPING was not imported: {exception}")
|
72
|
+
self.id_mapping = {}
|
73
|
+
|
74
|
+
def __call__(
|
75
|
+
self, path: str
|
76
|
+
) -> tuple[str, Callable[[str], str] | Callable[[int], int]]:
|
77
|
+
try:
|
78
|
+
value_or_mapping = self.id_mapping[path]
|
79
|
+
if isinstance(value_or_mapping, str):
|
80
|
+
return (value_or_mapping, dummy_transformer)
|
81
|
+
return value_or_mapping
|
82
|
+
except KeyError:
|
83
|
+
default_id_name = DEFAULT_ID_PROPERTY_NAME.id_property_name
|
84
|
+
logger.debug(f"No id mapping for {path} ('{default_id_name}' will be used)")
|
85
|
+
return (default_id_name, dummy_transformer)
|
86
|
+
|
87
|
+
|
88
|
+
@overload
|
89
|
+
def dummy_transformer(valid_id: str) -> str: ...
|
90
|
+
|
91
|
+
|
92
|
+
@overload
|
93
|
+
def dummy_transformer(valid_id: int) -> int: ...
|
94
|
+
|
95
|
+
|
96
|
+
def dummy_transformer(valid_id: Any) -> Any:
|
97
|
+
return valid_id
|