robotframework-openapitools 1.0.0b3__py3-none-any.whl → 1.0.0b5__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 (34) hide show
  1. OpenApiDriver/openapi_executors.py +15 -11
  2. OpenApiDriver/openapi_reader.py +12 -13
  3. OpenApiDriver/openapidriver.libspec +5 -42
  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 +54 -73
  8. OpenApiLibCore/data_generation/data_generation_core.py +75 -82
  9. OpenApiLibCore/data_invalidation.py +38 -25
  10. OpenApiLibCore/dto_base.py +48 -105
  11. OpenApiLibCore/dto_utils.py +31 -3
  12. OpenApiLibCore/localized_faker.py +88 -0
  13. OpenApiLibCore/models.py +723 -0
  14. OpenApiLibCore/openapi_libcore.libspec +48 -284
  15. OpenApiLibCore/openapi_libcore.py +54 -71
  16. OpenApiLibCore/parameter_utils.py +20 -14
  17. OpenApiLibCore/path_functions.py +10 -10
  18. OpenApiLibCore/path_invalidation.py +5 -7
  19. OpenApiLibCore/protocols.py +13 -5
  20. OpenApiLibCore/request_data.py +67 -102
  21. OpenApiLibCore/resource_relations.py +6 -5
  22. OpenApiLibCore/validation.py +50 -167
  23. OpenApiLibCore/value_utils.py +46 -358
  24. openapi_libgen/__init__.py +0 -46
  25. openapi_libgen/command_line.py +7 -19
  26. openapi_libgen/generator.py +84 -0
  27. openapi_libgen/parsing_utils.py +9 -5
  28. openapi_libgen/spec_parser.py +41 -114
  29. {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b5.dist-info}/METADATA +2 -1
  30. robotframework_openapitools-1.0.0b5.dist-info/RECORD +40 -0
  31. robotframework_openapitools-1.0.0b3.dist-info/RECORD +0 -37
  32. {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b5.dist-info}/LICENSE +0 -0
  33. {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b5.dist-info}/WHEEL +0 -0
  34. {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b5.dist-info}/entry_points.txt +0 -0
@@ -6,11 +6,15 @@ from functools import cached_property
6
6
  from random import sample
7
7
  from typing import Any
8
8
 
9
- from OpenApiLibCore.dto_base import (
10
- Dto,
11
- resolve_schema,
12
- )
9
+ from OpenApiLibCore.annotations import JSON
10
+ from OpenApiLibCore.dto_base import Dto
13
11
  from OpenApiLibCore.dto_utils import DefaultDto
12
+ from OpenApiLibCore.models import (
13
+ ObjectSchema,
14
+ ParameterObject,
15
+ ResolvedSchemaObjectTypes,
16
+ UnionTypeSchema,
17
+ )
14
18
 
15
19
 
16
20
  @dataclass
@@ -19,23 +23,23 @@ class RequestValues:
19
23
 
20
24
  url: str
21
25
  method: str
22
- params: dict[str, Any] = field(default_factory=dict)
23
- headers: dict[str, str] = field(default_factory=dict)
24
- json_data: dict[str, Any] = field(default_factory=dict)
26
+ params: dict[str, JSON] = field(default_factory=dict)
27
+ headers: dict[str, JSON] = field(default_factory=dict)
28
+ json_data: dict[str, JSON] = field(default_factory=dict)
25
29
 
26
- def override_body_value(self, name: str, value: Any) -> None:
30
+ def override_body_value(self, name: str, value: JSON) -> None:
27
31
  if name in self.json_data:
28
32
  self.json_data[name] = value
29
33
 
30
- def override_header_value(self, name: str, value: Any) -> None:
34
+ def override_header_value(self, name: str, value: JSON) -> None:
31
35
  if name in self.headers:
32
36
  self.headers[name] = value
33
37
 
34
- def override_param_value(self, name: str, value: str) -> None:
38
+ def override_param_value(self, name: str, value: JSON) -> None:
35
39
  if name in self.params:
36
40
  self.params[name] = str(value)
37
41
 
38
- def override_request_value(self, name: str, value: Any) -> None:
42
+ def override_request_value(self, name: str, value: JSON) -> None:
39
43
  self.override_body_value(name=name, value=value)
40
44
  self.override_header_value(name=name, value=value)
41
45
  self.override_param_value(name=name, value=value)
@@ -52,16 +56,14 @@ class RequestData:
52
56
  """Helper class to manage parameters used when making requests."""
53
57
 
54
58
  dto: Dto | DefaultDto = field(default_factory=DefaultDto)
