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
@@ -0,0 +1,294 @@
|
|
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.annotations import JSON
|
15
|
+
from OpenApiLibCore.dto_base import (
|
16
|
+
NOT_SET,
|
17
|
+
Dto,
|
18
|
+
IdReference,
|
19
|
+
PropertyValueConstraint,
|
20
|
+
UniquePropertyValueConstraint,
|
21
|
+
)
|
22
|
+
from OpenApiLibCore.models import ParameterObject, UnionTypeSchema
|
23
|
+
from OpenApiLibCore.request_data import RequestData
|
24
|
+
from OpenApiLibCore.value_utils import IGNORE, get_invalid_value
|
25
|
+
|
26
|
+
run_keyword = BuiltIn().run_keyword
|
27
|
+
|
28
|
+
|
29
|
+
def get_invalid_body_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_body_relations_for_error_code(status_code)
|
38
|
+
if not data_relations:
|
39
|
+
if request_data.body_schema is None:
|
40
|
+
raise ValueError(
|
41
|
+
"Failed to invalidate: request_data does not contain a body_schema."
|
42
|
+
)
|
43
|
+
json_data = request_data.dto.get_invalidated_data(
|
44
|
+
schema=request_data.body_schema,
|
45
|
+
status_code=status_code,
|
46
|
+
invalid_property_default_code=invalid_property_default_response,
|
47
|
+
)
|
48
|
+
return json_data
|
49
|
+
resource_relation = choice(data_relations)
|
50
|
+
if isinstance(resource_relation, UniquePropertyValueConstraint):
|
51
|
+
json_data = run_keyword(
|
52
|
+
"get_json_data_with_conflict",
|
53
|
+
url,
|
54
|
+
method,
|
55
|
+
request_data.dto,
|
56
|
+
status_code,
|
57
|
+
)
|
58
|
+
elif isinstance(resource_relation, IdReference):
|
59
|
+
run_keyword("ensure_in_use", url, resource_relation)
|
60
|
+
json_data = request_data.dto.as_dict()
|
61
|
+
else:
|
62
|
+
if request_data.body_schema is None:
|
63
|
+
raise ValueError(
|
64
|
+
"Failed to invalidate: request_data does not contain a body_schema."
|
65
|
+
)
|
66
|
+
json_data = request_data.dto.get_invalidated_data(
|
67
|
+
schema=request_data.body_schema,
|
68
|
+
status_code=status_code,
|
69
|
+
invalid_property_default_code=invalid_property_default_response,
|
70
|
+
)
|
71
|
+
return json_data
|
72
|
+
|
73
|
+
|
74
|
+
def get_invalidated_parameters(
|
75
|
+
status_code: int, request_data: RequestData, invalid_property_default_response: int
|
76
|
+
) -> tuple[dict[str, JSON], dict[str, JSON]]:
|
77
|
+
if not request_data.parameters:
|
78
|
+
raise ValueError("No params or headers to invalidate.")
|
79
|
+
|
80
|
+
# ensure the status_code can be triggered
|
81
|
+
relations = request_data.dto.get_parameter_relations_for_error_code(status_code)
|
82
|
+
relations_for_status_code = [
|
83
|
+
r
|
84
|
+
for r in relations
|
85
|
+
if isinstance(r, PropertyValueConstraint)
|
86
|
+
and (status_code in (r.error_code, r.invalid_value_error_code))
|
87
|
+
]
|
88
|
+
parameters_to_ignore = {
|
89
|
+
r.property_name
|
90
|
+
for r in relations_for_status_code
|
91
|
+
if r.invalid_value_error_code == status_code and r.invalid_value == IGNORE
|
92
|
+
}
|
93
|
+
relation_property_names = {r.property_name for r in relations_for_status_code}
|
94
|
+
if not relation_property_names:
|
95
|
+
if status_code != invalid_property_default_response:
|
96
|
+
raise ValueError(f"No relations to cause status_code {status_code} found.")
|
97
|
+
|
98
|
+
# ensure we're not modifying mutable properties
|
99
|
+
params = deepcopy(request_data.params)
|
100
|
+
headers = deepcopy(request_data.headers)
|
101
|
+
|
102
|
+
if status_code == invalid_property_default_response:
|
103
|
+
# take the params and headers that can be invalidated based on data type
|
104
|
+
# and expand the set with properties that can be invalided by relations
|
105
|
+
parameter_names = set(request_data.params_that_can_be_invalidated).union(
|
106
|
+
request_data.headers_that_can_be_invalidated
|
107
|
+
)
|
108
|
+
parameter_names.update(relation_property_names)
|
109
|
+
if not parameter_names:
|
110
|
+
raise ValueError(
|
111
|
+
"None of the query parameters and headers can be invalidated."
|
112
|
+
)
|
113
|
+
else:
|
114
|
+
# non-default status_codes can only be the result of a Relation
|
115
|
+
parameter_names = relation_property_names
|
116
|
+
|
117
|
+
# Dto mappings may contain generic mappings for properties that are not present
|
118
|
+
# in this specific schema
|
119
|
+
request_data_parameter_names = [p.name for p in request_data.parameters]
|
120
|
+
additional_relation_property_names = {
|
121
|
+
n for n in relation_property_names if n not in request_data_parameter_names
|
122
|
+
}
|
123
|
+
if additional_relation_property_names:
|
124
|
+
logger.warn(
|
125
|
+
f"get_parameter_relations_for_error_code yielded properties that are "
|
126
|
+
f"not defined in the schema: {additional_relation_property_names}\n"
|
127
|
+
f"These properties will be ignored for parameter invalidation."
|
128
|
+
)
|
129
|
+
parameter_names = parameter_names - additional_relation_property_names
|
130
|
+
|
131
|
+
if not parameter_names:
|
132
|
+
raise ValueError(
|
133
|
+
f"No parameter can be changed to cause status_code {status_code}."
|
134
|
+
)
|
135
|
+
|
136
|
+
parameter_names = parameter_names - parameters_to_ignore
|
137
|
+
parameter_to_invalidate = choice(tuple(parameter_names))
|
138
|
+
|
139
|
+
# check for invalid parameters in the provided request_data
|
140
|
+
try:
|
141
|
+
[parameter_data] = [
|
142
|
+
data
|
143
|
+
for data in request_data.parameters
|
144
|
+
if data.name == parameter_to_invalidate
|
145
|
+
]
|
146
|
+
except Exception:
|
147
|
+
raise ValueError(
|
148
|
+
f"{parameter_to_invalidate} not found in provided parameters."
|
149
|
+
) from None
|
150
|
+
|
151
|
+
# get the invalid_value for the chosen parameter
|
152
|
+
try:
|
153
|
+
[invalid_value_for_error_code] = [
|
154
|
+
r.invalid_value
|
155
|
+
for r in relations_for_status_code
|
156
|
+
if r.property_name == parameter_to_invalidate
|
157
|
+
and r.invalid_value_error_code == status_code
|
158
|
+
]
|
159
|
+
except ValueError:
|
160
|
+
invalid_value_for_error_code = NOT_SET
|
161
|
+
|
162
|
+
# get the constraint values if available for the chosen parameter
|
163
|
+
try:
|
164
|
+
[values_from_constraint] = [
|
165
|
+
r.values
|
166
|
+
for r in relations_for_status_code
|
167
|
+
if r.property_name == parameter_to_invalidate
|
168
|
+
]
|
169
|
+
except ValueError:
|
170
|
+
values_from_constraint = []
|
171
|
+
|
172
|
+
# if the parameter was not provided, add it to params / headers
|
173
|
+
params, headers = ensure_parameter_in_parameters(
|
174
|
+
parameter_to_invalidate=parameter_to_invalidate,
|
175
|
+
params=params,
|
176
|
+
headers=headers,
|
177
|
+
parameter_data=parameter_data,
|
178
|
+
values_from_constraint=values_from_constraint,
|
179
|
+
)
|
180
|
+
|
181
|
+
# determine the invalid_value
|
182
|
+
if invalid_value_for_error_code != NOT_SET:
|
183
|
+
invalid_value = invalid_value_for_error_code
|
184
|
+
else:
|
185
|
+
if parameter_to_invalidate in params.keys():
|
186
|
+
valid_value = params[parameter_to_invalidate]
|
187
|
+
else:
|
188
|
+
valid_value = headers[parameter_to_invalidate]
|
189
|
+
|
190
|
+
value_schema = parameter_data.schema_
|
191
|
+
if value_schema is None:
|
192
|
+
raise ValueError(f"No schema defined for parameter: {parameter_data}.")
|
193
|
+
|
194
|
+
if isinstance(value_schema, UnionTypeSchema):
|
195
|
+
# FIXME: extra handling may be needed in case of values_from_constraint
|
196
|
+
value_schema = choice(value_schema.resolved_schemas)
|
197
|
+
|
198
|
+
invalid_value = get_invalid_value(
|
199
|
+
value_schema=value_schema,
|
200
|
+
current_value=valid_value,
|
201
|
+
values_from_constraint=values_from_constraint,
|
202
|
+
)
|
203
|
+
logger.debug(f"{parameter_to_invalidate} changed to {invalid_value}")
|
204
|
+
|
205
|
+
# update the params / headers and return
|
206
|
+
if parameter_to_invalidate in params.keys():
|
207
|
+
params[parameter_to_invalidate] = invalid_value
|
208
|
+
else:
|
209
|
+
headers[parameter_to_invalidate] = str(invalid_value)
|
210
|
+
return params, headers
|
211
|
+
|
212
|
+
|
213
|
+
def ensure_parameter_in_parameters(
|
214
|
+
parameter_to_invalidate: str,
|
215
|
+
params: dict[str, JSON],
|
216
|
+
headers: dict[str, JSON],
|
217
|
+
parameter_data: ParameterObject,
|
218
|
+
values_from_constraint: list[JSON],
|
219
|
+
) -> tuple[dict[str, JSON], dict[str, JSON]]:
|
220
|
+
"""
|
221
|
+
Returns the params, headers tuple with parameter_to_invalidate with a valid
|
222
|
+
value to params or headers if not originally present.
|
223
|
+
"""
|
224
|
+
if (
|
225
|
+
parameter_to_invalidate not in params.keys()
|
226
|
+
and parameter_to_invalidate not in headers.keys()
|
227
|
+
):
|
228
|
+
if values_from_constraint:
|
229
|
+
valid_value = choice(values_from_constraint)
|
230
|
+
else:
|
231
|
+
value_schema = parameter_data.schema_
|
232
|
+
if value_schema is None:
|
233
|
+
raise ValueError(f"No schema defined for parameter: {parameter_data}.")
|
234
|
+
|
235
|
+
if isinstance(value_schema, UnionTypeSchema):
|
236
|
+
value_schema = choice(value_schema.resolved_schemas)
|
237
|
+
valid_value = value_schema.get_valid_value()
|
238
|
+
if (
|
239
|
+
parameter_data.in_ == "query"
|
240
|
+
and parameter_to_invalidate not in params.keys()
|
241
|
+
):
|
242
|
+
params[parameter_to_invalidate] = valid_value
|
243
|
+
if (
|
244
|
+
parameter_data.in_ == "header"
|
245
|
+
and parameter_to_invalidate not in headers.keys()
|
246
|
+
):
|
247
|
+
headers[parameter_to_invalidate] = str(valid_value)
|
248
|
+
return params, headers
|
249
|
+
|
250
|
+
|
251
|
+
def get_json_data_with_conflict(
|
252
|
+
url: str, base_url: str, method: str, dto: Dto, conflict_status_code: int
|
253
|
+
) -> dict[str, Any]:
|
254
|
+
method = method.lower()
|
255
|
+
json_data = dto.as_dict()
|
256
|
+
unique_property_value_constraints = [
|
257
|
+
r for r in dto.get_relations() if isinstance(r, UniquePropertyValueConstraint)
|
258
|
+
]
|
259
|
+
for relation in unique_property_value_constraints:
|
260
|
+
json_data[relation.property_name] = relation.value
|
261
|
+
# create a new resource that the original request will conflict with
|
262
|
+
if method in ["patch", "put"]:
|
263
|
+
post_url_parts = url.split("/")[:-1]
|
264
|
+
post_url = "/".join(post_url_parts)
|
265
|
+
# the PATCH or PUT may use a different dto than required for POST
|
266
|
+
# so a valid POST dto must be constructed
|
267
|
+
path = post_url.replace(base_url, "")
|
268
|
+
request_data: RequestData = run_keyword("get_request_data", path, "post")
|
269
|
+
post_json = request_data.dto.as_dict()
|
270
|
+
for key in post_json.keys():
|
271
|
+
if key in json_data:
|
272
|
+
post_json[key] = json_data.get(key)
|
273
|
+
else:
|
274
|
+
post_url = url
|
275
|
+
post_json = json_data
|
276
|
+
path = post_url.replace(base_url, "")
|
277
|
+
request_data = run_keyword("get_request_data", path, "post")
|
278
|
+
|
279
|
+
response: Response = run_keyword(
|
280
|
+
"authorized_request",
|
281
|
+
post_url,
|
282
|
+
"post",
|
283
|
+
request_data.params,
|
284
|
+
request_data.headers,
|
285
|
+
post_json,
|
286
|
+
)
|
287
|
+
# conflicting resource may already exist
|
288
|
+
assert response.ok or response.status_code == conflict_status_code, (
|
289
|
+
f"get_json_data_with_conflict received {response.status_code}: {response.json()}"
|
290
|
+
)
|
291
|
+
return json_data
|
292
|
+
raise ValueError(
|
293
|
+
f"No UniquePropertyValueConstraint in the get_relations list on dto {dto}."
|
294
|
+
)
|
OpenApiLibCore/dto_base.py
CHANGED
@@ -5,88 +5,22 @@ test and constraints / restrictions on properties of the resources.
|
|
5
5
|
"""
|
6
6
|
|
7
7
|
from abc import ABC
|
8
|
-
from copy import deepcopy
|
9
8
|
from dataclasses import dataclass, fields
|
10
|
-
from logging import getLogger
|
11
9
|
from random import choice, shuffle
|
12
|
-
from typing import Any
|
10
|
+
from typing import Any
|
13
11
|
from uuid import uuid4
|
14
12
|
|
15
|
-
from
|
13
|
+
from robot.api import logger
|
16
14
|
|
17
|
-
|
15
|
+
from OpenApiLibCore import value_utils
|
16
|
+
from OpenApiLibCore.models import NullSchema, ObjectSchema, UnionTypeSchema
|
17
|
+
from OpenApiLibCore.parameter_utils import get_oas_name_from_safe_name
|
18
18
|
|
19
19
|
NOT_SET = object()
|
20
20
|
SENTINEL = object()
|
21
21
|
|
22
22
|
|
23
|
-
|
24
|
-
"""
|
25
|
-
Helper function to resolve allOf, anyOf and oneOf instances in a schema.
|
26
|
-
|
27
|
-
The schemas are used to generate values for headers, query parameters and json
|
28
|
-
bodies to be able to make requests.
|
29
|
-
"""
|
30
|
-
# Schema is mutable, so deepcopy to prevent mutation of original schema argument
|
31
|
-
resolved_schema = deepcopy(schema)
|
32
|
-
|
33
|
-
# allOf / anyOf / oneOf may be nested, so recursively resolve the dict-typed values
|
34
|
-
for key, value in resolved_schema.items():
|
35
|
-
if isinstance(value, dict):
|
36
|
-
resolved_schema[key] = resolve_schema(value)
|
37
|
-
|
38
|
-
# When handling allOf there should no duplicate keys, so the schema parts can
|
39
|
-
# just be merged after resolving the individual parts
|
40
|
-
if schema_parts := resolved_schema.pop("allOf", None):
|
41
|
-
for schema_part in schema_parts:
|
42
|
-
resolved_part = resolve_schema(schema_part)
|
43
|
-
resolved_schema = merge_schemas(resolved_schema, resolved_part)
|
44
|
-
# Handling anyOf and oneOf requires extra logic to deal with the "type" information.
|
45
|
-
# Some properties / parameters may be of different types and each type may have its
|
46
|
-
# own restrictions e.g. a parameter that accepts an enum value (string) or an
|
47
|
-
# integer value within a certain range.
|
48
|
-
# Since the library needs all this information for different purposes, the
|
49
|
-
# schema_parts cannot be merged, so a helper property / key "types" is introduced.
|
50
|
-
any_of = resolved_schema.pop("anyOf", [])
|
51
|
-
one_of = resolved_schema.pop("oneOf", [])
|
52
|
-
schema_parts = any_of if any_of else one_of
|
53
|
-
|
54
|
-
for schema_part in schema_parts:
|
55
|
-
resolved_part = resolve_schema(schema_part)
|
56
|
-
if isinstance(resolved_part, dict) and "type" in resolved_part.keys():
|
57
|
-
if "types" in resolved_schema.keys():
|
58
|
-
resolved_schema["types"].append(resolved_part)
|
59
|
-
else:
|
60
|
-
resolved_schema["types"] = [resolved_part]
|
61
|
-
else:
|
62
|
-
resolved_schema = merge_schemas(resolved_schema, resolved_part)
|
63
|
-
|
64
|
-
return resolved_schema
|
65
|
-
|
66
|
-
|
67
|
-
def merge_schemas(first: Dict[str, Any], second: Dict[str, Any]) -> Dict[str, Any]:
|
68
|
-
"""Helper method to merge two schemas, recursively."""
|
69
|
-
merged_schema = deepcopy(first)
|
70
|
-
for key, value in second.items():
|
71
|
-
# for existing keys, merge dict and list values, leave others unchanged
|
72
|
-
if key in merged_schema.keys():
|
73
|
-
if isinstance(value, dict):
|
74
|
-
# if the key holds a dict, merge the values (e.g. 'properties')
|
75
|
-
merged_schema[key].update(value)
|
76
|
-
elif isinstance(value, list):
|
77
|
-
# if the key holds a list, extend the values (e.g. 'required')
|
78
|
-
merged_schema[key].extend(value)
|
79
|
-
elif value != merged_schema[key]:
|
80
|
-
logger.debug(
|
81
|
-
f"key '{key}' with value '{merged_schema[key]}'"
|
82
|
-
f" not updated to '{value}'"
|
83
|
-
)
|
84
|
-
else:
|
85
|
-
merged_schema[key] = value
|
86
|
-
return merged_schema
|
87
|
-
|
88
|
-
|
89
|
-
class ResourceRelation(ABC): # pylint: disable=too-few-public-methods
|
23
|
+
class ResourceRelation(ABC):
|
90
24
|
"""ABC for all resource relations or restrictions within the API."""
|
91
25
|
|
92
26
|
property_name: str
|
@@ -95,10 +29,12 @@ class ResourceRelation(ABC): # pylint: disable=too-few-public-methods
|
|
95
29
|
|
96
30
|
@dataclass
|
97
31
|
class PathPropertiesConstraint(ResourceRelation):
|
98
|
-
"""The
|
32
|
+
"""The value to be used as the ``path`` for related requests."""
|
99
33
|
|
100
34
|
path: str
|
101
35
|
property_name: str = "id"
|
36
|
+
invalid_value: Any = NOT_SET
|
37
|
+
invalid_value_error_code: int = 422
|
102
38
|
error_code: int = 404
|
103
39
|
|
104
40
|
|
@@ -107,10 +43,11 @@ class PropertyValueConstraint(ResourceRelation):
|
|
107
43
|
"""The allowed values for property_name."""
|
108
44
|
|
109
45
|
property_name: str
|
110
|
-
values:
|
46
|
+
values: list[Any]
|
111
47
|
invalid_value: Any = NOT_SET
|
112
48
|
invalid_value_error_code: int = 422
|
113
49
|
error_code: int = 422
|
50
|
+
treat_as_mandatory: bool = False
|
114
51
|
|
115
52
|
|
116
53
|
@dataclass
|
@@ -119,7 +56,7 @@ class IdDependency(ResourceRelation):
|
|
119
56
|
|
120
57
|
property_name: str
|
121
58
|
get_path: str
|
122
|
-
operation_id:
|
59
|
+
operation_id: str = ""
|
123
60
|
error_code: int = 422
|
124
61
|
|
125
62
|
|
@@ -141,27 +78,40 @@ class UniquePropertyValueConstraint(ResourceRelation):
|
|
141
78
|
error_code: int = 422
|
142
79
|
|
143
80
|
|
144
|
-
Relation = Union[
|
145
|
-
IdDependency,
|
146
|
-
IdReference,
|
147
|
-
PathPropertiesConstraint,
|
148
|
-
PropertyValueConstraint,
|
149
|
-
UniquePropertyValueConstraint,
|
150
|
-
]
|
151
|
-
|
152
|
-
|
153
81
|
@dataclass
|
154
82
|
class Dto(ABC):
|
155
83
|
"""Base class for the Dto class."""
|
156
84
|
|
157
85
|
@staticmethod
|
158
|
-
def
|
86
|
+
def get_path_relations() -> list[PathPropertiesConstraint]:
|
87
|
+
"""Return the list of Relations for the header and query parameters."""
|
88
|
+
return []
|
89
|
+
|
90
|
+
def get_path_relations_for_error_code(
|
91
|
+
self, error_code: int
|
92
|
+
) -> list[PathPropertiesConstraint]:
|
93
|
+
"""Return the list of Relations associated with the given error_code."""
|
94
|
+
relations: list[PathPropertiesConstraint] = [
|
95
|
+
r
|
96
|
+
for r in self.get_path_relations()
|
97
|
+
if r.error_code == error_code
|
98
|
+
or (
|
99
|
+
getattr(r, "invalid_value_error_code", None) == error_code
|
100
|
+
and getattr(r, "invalid_value", None) != NOT_SET
|
101
|
+
)
|
102
|
+
]
|
103
|
+
return relations
|
104
|
+
|
105
|
+
@staticmethod
|
106
|
+
def get_parameter_relations() -> list[ResourceRelation]:
|
159
107
|
"""Return the list of Relations for the header and query parameters."""
|
160
108
|
return []
|
161
109
|
|
162
|
-
def get_parameter_relations_for_error_code(
|
110
|
+
def get_parameter_relations_for_error_code(
|
111
|
+
self, error_code: int
|
112
|
+
) -> list[ResourceRelation]:
|
163
113
|
"""Return the list of Relations associated with the given error_code."""
|
164
|
-
relations:
|
114
|
+
relations: list[ResourceRelation] = [
|
165
115
|
r
|
166
116
|
for r in self.get_parameter_relations()
|
167
117
|
if r.error_code == error_code
|
@@ -173,13 +123,18 @@ class Dto(ABC):
|
|
173
123
|
return relations
|
174
124
|
|
175
125
|
@staticmethod
|
176
|
-
def get_relations() ->
|
126
|
+
def get_relations() -> list[ResourceRelation]:
|
177
127
|
"""Return the list of Relations for the (json) body."""
|
178
128
|
return []
|
179
129
|
|
180
|
-
def
|
181
|
-
|
182
|
-
|
130
|
+
def get_body_relations_for_error_code(
|
131
|
+
self, error_code: int
|
132
|
+
) -> list[ResourceRelation]:
|
133
|
+
"""
|
134
|
+
Return the list of Relations associated with the given error_code that are
|
135
|
+
applicable to the body / payload of the request.
|
136
|
+
"""
|
137
|
+
relations: list[ResourceRelation] = [
|
183
138
|
r
|
184
139
|
for r in self.get_relations()
|
185
140
|
if r.error_code == error_code
|
@@ -192,33 +147,25 @@ class Dto(ABC):
|
|
192
147
|
|
193
148
|
def get_invalidated_data(
|
194
149
|
self,
|
195
|
-
schema:
|
150
|
+
schema: ObjectSchema,
|
196
151
|
status_code: int,
|
197
152
|
invalid_property_default_code: int,
|
198
|
-
) ->
|
153
|
+
) -> dict[str, Any]:
|
199
154
|
"""Return a data set with one of the properties set to an invalid value or type."""
|
200
|
-
properties:
|
155
|
+
properties: dict[str, Any] = self.as_dict()
|
201
156
|
|
202
|
-
|
203
|
-
|
204
|
-
relations = self.get_relations_for_error_code(error_code=status_code)
|
205
|
-
# filter PathProperyConstraints since in that case no data can be invalidated
|
206
|
-
relations = [
|
207
|
-
r for r in relations if not isinstance(r, PathPropertiesConstraint)
|
208
|
-
]
|
157
|
+
relations = self.get_body_relations_for_error_code(error_code=status_code)
|
209
158
|
property_names = [r.property_name for r in relations]
|
210
159
|
if status_code == invalid_property_default_code:
|
211
160
|
# add all properties defined in the schema, including optional properties
|
212
|
-
property_names.extend((schema
|
213
|
-
# remove duplicates
|
214
|
-
property_names = list(set(property_names))
|
161
|
+
property_names.extend((schema.properties.root.keys())) # type: ignore[union-attr]
|
215
162
|
if not property_names:
|
216
163
|
raise ValueError(
|
217
164
|
f"No property can be invalidated to cause status_code {status_code}"
|
218
165
|
)
|
219
|
-
# shuffle the property_names so different properties on
|
220
|
-
# when rerunning the test
|
221
|
-
shuffle(property_names)
|
166
|
+
# Remove duplicates, then shuffle the property_names so different properties on
|
167
|
+
# the Dto are invalidated when rerunning the test.
|
168
|
+
shuffle(list(set(property_names)))
|
222
169
|
for property_name in property_names:
|
223
170
|
# if possible, invalidate a constraint but send otherwise valid data
|
224
171
|
id_dependencies = [
|
@@ -227,12 +174,12 @@ class Dto(ABC):
|
|
227
174
|
if isinstance(r, IdDependency) and r.property_name == property_name
|
228
175
|
]
|
229
176
|
if id_dependencies:
|
230
|
-
|
177
|
+
invalid_id = uuid4().hex
|
231
178
|
logger.debug(
|
232
|
-
f"Breaking IdDependency for status_code {status_code}:
|
233
|
-
f"{
|
179
|
+
f"Breaking IdDependency for status_code {status_code}: setting "
|
180
|
+
f"{property_name} to {invalid_id}"
|
234
181
|
)
|
235
|
-
properties[property_name] =
|
182
|
+
properties[property_name] = invalid_id
|
236
183
|
return properties
|
237
184
|
|
238
185
|
invalid_value_from_constraint = [
|
@@ -253,31 +200,30 @@ class Dto(ABC):
|
|
253
200
|
)
|
254
201
|
return properties
|
255
202
|
|
256
|
-
value_schema = schema
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
value_schema = choice(value_schemas)
|
203
|
+
value_schema = schema.properties.root[property_name] # type: ignore[union-attr]
|
204
|
+
if isinstance(value_schema, UnionTypeSchema):
|
205
|
+
# Filter "type": "null" from the possible types since this indicates an
|
206
|
+
# optional / nullable property that can only be invalidated by sending
|
207
|
+
# invalid data of a non-null type
|
208
|
+
non_null_schemas = [
|
209
|
+
s
|
210
|
+
for s in value_schema.resolved_schemas
|
211
|
+
if not isinstance(s, NullSchema)
|
212
|
+
]
|
213
|
+
value_schema = choice(non_null_schemas)
|
268
214
|
|
269
215
|
# there may not be a current_value when invalidating an optional property
|
270
216
|
current_value = properties.get(property_name, SENTINEL)
|
271
217
|
if current_value is SENTINEL:
|
272
218
|
# the current_value isn't very relevant as long as the type is correct
|
273
219
|
# so no logic to handle Relations / objects / arrays here
|
274
|
-
property_type = value_schema
|
220
|
+
property_type = value_schema.type
|
275
221
|
if property_type == "object":
|
276
222
|
current_value = {}
|
277
223
|
elif property_type == "array":
|
278
224
|
current_value = []
|
279
225
|
else:
|
280
|
-
current_value =
|
226
|
+
current_value = value_schema.get_valid_value()
|
281
227
|
|
282
228
|
values_from_constraint = [
|
283
229
|
r.values[0]
|
@@ -293,14 +239,14 @@ class Dto(ABC):
|
|
293
239
|
)
|
294
240
|
properties[property_name] = invalid_value
|
295
241
|
logger.debug(
|
296
|
-
f"Property {property_name} changed to {invalid_value} (received from "
|
242
|
+
f"Property {property_name} changed to {invalid_value!r} (received from "
|
297
243
|
f"get_invalid_value)"
|
298
244
|
)
|
299
245
|
return properties
|
300
|
-
logger.
|
246
|
+
logger.warn("get_invalidated_data returned unchanged properties")
|
301
247
|
return properties # pragma: no cover
|
302
248
|
|
303
|
-
def as_dict(self) ->
|
249
|
+
def as_dict(self) -> dict[Any, Any]:
|
304
250
|
"""Return the dict representation of the Dto."""
|
305
251
|
result = {}
|
306
252
|
|
@@ -308,7 +254,7 @@ class Dto(ABC):
|
|
308
254
|
field_name = field.name
|
309
255
|
if field_name not in self.__dict__:
|
310
256
|
continue
|
311
|
-
original_name =
|
257
|
+
original_name = get_oas_name_from_safe_name(field_name)
|
312
258
|
result[original_name] = getattr(self, field_name)
|
313
259
|
|
314
260
|
return result
|