robotframework-openapitools 1.0.0b3__py3-none-any.whl → 1.0.0b4__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/openapi_executors.py +8 -8
- OpenApiDriver/openapi_reader.py +12 -13
- OpenApiDriver/openapidriver.libspec +4 -41
- OpenApiLibCore/__init__.py +0 -2
- OpenApiLibCore/annotations.py +8 -1
- OpenApiLibCore/data_generation/__init__.py +0 -2
- OpenApiLibCore/data_generation/body_data_generation.py +52 -71
- OpenApiLibCore/data_generation/data_generation_core.py +82 -62
- OpenApiLibCore/data_invalidation.py +37 -20
- OpenApiLibCore/dto_base.py +20 -86
- OpenApiLibCore/localized_faker.py +88 -0
- OpenApiLibCore/models.py +715 -0
- OpenApiLibCore/openapi_libcore.libspec +47 -283
- OpenApiLibCore/openapi_libcore.py +20 -46
- OpenApiLibCore/parameter_utils.py +23 -17
- OpenApiLibCore/path_functions.py +5 -4
- OpenApiLibCore/protocols.py +7 -5
- OpenApiLibCore/request_data.py +67 -102
- OpenApiLibCore/resource_relations.py +2 -3
- OpenApiLibCore/validation.py +49 -161
- OpenApiLibCore/value_utils.py +46 -358
- openapi_libgen/__init__.py +0 -46
- openapi_libgen/command_line.py +7 -19
- openapi_libgen/generator.py +84 -0
- openapi_libgen/spec_parser.py +40 -113
- {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b4.dist-info}/METADATA +2 -1
- robotframework_openapitools-1.0.0b4.dist-info/RECORD +40 -0
- robotframework_openapitools-1.0.0b3.dist-info/RECORD +0 -37
- {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b4.dist-info}/LICENSE +0 -0
- {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b4.dist-info}/WHEEL +0 -0
- {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b4.dist-info}/entry_points.txt +0 -0
OpenApiLibCore/validation.py
CHANGED
@@ -18,7 +18,11 @@ from robot.api import logger
|
|
18
18
|
from robot.api.exceptions import Failure
|
19
19
|
from robot.libraries.BuiltIn import BuiltIn
|
20
20
|
|
21
|
-
from OpenApiLibCore.
|
21
|
+
from OpenApiLibCore.models import (
|
22
|
+
OpenApiObject,
|
23
|
+
ResponseObject,
|
24
|
+
UnionTypeSchema,
|
25
|
+
)
|
22
26
|
from OpenApiLibCore.protocols import ResponseValidatorType
|
23
27
|
from OpenApiLibCore.request_data import RequestData, RequestValues
|
24
28
|
|
@@ -102,20 +106,6 @@ def perform_validated_request(
|
|
102
106
|
)
|
103
107
|
|
104
108
|
|
105
|
-
def assert_href_to_resource_is_valid(
|
106
|
-
href: str, origin: str, base_url: str, referenced_resource: dict[str, Any]
|
107
|
-
) -> None:
|
108
|
-
url = f"{origin}{href}"
|
109
|
-
path = url.replace(base_url, "")
|
110
|
-
request_data: RequestData = run_keyword("get_request_data", path, "GET")
|
111
|
-
params = request_data.params
|
112
|
-
headers = request_data.headers
|
113
|
-
get_response = run_keyword("authorized_request", url, "GET", params, headers)
|
114
|
-
assert get_response.json() == referenced_resource, (
|
115
|
-
f"{get_response.json()} not equal to original {referenced_resource}"
|
116
|
-
)
|
117
|
-
|
118
|
-
|
119
109
|
def validate_response(
|
120
110
|
path: str,
|
121
111
|
response: Response,
|
@@ -124,7 +114,7 @@ def validate_response(
|
|
124
114
|
disable_server_validation: bool,
|
125
115
|
invalid_property_default_response: int,
|
126
116
|
response_validation: str,
|
127
|
-
openapi_spec:
|
117
|
+
openapi_spec: OpenApiObject,
|
128
118
|
original_data: Mapping[str, Any],
|
129
119
|
) -> None:
|
130
120
|
if response.status_code == int(HTTPStatus.NO_CONTENT):
|
@@ -153,7 +143,7 @@ def validate_response(
|
|
153
143
|
)
|
154
144
|
return None
|
155
145
|
|
156
|
-
|
146
|
+
response_object = _get_response_object(
|
157
147
|
path=path,
|
158
148
|
method=request_method,
|
159
149
|
status_code=response.status_code,
|
@@ -163,14 +153,14 @@ def validate_response(
|
|
163
153
|
content_type_from_response = response.headers.get("Content-Type", "unknown")
|
164
154
|
mime_type_from_response, _, _ = content_type_from_response.partition(";")
|
165
155
|
|
166
|
-
if not
|
156
|
+
if not response_object.content:
|
167
157
|
logger.warn(
|
168
158
|
"The response cannot be validated: 'content' not specified in the OAS."
|
169
159
|
)
|
170
160
|
return None
|
171
161
|
|
172
162
|
# multiple content types can be specified in the OAS
|
173
|
-
content_types = list(
|
163
|
+
content_types = list(response_object.content.keys())
|
174
164
|
supported_types = [
|
175
165
|
ct for ct in content_types if ct.partition(";")[0].endswith("json")
|
176
166
|
]
|
@@ -189,39 +179,17 @@ def validate_response(
|
|
189
179
|
)
|
190
180
|
|
191
181
|
json_response = response.json()
|
192
|
-
response_schema =
|
193
|
-
|
194
|
-
|
195
|
-
if
|
196
|
-
# In case of oneOf / anyOf there can be multiple possible response types
|
197
|
-
# which makes generic validation too complex
|
198
|
-
return None
|
199
|
-
response_type = response_schema.get("type", "undefined")
|
200
|
-
if response_type not in ["object", "array"]:
|
201
|
-
_validate_value_type(value=json_response, expected_type=response_type)
|
182
|
+
response_schema = response_object.content[content_type].schema_
|
183
|
+
# No additional validations if schema is missing or when multiple responses
|
184
|
+
# are possible.
|
185
|
+
if not response_schema or isinstance(response_schema, UnionTypeSchema):
|
202
186
|
return None
|
203
187
|
|
204
|
-
if
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
f"response type but the response was of type {type(json_response)}."
|
209
|
-
)
|
210
|
-
type_of_list_items = list_item_schema.get("type")
|
211
|
-
if type_of_list_items == "object":
|
212
|
-
for resource in json_response:
|
213
|
-
run_keyword("validate_resource_properties", resource, list_item_schema)
|
214
|
-
else:
|
215
|
-
for item in json_response:
|
216
|
-
_validate_value_type(value=item, expected_type=type_of_list_items)
|
217
|
-
# no further validation; value validation of individual resources should
|
218
|
-
# be performed on the path for the specific resources
|
219
|
-
return None
|
188
|
+
# ensure the href is valid if the response is an object that contains a href
|
189
|
+
if isinstance(json_response, dict):
|
190
|
+
if href := json_response.get("href"):
|
191
|
+
run_keyword("assert_href_to_resource_is_valid", href, json_response)
|
220
192
|
|
221
|
-
run_keyword("validate_resource_properties", json_response, response_schema)
|
222
|
-
# ensure the href is valid if present in the response
|
223
|
-
if href := json_response.get("href"):
|
224
|
-
run_keyword("assert_href_to_resource_is_valid", href, json_response)
|
225
193
|
# every property that was sucessfully send and that is in the response
|
226
194
|
# schema must have the value that was send
|
227
195
|
if response.ok and response.request.method in ["POST", "PUT", "PATCH"]:
|
@@ -229,65 +197,18 @@ def validate_response(
|
|
229
197
|
return None
|
230
198
|
|
231
199
|
|
232
|
-
def
|
233
|
-
|
200
|
+
def assert_href_to_resource_is_valid(
|
201
|
+
href: str, origin: str, base_url: str, referenced_resource: dict[str, Any]
|
234
202
|
) -> None:
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
if isinstance(additional_properties, bool):
|
245
|
-
allow_additional_properties = additional_properties
|
246
|
-
allowed_additional_properties_type = None
|
247
|
-
else:
|
248
|
-
allow_additional_properties = True
|
249
|
-
allowed_additional_properties_type = additional_properties["type"]
|
250
|
-
|
251
|
-
extra_property_names = property_names_in_resource.difference(
|
252
|
-
property_names_from_schema
|
253
|
-
)
|
254
|
-
if allow_additional_properties:
|
255
|
-
# If a type is defined for extra properties, validate them
|
256
|
-
if allowed_additional_properties_type:
|
257
|
-
extra_properties = {
|
258
|
-
key: value
|
259
|
-
for key, value in resource.items()
|
260
|
-
if key in extra_property_names
|
261
|
-
}
|
262
|
-
_validate_type_of_extra_properties(
|
263
|
-
extra_properties=extra_properties,
|
264
|
-
expected_type=allowed_additional_properties_type,
|
265
|
-
)
|
266
|
-
# If allowed, validation should not fail on extra properties
|
267
|
-
extra_property_names = set()
|
268
|
-
|
269
|
-
required_properties = set(schema.get("required", []))
|
270
|
-
missing_properties = required_properties.difference(property_names_in_resource)
|
271
|
-
|
272
|
-
if extra_property_names or missing_properties:
|
273
|
-
extra = (
|
274
|
-
f"\n\tExtra properties in response: {extra_property_names}"
|
275
|
-
if extra_property_names
|
276
|
-
else ""
|
277
|
-
)
|
278
|
-
missing = (
|
279
|
-
f"\n\tRequired properties missing in response: {missing_properties}"
|
280
|
-
if missing_properties
|
281
|
-
else ""
|
282
|
-
)
|
283
|
-
raise AssertionError(
|
284
|
-
f"Response schema violation: the response contains properties that are "
|
285
|
-
f"not specified in the schema or does not contain properties that are "
|
286
|
-
f"required according to the schema."
|
287
|
-
f"\n\tReceived in the response: {property_names_in_resource}"
|
288
|
-
f"\n\tDefined in the schema: {property_names_from_schema}"
|
289
|
-
f"{extra}{missing}"
|
290
|
-
)
|
203
|
+
url = f"{origin}{href}"
|
204
|
+
path = url.replace(base_url, "")
|
205
|
+
request_data: RequestData = run_keyword("get_request_data", path, "GET")
|
206
|
+
params = request_data.params
|
207
|
+
headers = request_data.headers
|
208
|
+
get_response = run_keyword("authorized_request", url, "GET", params, headers)
|
209
|
+
assert get_response.json() == referenced_resource, (
|
210
|
+
f"{get_response.json()} not equal to original {referenced_resource}"
|
211
|
+
)
|
291
212
|
|
292
213
|
|
293
214
|
def validate_send_response(
|
@@ -344,15 +265,25 @@ def validate_send_response(
|
|
344
265
|
"on the provided response was None."
|
345
266
|
)
|
346
267
|
return None
|
268
|
+
|
347
269
|
if isinstance(response.request.body, bytes):
|
348
270
|
send_json = _json.loads(response.request.body.decode("UTF-8"))
|
349
271
|
else:
|
350
272
|
send_json = _json.loads(response.request.body)
|
351
273
|
|
352
274
|
response_data = response.json()
|
275
|
+
if not isinstance(response_data, dict):
|
276
|
+
logger.info(
|
277
|
+
"Could not validate send data against the response; "
|
278
|
+
"the received response was not a representation of a resource."
|
279
|
+
)
|
280
|
+
return None
|
281
|
+
|
282
|
+
# FIXME: this applies to removed code
|
353
283
|
# POST on /resource_type/{id}/array_item/ will return the updated {id} resource
|
354
284
|
# instead of a newly created resource. In this case, the send_json must be
|
355
285
|
# in the array of the 'array_item' property on {id}
|
286
|
+
|
356
287
|
send_path: str = response.request.path_url
|
357
288
|
response_path = response_data.get("href", None)
|
358
289
|
if response_path and send_path not in response_path:
|
@@ -366,7 +297,7 @@ def validate_send_response(
|
|
366
297
|
item for item in item_list if item["id"] == send_json["id"]
|
367
298
|
]
|
368
299
|
|
369
|
-
#
|
300
|
+
# TODO: add support for non-dict bodies
|
370
301
|
validate_dict_response(send_dict=send_json, received_dict=response_data)
|
371
302
|
|
372
303
|
# In case of PATCH requests, ensure that only send properties have changed
|
@@ -440,58 +371,15 @@ def _validate_response(
|
|
440
371
|
logger.info(error_message)
|
441
372
|
|
442
373
|
|
443
|
-
def
|
444
|
-
|
445
|
-
|
446
|
-
"number": float,
|
447
|
-
"integer": int,
|
448
|
-
"boolean": bool,
|
449
|
-
"array": list,
|
450
|
-
"object": dict,
|
451
|
-
}
|
452
|
-
python_type = type_mapping.get(expected_type, None)
|
453
|
-
if python_type is None:
|
454
|
-
raise AssertionError(f"Validation of type '{expected_type}' is not supported.")
|
455
|
-
if not isinstance(value, python_type):
|
456
|
-
raise AssertionError(f"{value} is not of type {expected_type}")
|
457
|
-
|
458
|
-
|
459
|
-
def _validate_type_of_extra_properties(
|
460
|
-
extra_properties: dict[str, Any], expected_type: str
|
461
|
-
) -> None:
|
462
|
-
type_mapping = {
|
463
|
-
"string": str,
|
464
|
-
"number": float,
|
465
|
-
"integer": int,
|
466
|
-
"boolean": bool,
|
467
|
-
"array": list,
|
468
|
-
"object": dict,
|
469
|
-
}
|
470
|
-
|
471
|
-
python_type = type_mapping.get(expected_type, None)
|
472
|
-
if python_type is None:
|
473
|
-
logger.warn(
|
474
|
-
f"Additonal properties were not validated: "
|
475
|
-
f"type '{expected_type}' is not supported."
|
476
|
-
)
|
477
|
-
return
|
478
|
-
|
479
|
-
invalid_extra_properties = {
|
480
|
-
key: value
|
481
|
-
for key, value in extra_properties.items()
|
482
|
-
if not isinstance(value, python_type)
|
483
|
-
}
|
484
|
-
if invalid_extra_properties:
|
485
|
-
raise AssertionError(
|
486
|
-
f"Response contains invalid additionalProperties: "
|
487
|
-
f"{invalid_extra_properties} are not of type {expected_type}."
|
488
|
-
)
|
489
|
-
|
490
|
-
|
491
|
-
def _get_response_spec(
|
492
|
-
path: str, method: str, status_code: int, openapi_spec: dict[str, Any]
|
493
|
-
) -> dict[str, Any]:
|
374
|
+
def _get_response_object(
|
375
|
+
path: str, method: str, status_code: int, openapi_spec: OpenApiObject
|
376
|
+
) -> ResponseObject:
|
494
377
|
method = method.lower()
|
495
378
|
status = str(status_code)
|
496
|
-
|
497
|
-
|
379
|
+
path_item = openapi_spec.paths[path]
|
380
|
+
path_operations = path_item.get_operations()
|
381
|
+
operation_data = path_operations.get(method)
|
382
|
+
if operation_data is None:
|
383
|
+
raise ValueError(f"method '{method}' not supported for {path}")
|
384
|
+
|
385
|
+
return operation_data.responses[status]
|