55
- dto_schema: dict[str, Any] = field(default_factory=dict)
56
- parameters: list[dict[str, Any]] = field(default_factory=list)
57
- params: dict[str, Any] = field(default_factory=dict)
58
- headers: dict[str, Any] = field(default_factory=dict)
59
+ body_schema: ObjectSchema | None = None
60
+ parameters: list[ParameterObject] = field(default_factory=list)
61
+ params: dict[str, JSON] = field(default_factory=dict)
62
+ headers: dict[str, JSON] = field(default_factory=dict)
59
63
  has_body: bool = True
60
64
 
61
65
  def __post_init__(self) -> None:
62
66
  # prevent modification by reference
63
- self.dto_schema = deepcopy(self.dto_schema)
64
- self.parameters = deepcopy(self.parameters)
65
67
  self.params = deepcopy(self.params)
66
68
  self.headers = deepcopy(self.headers)
67
69
 
@@ -70,20 +72,24 @@ class RequestData:
70
72
  """Whether or not the dto data (json data) contains optional properties."""
71
73
 
72
74
  def is_required_property(property_name: str) -> bool:
73
- return property_name in self.dto_schema.get("required", [])
75
+ return property_name in self.required_property_names
74
76
 
75
77
  properties = (self.dto.as_dict()).keys()
76
78
  return not all(map(is_required_property, properties))
77
79
 
80
+ @property
81
+ def required_property_names(self) -> list[str]:
82
+ if self.body_schema:
83
+ return self.body_schema.required
84
+ return []
85
+
78
86
  @property
79
87
  def has_optional_params(self) -> bool:
80
88
  """Whether or not any of the query parameters are optional."""
81
89
 
82
90
  def is_optional_param(query_param: str) -> bool:
83
91
  optional_params = [
84
- p.get("name")
85
- for p in self.parameters
86
- if p.get("in") == "query" and not p.get("required")
92
+ p.name for p in self.parameters if p.in_ == "query" and not p.required
87
93
  ]
88
94
  return query_param in optional_params
89
95
 
@@ -96,47 +102,26 @@ class RequestData:
96
102
  restrictions, data type or by not providing them in a request.
97
103
  """
98
104
  result = set()
99
- params = [h for h in self.parameters if h.get("in") == "query"]
105
+ params = [h for h in self.parameters if h.in_ == "query"]
100
106
  for param in params:
101
107
  # required params can be omitted to invalidate a request
102
- if param["required"]:
103
- result.add(param["name"])
108
+ if param.required:
109
+ result.add(param.name)
104
110
  continue
105
111
 
106
- schema = resolve_schema(param["schema"])
107
- if schema.get("type", None):
108
- param_types = [schema]
112
+ if param.schema_ is None:
113
+ continue
114
+
115
+ possible_schemas: list[ResolvedSchemaObjectTypes] = []
116
+ if isinstance(param.schema_, UnionTypeSchema):
117
+ possible_schemas = param.schema_.resolved_schemas
109
118
  else:
110
- param_types = schema["types"]
111
- for param_type in param_types:
112
- # any basic non-string type except "null" can be invalidated by
113
- # replacing it with a string
114
- if param_type["type"] not in ["string", "array", "object", "null"]:
115
- result.add(param["name"])
116
- continue
117
- # enums, strings and arrays with boundaries can be invalidated
118
- if set(param_type.keys()).intersection(
119
- {
120
- "enum",
121
- "minLength",
122
- "maxLength",
123
- "minItems",
124
- "maxItems",
125
- }
126
- ):
127
- result.add(param["name"])
128
- continue
129
- # an array of basic non-string type can be invalidated by replacing the
130
- # items in the array with strings
131
- if param_type["type"] == "array" and param_type["items"][
132
- "type"
133
- ] not in [
134
- "string",
135
- "array",
136
- "object",
137
- "null",
138
- ]:
139
- result.add(param["name"])
119
+ possible_schemas = [param.schema_]
120
+
121
+ for param_schema in possible_schemas:
122
+ if param_schema.can_be_invalidated:
123
+ result.add(param.name)
124
+
140
125
  return result
141
126
 
142
127
  @property
@@ -145,9 +130,7 @@ class RequestData:
145
130
 
146
131
  def is_optional_header(header: str) -> bool:
147
132
  optional_headers = [
148
- p.get("name")
149
- for p in self.parameters
150
- if p.get("in") == "header" and not p.get("required")
133
+ p.name for p in self.parameters if p.in_ == "header" and not p.required
151
134
  ]
152
135
  return header in optional_headers
153
136
 
@@ -160,47 +143,26 @@ class RequestData:
160
143
  restrictions or by not providing them in a request.
161
144
  """
