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.
- OpenApiDriver/openapi_executors.py +15 -11
- OpenApiDriver/openapi_reader.py +12 -13
- OpenApiDriver/openapidriver.libspec +5 -42
- OpenApiLibCore/__init__.py +0 -2
- OpenApiLibCore/annotations.py +8 -1
- OpenApiLibCore/data_generation/__init__.py +0 -2
- OpenApiLibCore/data_generation/body_data_generation.py +54 -73
- OpenApiLibCore/data_generation/data_generation_core.py +75 -82
- OpenApiLibCore/data_invalidation.py +38 -25
- OpenApiLibCore/dto_base.py +48 -105
- OpenApiLibCore/dto_utils.py +31 -3
- OpenApiLibCore/localized_faker.py +88 -0
- OpenApiLibCore/models.py +723 -0
- OpenApiLibCore/openapi_libcore.libspec +48 -284
- OpenApiLibCore/openapi_libcore.py +54 -71
- OpenApiLibCore/parameter_utils.py +20 -14
- OpenApiLibCore/path_functions.py +10 -10
- OpenApiLibCore/path_invalidation.py +5 -7
- OpenApiLibCore/protocols.py +13 -5
- OpenApiLibCore/request_data.py +67 -102
- OpenApiLibCore/resource_relations.py +6 -5
- OpenApiLibCore/validation.py +50 -167
- 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/parsing_utils.py +9 -5
- openapi_libgen/spec_parser.py +41 -114
- {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b5.dist-info}/METADATA +2 -1
- robotframework_openapitools-1.0.0b5.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.0b5.dist-info}/LICENSE +0 -0
- {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b5.dist-info}/WHEEL +0 -0
- {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b5.dist-info}/entry_points.txt +0 -0
OpenApiLibCore/request_data.py
CHANGED
@@ -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.
|
10
|
-
|
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,
|
23
|
-
headers: dict[str,
|
24
|
-
json_data: dict[str,
|
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:
|
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:
|
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:
|
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:
|
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
|
-
|
56
|
-
parameters: list[
|
57
|
-
params: dict[str,
|
58
|
-
headers: dict[str,
|
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.
|
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.
|
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.
|
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
|
103
|
-
result.add(param
|
108
|
+
if param.required:
|
109
|
+
result.add(param.name)
|
104
110
|
continue
|
105
111
|
|
106
|
-
|
107
|
-
|
108
|
-
|
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
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
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.
|
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.
|
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
|
167
|
-
result.add(header
|
149
|
+
if header.required:
|
150
|
+
result.add(header.name)
|
151
|
+
continue
|
152
|
+
|
153
|
+
if header.schema_ is None:
|
168
154
|
continue
|
169
155
|
|
170
|
-
|
171
|
-
if
|
172
|
-
|
156
|
+
possible_schemas: list[ResolvedSchemaObjectTypes] = []
|
157
|
+
if isinstance(header.schema_, UnionTypeSchema):
|
158
|
+
possible_schemas = header.schema_.resolved_schemas
|
173
159
|
else:
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
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
|
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 =
|
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,
|
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,
|
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
|
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
|
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
|
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:
|
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 =
|
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)
|
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
|
|
@@ -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:
|
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,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
|
-
|
354
|
-
|
355
|
-
|
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
|
-
#
|
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
|
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]:
|
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
|
-
|
497
|
-
|
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]
|