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.
Files changed (31) hide show
  1. OpenApiDriver/openapi_executors.py +8 -8
  2. OpenApiDriver/openapi_reader.py +12 -13
  3. OpenApiDriver/openapidriver.libspec +4 -41
  4. OpenApiLibCore/__init__.py +0 -2
  5. OpenApiLibCore/annotations.py +8 -1
  6. OpenApiLibCore/data_generation/__init__.py +0 -2
  7. OpenApiLibCore/data_generation/body_data_generation.py +52 -71
  8. OpenApiLibCore/data_generation/data_generation_core.py +82 -62
  9. OpenApiLibCore/data_invalidation.py +37 -20
  10. OpenApiLibCore/dto_base.py +20 -86
  11. OpenApiLibCore/localized_faker.py +88 -0
  12. OpenApiLibCore/models.py +715 -0
  13. OpenApiLibCore/openapi_libcore.libspec +47 -283
  14. OpenApiLibCore/openapi_libcore.py +20 -46
  15. OpenApiLibCore/parameter_utils.py +23 -17
  16. OpenApiLibCore/path_functions.py +5 -4
  17. OpenApiLibCore/protocols.py +7 -5
  18. OpenApiLibCore/request_data.py +67 -102
  19. OpenApiLibCore/resource_relations.py +2 -3
  20. OpenApiLibCore/validation.py +49 -161
  21. OpenApiLibCore/value_utils.py +46 -358
  22. openapi_libgen/__init__.py +0 -46
  23. openapi_libgen/command_line.py +7 -19
  24. openapi_libgen/generator.py +84 -0
  25. openapi_libgen/spec_parser.py +40 -113
  26. {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b4.dist-info}/METADATA +2 -1
  27. robotframework_openapitools-1.0.0b4.dist-info/RECORD +40 -0
  28. robotframework_openapitools-1.0.0b3.dist-info/RECORD +0 -37
  29. {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b4.dist-info}/LICENSE +0 -0
  30. {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b4.dist-info}/WHEEL +0 -0
  31. {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b4.dist-info}/entry_points.txt +0 -0
@@ -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.dto_base import resolve_schema
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: dict[str, Any],
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
- response_spec = _get_response_spec(
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 response_spec.get("content"):
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(response_spec["content"].keys())
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 = resolve_schema(response_spec["content"][content_type]["schema"])
193
-
194
- response_types = response_schema.get("types")
195
- if response_types:
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 list_item_schema := response_schema.get("items"):
205
- if not isinstance(json_response, list):
206
- raise AssertionError(
207
- f"Response schema violation: the schema specifies an array as "
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 validate_resource_properties(
233
- resource: dict[str, Any], schema: dict[str, Any]
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
- schema_properties = schema.get("properties", {})
236
- property_names_from_schema = set(schema_properties.keys())
237
- property_names_in_resource = set(resource.keys())
238
-
239
- if property_names_from_schema != property_names_in_resource:
240
- # The additionalProperties property determines whether properties with
241
- # unspecified names are allowed. This property can be boolean or an object
242
- # (dict) that specifies the type of any additional properties.
243
- additional_properties = schema.get("additionalProperties", True)
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
- # incoming arguments are dictionaries, so they can be validated as such
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 _validate_value_type(value: Any, expected_type: str) -> None:
444
- type_mapping = {
445
- "string": str,
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
- spec: dict[str, Any] = {**openapi_spec}["paths"][path][method]["responses"][status]
497
- return spec
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]