162
145
  result = set()
163
- headers = [h for h in self.parameters if h.get("in") == "header"]
146
+ headers = [h for h in self.parameters if h.in_ == "header"]
164
147
  for header in headers:
165
148
  # required headers can be omitted to invalidate a request
166
- if header["required"]:
167
- result.add(header["name"])
149
+ if header.required:
150
+ result.add(header.name)
151
+ continue
152
+
153
+ if header.schema_ is None:
168
154
  continue
169
155
 
170
- schema = resolve_schema(header["schema"])
171
- if schema.get("type", None):
172
- header_types = [schema]
156
+ possible_schemas: list[ResolvedSchemaObjectTypes] = []
157
+ if isinstance(header.schema_, UnionTypeSchema):
158
+ possible_schemas = header.schema_.resolved_schemas
173
159
  else:
174
- header_types = schema["types"]
175
- for header_type in header_types:
176
- # any basic non-string type except "null" can be invalidated by
177
- # replacing it with a string
178
- if header_type["type"] not in ["string", "array", "object", "null"]:
179
- result.add(header["name"])
180
- continue
181
- # enums, strings and arrays with boundaries can be invalidated
182
- if set(header_type.keys()).intersection(
183
- {
184
- "enum",
185
- "minLength",
186
- "maxLength",
187
- "minItems",
188
- "maxItems",
189
- }
190
- ):
191
- result.add(header["name"])
192
- continue
193
- # an array of basic non-string type can be invalidated by replacing the
194
- # items in the array with strings
195
- if header_type["type"] == "array" and header_type["items"][
196
- "type"
197
- ] not in [
198
- "string",
199
- "array",
200
- "object",
201
- "null",
202
- ]:
203
- result.add(header["name"])
160
+ possible_schemas = [header.schema_]
161
+
162
+ for param_schema in possible_schemas:
163
+ if param_schema.can_be_invalidated:
164
+ result.add(header.name)
165
+
204
166
  return result
205
167
 
206
168
  def get_required_properties_dict(self) -> dict[str, Any]:
@@ -211,7 +173,7 @@ class RequestData:
211
173
  for relation in relations
212
174
  if getattr(relation, "treat_as_mandatory", False)
213
175
  ]
214
- required_properties: list[str] = self.dto_schema.get("required", [])
176
+ required_properties = self.body_schema.required if self.body_schema else []
215
177
  required_properties.extend(mandatory_properties)
216
178
 
217
179
  required_properties_dict: dict[str, Any] = {}
@@ -223,7 +185,10 @@ class RequestData:
223
185
  def get_minimal_body_dict(self) -> dict[str, Any]:
224
186
  required_properties_dict = self.get_required_properties_dict()
225
187
 
226
- min_properties = self.dto_schema.get("minProperties", 0)
188
+ min_properties = 0
189
+ if self.body_schema and self.body_schema.minProperties is not None:
190
+ min_properties = self.body_schema.minProperties
191
+
227
192
  number_of_optional_properties_to_add = min_properties - len(
228
193
  required_properties_dict
229
194
  )
@@ -247,13 +212,13 @@ class RequestData:
247
212
 
248
213
  return {**required_properties_dict, **optional_properties_dict}
249
214
 
250
- def get_required_params(self) -> dict[str, str]:
215
+ def get_required_params(self) -> dict[str, JSON]:
251
216
  """Get the params dict containing only the required query parameters."""
252
217
  return {
253
218
  k: v for k, v in self.params.items() if k in self.required_parameter_names
254
219
  }
255
220
 
256
- def get_required_headers(self) -> dict[str, str]:
221
+ def get_required_headers(self) -> dict[str, JSON]:
257
222
  """Get the headers dict containing only the required headers."""
258
223
  return {
259
224
  k: v for k, v in self.headers.items() if k in self.required_parameter_names
@@ -271,11 +236,11 @@ class RequestData:
271
236
  for relation in relations
272
237
  if getattr(relation, "treat_as_mandatory", False)
273
238
  ]
274
- parameter_names = [p["name"] for p in self.parameters]
239
+ parameter_names = [p.name for p in self.parameters]
275
240
  mandatory_parameters = [
276
241
  p for p in mandatory_property_names if p in parameter_names
277
242
  ]
278
243
 
279
- required_parameters = [p["name"] for p in self.parameters if p.get("required")]
244
+ required_parameters = [p.name for p in self.parameters if p.required]
280
245
  required_parameters.extend(mandatory_parameters)
