-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathjson_asserter.py
More file actions
472 lines (353 loc) · 18.9 KB
/
json_asserter.py
File metadata and controls
472 lines (353 loc) · 18.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
"""
Copyright 2019 ShipChain, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from rest_framework import status
# pylint: disable=too-many-branches
# pylint: disable=too-many-arguments
class EntityReferenceClass:
def __init__(self, resource=None, pk=None, attributes=None, relationships=None, meta=None):
self.resource = resource
self.pk = pk
self.attributes = attributes
self.relationships = relationships
self.meta = meta
if self.pk is not None:
# entity_ref.pk can be a CharField or a UUIDField. Force to string for easier comparisons.
self.pk = str(self.pk)
def __str__(self):
return f'Type: {self.resource}; ID: {self.pk}; ' \
f'attributes: {self.attributes}; relationships: {self.relationships}'
def _vnd_has_error(response_json, error, pointer=None):
assert 'errors' in response_json, f'Malformed error response: {response_json}'
errors = response_json['errors']
assert isinstance(errors, list), f'Error response not a list: {errors}'
error_found = False
for single_error in errors:
if error in single_error['detail']:
error_found = True
if pointer:
found_pointer = single_error['source']['pointer']
assert pointer in found_pointer, f'Error `{pointer}` not found in {found_pointer}'
if not error_found:
assert False, f'Error `{error}` not found in {errors}'
def _json_has_error(errors, error):
assert isinstance(errors, dict), f'Error response not a dict: {errors}'
assert 'detail' in errors, f'Malformed error response: {errors}'
assert error in errors['detail'], f'Error {error} not found in {errors["detail"]}'
def response_has_error(response, error, pointer=None, vnd=True):
if error is None:
return
response_json = response.json()
# application/vnd.api+json
if vnd:
_vnd_has_error(response_json, error, pointer)
# application/json
else:
_json_has_error(response_json, error)
def _vnd_assert_attributes(response_data, attributes):
"""
Scan response data for all attributes
"""
assert 'attributes' in response_data, f'Attributes missing in {response_data}'
response_attributes = response_data['attributes']
for key, value in attributes.items():
assert key in response_attributes, f'Missing Attribute `{key}` in {response_attributes}'
assert response_attributes[key] == value, f'Attribute Value incorrect `{value}` in {response_attributes}'
def _vnd_assert_entity_ref_in_list(response_list, entity_ref, skip_attributes_property=False):
found_include = False
if entity_ref.attributes is None:
entity_ref.attributes = dict()
for response_single in response_list:
if entity_ref.resource and entity_ref.pk:
if response_single['type'] == entity_ref.resource and response_single['id'] == entity_ref.pk:
found_include = True
if skip_attributes_property:
break
for attr_key, attr_value in entity_ref.attributes.items():
assert attr_key in response_single['attributes'], \
f'List Attribute key `{attr_key}` missing in {response_single}'
assert response_single['attributes'][attr_key] == attr_value, \
f'List Attribute Value incorrect `{attr_value}` in {response_single}'
if entity_ref.meta:
_vnd_assert_meta(response_single, entity_ref.meta)
break
else:
single_attribute_failed = False
for attr_key, attr_value in entity_ref.attributes.items():
if attr_key not in response_single['attributes']:
single_attribute_failed = True
break
elif response_single['attributes'][attr_key] != attr_value:
single_attribute_failed = True
break
if entity_ref.meta:
_vnd_assert_meta(response_single, entity_ref.meta)
if not single_attribute_failed:
found_include = True
assert found_include, f'{entity_ref} NOT IN {response_list}'
def _vnd_assert_relationships(response_data, relationships):
"""
Scan response data for all relationships
"""
assert 'relationships' in response_data, f'Relationships missing in {response_data}'
response_relationships = response_data['relationships']
if not isinstance(relationships, list):
relationships = [relationships]
for relationship in relationships:
for relationship_name, relationship_refs in relationship.items():
assert relationship_name in response_relationships, \
f'Relationship `{relationship_name}` not in {response_relationships}'
if not isinstance(relationship_refs, list):
relationship_refs = [relationship_refs]
for relationship_ref in relationship_refs:
assert isinstance(relationship_ref, EntityReferenceClass), \
f'asserted relationship is not an EntityRef {relationship_ref}'
assert 'data' in response_relationships[relationship_name], \
f'Data missing in {relationship_name} relationship : {response_relationships[relationship_name]}'
if isinstance(response_relationships[relationship_name]['data'], list):
_vnd_assert_entity_ref_in_list(response_relationships[relationship_name]['data'], relationship_ref,
skip_attributes_property=True)
else:
if relationship_ref.resource:
assert response_relationships[relationship_name]['data']['type'] == relationship_ref.resource, \
f'EntityRef resource type `{relationship_ref.resource}` does not match {response_relationships}'
if relationship_ref.pk:
assert response_relationships[relationship_name]['data']['id'] == relationship_ref.pk, \
f'EntityRef ID `{relationship_ref.pk}` does not match {response_relationships}'
def _vnd_assert_meta(response_data, meta_data):
"""
Scan response data for meta data
"""
assert 'meta' in response_data, f'Meta missing in {response_data}'
assert isinstance(meta_data, dict), f'Invalid format for meta data {type(meta_data)}, must be dict'
response_meta = response_data['meta']
for key, value in meta_data.items():
assert key in response_meta, f'Meta field `{key}` not found in {response_meta}'
assert response_meta[key] == value,\
f'Meta field `{key}` had value `{response_meta[key]}` not `{value}` as expected.'
def _vnd_assert_include(response, included):
"""
Scan a response for all included resources
"""
assert 'included' in response, f'Included missing in {response}'
response_included = response['included']
if not isinstance(included, list):
included = [included]
for single_include in included:
assert isinstance(single_include, EntityReferenceClass), \
f'asserted includes is not an EntityRef {single_include}'
_vnd_assert_entity_ref_in_list(response_included, single_include)
def _vnd_assertions(response_data, entity_ref):
if entity_ref.resource:
assert response_data['type'] == entity_ref.resource, f'Invalid Resource Type in {response_data}'
if entity_ref.pk:
assert response_data['id'] == entity_ref.pk, f'Invalid ID in {response_data}'
if entity_ref.attributes:
_vnd_assert_attributes(response_data, entity_ref.attributes)
if entity_ref.relationships:
_vnd_assert_relationships(response_data, entity_ref.relationships)
if entity_ref.meta:
_vnd_assert_meta(response_data, entity_ref.meta)
def _plain_assert_attributes_in_response(response, attributes):
for key, value in attributes.items():
assert key in response, f'Missing Attribute `{key}` in {response}'
if isinstance(value, dict):
_plain_assert_attributes_in_response(response[key], value)
else:
assert response[key] == value, f'Attribute Value incorrect `{value}` in {response}'
def _plain_assert_attributes_in_list(response_list, attributes):
found_include = False
if attributes is None:
attributes = dict()
for response_single in response_list:
single_attribute_failed = False
try:
_plain_assert_attributes_in_response(response_single, attributes)
except AssertionError:
single_attribute_failed = True
if not single_attribute_failed:
found_include = True
assert found_include, f'{attributes} NOT IN {response_list}'
def _test_vnd_json(response, entity_refs=None, included=None, is_list=False, count=None, resource=None, pk=None,
attributes=None, relationships=None, check_ordering=False):
assert 'data' in response, f'response does not contain `data` property: {response}'
# if (attributes or relationships or resource or pk) and entity_refs:
assert not ((attributes or relationships or resource or pk) and entity_refs), \
'Use Only `entity_refs` or explicit `attributes`, `relationships`, `resource`, and `pk` but not both'
if (attributes or relationships or resource or pk) and not entity_refs:
entity_refs = EntityReferenceClass(resource=resource,
pk=pk,
attributes=attributes,
relationships=relationships)
response_data = response['data']
if is_list:
assert isinstance(response_data, list), f'Response should be a list'
# Included resources are outside of the list response
if included:
_vnd_assert_include(response, included)
# Assertion for only included and not entities is valid
if entity_refs:
if not isinstance(entity_refs, list):
entity_refs = [entity_refs]
if not check_ordering:
for entity_ref in entity_refs:
_vnd_assert_entity_ref_in_list(response_data, entity_ref)
else:
assert len(entity_refs) <= len(response_data), \
f'Error: more entity refs supplied than available in response data. ' \
f'{len(response_data)} found asserted {len(entity_refs)}'
for iteration, entity_ref in enumerate(entity_refs):
_vnd_assertions(response_data[iteration], entity_ref)
if count is not None:
assert len(response_data) == count, \
f'Difference in count of response_data, got {len(response_data)} expected {count}'
else:
assert not isinstance(response_data, list), f'Response should not be a list'
assert not (entity_refs and isinstance(entity_refs, list)), \
f'entity_refs should not be a list for a non-list response'
assert (count is None), 'Count is only checked when response is list'
assert not check_ordering, 'Ordering is only checked when response is list'
# Included resources are outside of the list response
if included:
_vnd_assert_include(response, included)
# Assertion for only status is valid
if entity_refs:
_vnd_assertions(response_data, entity_refs)
def _test_regular_json(response, entity_refs=None, included=None, is_list=False, count=None, attributes=None,
relationships=None, check_ordering=False):
assert not relationships, f'relationships not valid when vnd=False'
assert not entity_refs, f'entity_refs not valid when vnd=False'
assert not included, f'included not valid when vnd=False'
assert attributes, f'attributes must be provided when vnd=False'
if is_list:
assert isinstance(response, list), f'Response should be a list'
if not isinstance(attributes, list):
attributes = [attributes]
if not check_ordering:
for attribute in attributes:
_plain_assert_attributes_in_list(response, attribute)
else:
assert len(attributes) <= len(response), \
f'Error: more attributes supplied than available in response. ' \
f'{len(attributes)} found asserted {len(response)}'
for iteration, attribute in enumerate(attributes):
_plain_assert_attributes_in_response(response[iteration], attribute)
if count is not None:
assert len(response) == count,\
f'Difference in count of response_data, got {len(response)} expected {count}'
else:
assert not isinstance(response, list), f'Response should not be a list'
assert not (attributes and isinstance(attributes, list)), \
f'attributes should not be a list for a non-list response'
_plain_assert_attributes_in_response(response, attributes)
def response_has_data(response, vnd=True, entity_refs=None, included=None, is_list=False, count=None,
resource=None, pk=None, attributes=None, relationships=None, check_ordering=False):
response = response.json()
# application/vnd.api+json
if vnd:
_test_vnd_json(response, entity_refs, included, is_list, count, resource, pk, attributes, relationships,
check_ordering)
# application/json
else:
_test_regular_json(response, entity_refs, included, is_list, count, attributes, relationships, check_ordering)
def assert_200(response, vnd=True, entity_refs=None, included=None, is_list=False, count=None, check_ordering=False,
resource=None, pk=None, attributes=None, relationships=None):
assert response is not None
assert response.status_code == status.HTTP_200_OK, f'status_code {response.status_code} != 200'
response_has_data(response,
attributes=attributes,
relationships=relationships,
included=included,
is_list=is_list,
vnd=vnd,
resource=resource,
pk=pk,
entity_refs=entity_refs,
count=count,
check_ordering=check_ordering)
def assert_201(response, vnd=True, entity_refs=None, included=None, is_list=False,
resource=None, pk=None, attributes=None, relationships=None):
assert response is not None
assert response.status_code == status.HTTP_201_CREATED, f'status_code {response.status_code} != 201'
response_has_data(response,
attributes=attributes,
relationships=relationships,
included=included,
is_list=is_list,
vnd=vnd,
resource=resource,
pk=pk,
entity_refs=entity_refs)
def assert_202(response, vnd=True, entity_refs=None, included=None, is_list=False,
resource=None, pk=None, attributes=None, relationships=None):
assert response is not None
assert response.status_code == status.HTTP_202_ACCEPTED, f'status_code {response.status_code} != 202'
response_has_data(response,
attributes=attributes,
relationships=relationships,
included=included,
is_list=is_list,
vnd=vnd,
resource=resource,
pk=pk,
entity_refs=entity_refs)
def assert_204(response):
assert response is not None
assert response.status_code == status.HTTP_204_NO_CONTENT, f'status_code {response.status_code} != 204'
def assert_400(response, error=None, pointer=None, vnd=True):
assert response is not None
assert response.status_code == status.HTTP_400_BAD_REQUEST, f'status_code {response.status_code} != 400'
response_has_error(response, error, pointer, vnd)
def assert_401(response, error='Authentication credentials were not provided', vnd=True):
assert response is not None
assert response.status_code == status.HTTP_401_UNAUTHORIZED, f'status_code {response.status_code} != 401'
response_has_error(response, error, vnd=vnd)
def assert_402(response, error='Request denied due to the restrictions of your current billing tier.', vnd=True):
assert response is not None
assert response.status_code == status.HTTP_402_PAYMENT_REQUIRED, f'status_code {response.status_code} != 402'
response_has_error(response, error, vnd=vnd)
def assert_403(response, error='You do not have permission to perform this action', vnd=True):
assert response is not None
assert response.status_code == status.HTTP_403_FORBIDDEN, f'status_code {response.status_code} != 403'
response_has_error(response, error, vnd=vnd)
def assert_404(response, error='Not found', pointer=None, vnd=True):
assert response is not None
assert response.status_code == status.HTTP_404_NOT_FOUND, f'status_code {response.status_code} != 404'
response_has_error(response, error, pointer, vnd)
def assert_405(response, error=None, pointer=None, vnd=True):
if error is None:
error = f'Method "{response.renderer_context["request"].method}" not allowed.'
assert response is not None
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED, f'status_code {response.status_code} != 405'
response_has_error(response, error, pointer, vnd)
def assert_500(response, error='A server error occurred.', pointer=None):
assert response is not None
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR, f'status_code {response.status_code} != 500'
response_has_error(response, error, pointer)
def assert_503(response, error='Service temporarily unavailable, try again later', pointer=None):
assert response is not None
assert response.status_code == status.HTTP_503_SERVICE_UNAVAILABLE, f'status_code {response.status_code} != 503'
response_has_error(response, error, pointer)
class AssertionHelper:
EntityRef = EntityReferenceClass
HTTP_200 = assert_200
HTTP_201 = assert_201
HTTP_202 = assert_202
HTTP_204 = assert_204
HTTP_400 = assert_400
HTTP_401 = assert_401
HTTP_402 = assert_402
HTTP_403 = assert_403
HTTP_404 = assert_404
HTTP_405 = assert_405
HTTP_500 = assert_500
HTTP_503 = assert_503