Skip to content

Commit cd54729

Browse files
committed
Add complete shelflistitem api tests
This finishes up the API tests for the `shelflist` app `shelflistitem` resource: using PUT/PATCH for updating writable resource data, requiring authentication for PUT/PATCH methods, and ensuring resources are always listed in shelflist-manifest order.
1 parent 6b0b790 commit cd54729

2 files changed

Lines changed: 306 additions & 1 deletion

File tree

django/sierra/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -709,7 +709,7 @@ def _simple_sig_auth_credentials(api_user, request_body=''):
709709
request_body))
710710
signature = hasher.hexdigest()
711711
return {
712-
'HTTP_X_USERNAME': 'test',
712+
'HTTP_X_USERNAME': api_user.user.username,
713713
'HTTP_X_TIMESTAMP': timestamp,
714714
'HTTP_AUTHORIZATION': 'Basic {}'.format(signature)
715715
}

django/sierra/shelflist/tests/test_api.py

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@
33
"""
44

55
import pytest
6+
import ujson
7+
import jsonpatch
68
from datetime import datetime
79

10+
from shelflist.exporters import ItemsToSolr
11+
from shelflist.search_indexes import ShelflistItemIndex
12+
from shelflist.serializers import ShelflistItemSerializer
13+
814
# FIXTURES AND TEST DATA
915
# Fixtures used in the below tests can be found in ...
1016
# django/sierra/base/tests/conftest.py:
@@ -13,6 +19,9 @@
1319
API_ROOT = '/api/v1/'
1420

1521

22+
REDIS_SHELFLIST_PREFIX = ItemsToSolr.redis_shelflist_prefix
23+
24+
1625
# PARAMETERS__* constants contain parametrization data for certain
1726
# tests. Each should be a tuple, where the first tuple member is a
1827
# header string that describes the parametrization values (such as
@@ -268,6 +277,125 @@ def _get_shelflist_urls(records):
268277
return _get_shelflist_urls
269278

270279

280+
@pytest.fixture
281+
def assemble_custom_shelflist(assemble_shelflist_test_records):
282+
"""
283+
Pytest fixture. Returns a utility function for creating a custom
284+
shelflist at the given location code. Uses the
285+
`assemble_shelflist_test_records` fixture to add the records to the
286+
active Solr environment for the duration of the test. Returns a
287+
tuple: environment records (erecs), location records (lrecs), and
288+
item test records (trecs).
289+
"""
290+
def _assemble_custom_shelflist(lcode, sl_item_data, id_field='id'):
291+
test_locdata = [(lcode, {})]
292+
test_itemdata = []
293+
for item_id, data in sl_item_data:
294+
new_data = data.copy()
295+
new_data['location_code'] = lcode
296+
test_itemdata.append((item_id, new_data))
297+
_, lrecs = assemble_shelflist_test_records(test_locdata,
298+
id_field='code',
299+
profile='location')
300+
erecs, trecs = assemble_shelflist_test_records(test_itemdata,
301+
id_field=id_field)
302+
return erecs, lrecs, trecs
303+
return _assemble_custom_shelflist
304+
305+
306+
@pytest.fixture
307+
def derive_updated_resource():
308+
"""
309+
Pytest fixture. Returns a helper function that lets you provide a
310+
dict representing an existing resource (`old_item`), the relevant
311+
REST API serializer for that resource (`serializer`), and the
312+
SolrProfile object for that resource (`solr_profile`). Returns an
313+
updated version of that resource (dict), giving all fields listed
314+
in `which_fields` updated values. (Updates all fields by default.)
315+
The dict that's returned can be converted to JSON and submitted
316+
directly as a PUT request to update the resource via the API.
317+
"""
318+
def _get_new_val(old_val, field_type):
319+
if field_type == 'str':
320+
return unicode('{} TEST').format((old_val or ''))
321+
if field_type == 'int':
322+
return (old_val or 0) + 1
323+
if field_type == 'bool':
324+
return not bool(old_val)
325+
if field_type == 'datetime':
326+
return '9999-01-01T00:00:00Z'
327+
return None
328+
329+
def _derive_updated_resource(old_item, serializer, solr_profile,
330+
which_fields=None):
331+
new_item = {}
332+
for fname, fopts in serializer.fields.items():
333+
rendered_fname = serializer.render_field_name(fname)
334+
old_val = old_item[rendered_fname]
335+
solr_fname = fopts.get('source', fname)
336+
if (which_fields is None) or (rendered_fname in which_fields):
337+
field = solr_profile.fields.get(solr_fname, {})
338+
if field.get('multi', False):
339+
old_val = old_val or [None]
340+
new_val = [_get_new_val(o, fopts['type']) for o in old_val]
341+
else:
342+
new_val = _get_new_val(old_val, fopts['type'])
343+
else:
344+
new_val = old_val
345+
new_item[rendered_fname] = new_val
346+
return new_item
347+
return _derive_updated_resource
348+
349+
350+
@pytest.fixture
351+
def filter_serializer_fields_by_opt():
352+
"""
353+
Pytest fixture. Returns a helper function that lets you filter a
354+
list of REST API serializer fields based on the `serializer.fields`
355+
field options. Provide the `serializer`, the field opts `attr` and
356+
field opts attr `value`, and get a list of matching fields.
357+
"""
358+
def _filter_serializer_fields_by_opt(serializer, attr, value):
359+
fields = []
360+
for fname, fopts in serializer.fields.items():
361+
rendered_fname = serializer.render_field_name(fname)
362+
if fopts.get(attr, None) == value:
363+
fields.append(rendered_fname)
364+
return fields
365+
return _filter_serializer_fields_by_opt
366+
367+
368+
@pytest.fixture
369+
def send_api_data(apiuser_with_custom_defaults, simple_sig_auth_credentials):
370+
"""
371+
Pytest fixture. Returns a helper function that sends API data via
372+
the provided `api_client`, to the provided `url`, with the given
373+
`req_body`, via the given HTTP `method`. An API user is created and
374+
used for authentication. You may optionally supply a `content_type`
375+
string; if not supplied, then JSON is assumed by default (or
376+
json-patch for patch requests). The response object is returned.
377+
"""
378+
content_types = {
379+
'put': 'application/json',
380+
'patch': 'application/json-patch+json',
381+
'post': 'application/json'
382+
}
383+
384+
def _send_api_data(api_client, url, req_body, method, content_type=None):
385+
test_cls = apiuser_with_custom_defaults()
386+
api_user = test_cls.objects.create_user('test', 'sec', password='pw',
387+
email='test@test.com',
388+
first_name='F', last_name='L')
389+
content_type = content_type or content_types[method]
390+
api_client.credentials(**simple_sig_auth_credentials(api_user,
391+
req_body))
392+
do_send = getattr(api_client, method)
393+
resp = do_send(url, req_body, content_type=content_type)
394+
api_client.credentials()
395+
return resp
396+
return _send_api_data
397+
398+
271399
# TESTS
272400
# ---------------------------------------------------------------------
273401

@@ -388,3 +516,180 @@ def test_shelflistitem_view_orderby(order_by, api_settings, shelflist_solr_env,
388516
response = api_client.get(test_url)
389517
assert response.status_code == 400
390518
assert 'not a valid field for ordering' in response.data['detail']
519+
520+
521+
def test_shelflistitem_row_order(api_settings, shelflist_solr_env,
522+
get_shelflist_urls, api_client, redis_obj,
523+
get_found_ids):
524+
"""
525+
The `shelflistitems` list view should list items in the same order
526+
that the shelflist manifest for that location lists them. The
527+
`rowNumber` value should be an incremented integer, starting at 0.
528+
"""
529+
recs = shelflist_solr_env.records['shelflistitem']
530+
loc = recs[0]['location_code']
531+
loc_recs = [r for r in recs if r['location_code'] == loc]
532+
index = ShelflistItemIndex()
533+
manifest = index.get_location_manifest(loc)
534+
redis_key = '{}:{}'.format(REDIS_SHELFLIST_PREFIX, loc)
535+
redis_obj(redis_key).set(manifest)
536+
537+
url = get_shelflist_urls(shelflist_solr_env.records['shelflistitem'])[loc]
538+
response = api_client.get(url)
539+
total = response.data['totalCount']
540+
found_ids = get_found_ids('id', response)
541+
row_numbers = get_found_ids('row_number', response)
542+
assert found_ids == manifest
543+
assert row_numbers == [num for num in range(0, total)]
544+
545+
546+
def test_shelflistitem_putpatch_requires_auth(api_settings,
547+
assemble_custom_shelflist,
548+
get_shelflist_urls, api_client):
549+
"""
550+
Saving data (via put or patch) to a shelflistitem resource should
551+
fail without authentication. A 403 status code should be returned,
552+
and the item should NOT be updated.
553+
"""
554+
test_lcode, test_id = '1test', 99999999
555+
_, _, trecs = assemble_custom_shelflist(test_lcode, [(test_id, {})])
556+
url = '{}{}'.format(get_shelflist_urls(trecs)[test_lcode], test_id)
557+
before = api_client.get(url)
558+
put_resp = api_client.put(url, {})
559+
patch_resp = api_client.patch(url, {})
560+
after = api_client.get(url)
561+
assert put_resp.status_code == 403
562+
assert patch_resp.status_code == 403
563+
assert before.data == after.data
564+
565+
566+
@pytest.mark.django_db
567+
@pytest.mark.parametrize('method', ['put', 'patch'])
568+
def test_shelflistitem_update_err_nonwritable(method, api_settings,
569+
assemble_custom_shelflist,
570+
shelflist_solr_env,
571+
filter_serializer_fields_by_opt,
572+
derive_updated_resource,
573+
send_api_data,
574+
get_shelflist_urls, api_client):
575+
"""
576+
Attempting to update nonwritable fields raises an error,
577+
'... is not a writable field'. The item should NOT be updated.
578+
"""
579+
test_lcode, test_id = '1test', 99999999
580+
_, _, trecs = assemble_custom_shelflist(test_lcode, [(test_id, {})])
581+
url = '{}{}'.format(get_shelflist_urls(trecs)[test_lcode], test_id)
582+
before = api_client.get(url)
583+
serializer = before.renderer_context['view'].get_serializer()
584+
profile = shelflist_solr_env.profiles['shelflistitem']
585+
try_item = derive_updated_resource(before.data, serializer, profile)
586+
587+
if method == 'put':
588+
req_body = ujson.dumps(try_item)
589+
elif method == 'patch':
590+
req_body = jsonpatch.make_patch(before.data, try_item)
591+
592+
unwritable = filter_serializer_fields_by_opt(serializer, 'writable', False)
593+
resp = send_api_data(api_client, url, req_body, method)
594+
after = api_client.get(url)
595+
596+
assert resp.status_code == 400
597+
assert before.data == after.data
598+
for fname in unwritable:
599+
msg = '{} is not a writable field'.format(fname)
600+
assert msg in resp.data['detail']
601+
602+
603+
@pytest.mark.django_db
604+
@pytest.mark.parametrize('method', ['put', 'patch'])
605+
def test_shelflistitem_update_items(method, api_settings,
606+
assemble_custom_shelflist,
607+
shelflist_solr_env,
608+
filter_serializer_fields_by_opt,
609+
derive_updated_resource, send_api_data,
610+
get_shelflist_urls, api_client):
611+
"""
612+
Updating writable fields on shelflistitems should update/save the
613+
resource: it should update the writable fields that were changed
614+
and keep all other fields exactly the same.
615+
"""
616+
test_lcode, test_id = '1test', 99999999
617+
_, _, trecs = assemble_custom_shelflist(test_lcode, [(test_id, {})])
618+
url = '{}{}'.format(get_shelflist_urls(trecs)[test_lcode], test_id)
619+
before = api_client.get(url)
620+
serializer = before.renderer_context['view'].get_serializer()
621+
writable = filter_serializer_fields_by_opt(serializer, 'writable', True)
622+
unwritable = filter_serializer_fields_by_opt(serializer, 'writable', False)
623+
profile = shelflist_solr_env.profiles['shelflistitem']
624+
try_item = derive_updated_resource(before.data, serializer, profile,
625+
which_fields=writable)
626+
627+
if method == 'put':
628+
req_body = ujson.dumps(try_item)
629+
elif method == 'patch':
630+
req_body = jsonpatch.make_patch(before.data, try_item)
631+
632+
resp = send_api_data(api_client, url, req_body, method)
633+
after = api_client.get(url)
634+
635+
assert resp.status_code == 200
636+
assert resp.data['links']['self']['href'].endswith(url)
637+
assert resp.data['links']['self']['id'] == test_id
638+
639+
print(before.data)
640+
print(try_item)
641+
print(after.data)
642+
643+
for fname in writable:
644+
assert after.data[fname] == try_item[fname]
645+
assert after.data[fname] != before.data[fname]
646+
647+
for fname in unwritable:
648+
assert after.data[fname] == try_item[fname]
649+
assert after.data[fname] == before.data[fname]
650+
651+
652+
@pytest.mark.django_db
653+
@pytest.mark.parametrize('fname_solr, fname_api, start_val, expect_error', [
654+
('barcode', 'barcode', '9876543210', True),
655+
('shelf_status', 'shelfStatus', 'onShelf', False),
656+
])
657+
def test_shelflistitem_put_data_missing_fields(fname_solr, fname_api,
658+
start_val, expect_error,
659+
api_settings,
660+
assemble_custom_shelflist,
661+
shelflist_solr_env,
662+
filter_serializer_fields_by_opt,
663+
derive_updated_resource,
664+
send_api_data,
665+
get_shelflist_urls, api_client):
666+
"""
667+
A PUT request should replace the item being updated with the item
668+
in the request body. In other words: if a field isn't provided in
669+
the PUT request body, the field is set to None/null. This results
670+
in an error if the field previously had data and is not writable.
671+
"""
672+
test_lcode, test_id = '1test', 99999999
673+
test_data = [(test_id, {fname_solr: start_val})]
674+
_, _, trecs = assemble_custom_shelflist(test_lcode, test_data)
675+
url = '{}{}'.format(get_shelflist_urls(trecs)[test_lcode], test_id)
676+
before = api_client.get(url)
677+
serializer = before.renderer_context['view'].get_serializer()
678+
profile = shelflist_solr_env.profiles['shelflistitem']
679+
writable = filter_serializer_fields_by_opt(serializer, 'writable', True)
680+
try_item = derive_updated_resource(before.data, serializer, profile,
681+
which_fields=writable)
682+
del(try_item[fname_api])
683+
req_body = ujson.dumps(try_item)
684+
resp = send_api_data(api_client, url, req_body, 'put')
685+
after = api_client.get(url)
686+
687+
if expect_error:
688+
assert resp.status_code == 400
689+
assert before.data == after.data
690+
msg = '{} is not a writable field'.format(fname_api)
691+
assert msg in resp.data['detail']
692+
else:
693+
assert resp.status_code == 200
694+
assert before.data[fname_api] == start_val
695+
assert after.data[fname_api] is None

0 commit comments

Comments
 (0)