281
246
  return required_parameters
@@ -1,13 +1,12 @@
1
1
  """Module holding the functions related to relations between resources."""
2
2
 
3
- from typing import Any
4
-
5
3
  from requests import Response
6
4
  from robot.api import logger
7
5
  from robot.libraries.BuiltIn import BuiltIn
8
6
 
9
- import OpenApiLibCore.path_functions as pf
7
+ import OpenApiLibCore.path_functions as _path_functions
10
8
  from OpenApiLibCore.dto_base import IdReference
9
+ from OpenApiLibCore.models import OpenApiObject
11
10
  from OpenApiLibCore.request_data import RequestData
12
11
 
13
12
  run_keyword = BuiltIn().run_keyword
@@ -16,14 +15,16 @@ run_keyword = BuiltIn().run_keyword
16
15
  def ensure_in_use(
17
16
  url: str,
18
17
  base_url: str,
19
- openapi_spec: dict[str, Any],
18
+ openapi_spec: OpenApiObject,
20
19
  resource_relation: IdReference,
21
20
  ) -> None:
22
21
  resource_id = ""
23
22
 
24
23
  path = url.replace(base_url, "")
25
24
  path_parts = path.split("/")
26
- parameterized_path = pf.get_parametrized_path(path=path, openapi_spec=openapi_spec)
25
+ parameterized_path = _path_functions.get_parametrized_path(
26
+ path=path, openapi_spec=openapi_spec
27
+ )
27
28
  parameterized_path_parts = parameterized_path.split("/")
28
29
  for part, param_part in zip(
29
30
  reversed(path_parts), reversed(parameterized_path_parts)
@@ -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
 
@@ -71,7 +75,7 @@ def perform_validated_request(
71
75
  f"\nGot: {_json.dumps(response_json, indent=4, sort_keys=True)}"
72
76
  )
73
77
  raise AssertionError(
74
- f"Response status_code {response.status_code} was not {status_code}"
78
+ f"Response status_code {response.status_code} was not {status_code}."
75
79
  )
76
80
 
77
81
  run_keyword("validate_response", path, response, original_data)
@@ -86,7 +90,7 @@ def perform_validated_request(
86
90
  if response.ok:
87
91
  if get_response.ok:
88
92
  raise AssertionError(
89
- f"Resource still exists after deletion. Url was {request_values.url}"
93
+ f"Resource still exists after deletion. Url was {request_values.url}."
90
94
  )
91
95
  # if the path supports GET, 404 is expected, if not 405 is expected
92
96
  if get_response.status_code not in [404, 405]:
@@ -98,24 +102,10 @@ def perform_validated_request(
98
102
  elif not get_response.ok:
99
103
  raise AssertionError(
100
104
  f"Resource could not be retrieved after failed deletion. "
101
- f"Url was {request_values.url}, status_code was {get_response.status_code}"
105
+ f"Url was {request_values.url}, status_code was {get_response.status_code}."
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,20 @@ 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()
353
- # POST on /resource_type/{id}/array_item/ will return the updated {id} resource
354
- # instead of a newly created resource. In this case, the send_json must be
355
- # in the array of the 'array_item' property on {id}
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
+
356
282
  send_path: str = response.request.path_url
357
283
  response_path = response_data.get("href", None)
358
284
  if response_path and send_path not in response_path:
@@ -366,7 +292,7 @@ def validate_send_response(
366
292
  item for item in item_list if item["id"] == send_json["id"]
367
293
  ]
368
294
 
369
- # incoming arguments are dictionaries, so they can be validated as such
295
+ # TODO: add support for non-dict bodies
370
296
  validate_dict_response(send_dict=send_json, received_dict=response_data)
371
297
 
372
298
  # In case of PATCH requests, ensure that only send properties have changed
@@ -440,58 +366,15 @@ def _validate_response(
440
366
  logger.info(error_message)
441
367
 
442
368
 
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]:
369
+ def _get_response_object(
370
+ path: str, method: str, status_code: int, openapi_spec: OpenApiObject
371
+ ) -> ResponseObject:
494
372
  method = method.lower()
495
373
  status = str(status_code)
496
- spec: dict[str, Any] = {**openapi_spec}["paths"][path][method]["responses"][status]
497
- return spec
374
+ path_item = openapi_spec.paths[path]
375
+ path_operations = path_item.get_operations()
376
+ operation_data = path_operations.get(method)
377
+ if operation_data is None:
378
+ raise ValueError(f"method '{method}' not supported for {path}.")
379
+
380
+ return operation_data.responses[status]