|
3 | 3 | """ |
4 | 4 |
|
5 | 5 | import pytest |
| 6 | +import ujson |
| 7 | +import jsonpatch |
6 | 8 | from datetime import datetime |
7 | 9 |
|
| 10 | +from shelflist.exporters import ItemsToSolr |
| 11 | +from shelflist.search_indexes import ShelflistItemIndex |
| 12 | +from shelflist.serializers import ShelflistItemSerializer |
| 13 | + |
8 | 14 | # FIXTURES AND TEST DATA |
9 | 15 | # Fixtures used in the below tests can be found in ... |
10 | 16 | # django/sierra/base/tests/conftest.py: |
|
13 | 19 | API_ROOT = '/api/v1/' |
14 | 20 |
|
15 | 21 |
|
| 22 | +REDIS_SHELFLIST_PREFIX = ItemsToSolr.redis_shelflist_prefix |
| 23 | + |
| 24 | + |
16 | 25 | # PARAMETERS__* constants contain parametrization data for certain |
17 | 26 | # tests. Each should be a tuple, where the first tuple member is a |
18 | 27 | # header string that describes the parametrization values (such as |
@@ -268,6 +277,125 @@ def _get_shelflist_urls(records): |
268 | 277 | return _get_shelflist_urls |
269 | 278 |
|
270 | 279 |
|
| 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 | + |
271 | 399 | # TESTS |
272 | 400 | # --------------------------------------------------------------------- |
273 | 401 |
|
@@ -388,3 +516,180 @@ def test_shelflistitem_view_orderby(order_by, api_settings, shelflist_solr_env, |
388 | 516 | response = api_client.get(test_url) |
389 | 517 | assert response.status_code == 400 |
390 | 518 | 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