|
6 | 6 | from pydantic import ValidationError |
7 | 7 | from pydantic import field_validator |
8 | 8 |
|
| 9 | +from scim2_models import BulkResponse |
9 | 10 | from scim2_models import Context |
| 11 | +from scim2_models import EnterpriseUser |
10 | 12 | from scim2_models import Error |
11 | 13 | from scim2_models import ExpiredCursorException |
| 14 | +from scim2_models import Group |
12 | 15 | from scim2_models import InvalidCountException |
13 | 16 | from scim2_models import InvalidCursorException |
14 | 17 | from scim2_models import InvalidFilterException |
15 | 18 | from scim2_models import InvalidPathException |
16 | 19 | from scim2_models import InvalidSyntaxException |
17 | 20 | from scim2_models import InvalidValueException |
18 | 21 | from scim2_models import InvalidVersionException |
| 22 | +from scim2_models import ListResponse |
19 | 23 | from scim2_models import MutabilityException |
20 | 24 | from scim2_models import NoTargetException |
| 25 | +from scim2_models import PatchOp |
21 | 26 | from scim2_models import PathNotFoundException |
22 | 27 | from scim2_models import SCIMException |
23 | 28 | from scim2_models import SensitiveException |
@@ -616,3 +621,108 @@ def test_from_error_without_status(): |
616 | 621 | error = Error(scim_type="uniqueness", detail="Duplicate userName") |
617 | 622 | exc = SCIMException.from_error(error) |
618 | 623 | assert exc.status == 409 |
| 624 | + |
| 625 | + |
| 626 | +ENTERPRISE_USER_SCHEMA = "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" |
| 627 | +USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User" |
| 628 | + |
| 629 | + |
| 630 | +def first_error_detail(model, payload, scim_ctx): |
| 631 | + """Return the detail of the first SCIM error a payload raises.""" |
| 632 | + with pytest.raises(ValidationError) as exc_info: |
| 633 | + model.model_validate(payload, scim_ctx=scim_ctx) |
| 634 | + return Error.from_validation_errors(exc_info.value)[0].detail |
| 635 | + |
| 636 | + |
| 637 | +@pytest.mark.parametrize( |
| 638 | + "model,payload,scim_ctx,path", |
| 639 | + [ |
| 640 | + ( |
| 641 | + User, |
| 642 | + {"userName": "bjensen", "name": {"givenName": 3}}, |
| 643 | + Context.RESOURCE_CREATION_REQUEST, |
| 644 | + "name.givenName", |
| 645 | + ), |
| 646 | + ( |
| 647 | + User, |
| 648 | + { |
| 649 | + "userName": "bjensen", |
| 650 | + "emails": [{"value": "a@example.com"}, {"value": 3}], |
| 651 | + }, |
| 652 | + Context.RESOURCE_CREATION_REQUEST, |
| 653 | + "emails[1].value", |
| 654 | + ), |
| 655 | + ( |
| 656 | + User[EnterpriseUser], |
| 657 | + {"userName": "bjensen", ENTERPRISE_USER_SCHEMA: {"manager": {"value": 3}}}, |
| 658 | + Context.RESOURCE_CREATION_REQUEST, |
| 659 | + f"{ENTERPRISE_USER_SCHEMA}:manager.value", |
| 660 | + ), |
| 661 | + ( |
| 662 | + ListResponse[User | Group], |
| 663 | + {"Resources": [{"schemas": [USER_SCHEMA], "id": "1", "userName": 3}]}, |
| 664 | + Context.RESOURCE_QUERY_RESPONSE, |
| 665 | + "Resources[0].userName", |
| 666 | + ), |
| 667 | + ( |
| 668 | + ListResponse[User[EnterpriseUser] | Group], |
| 669 | + { |
| 670 | + "Resources": [ |
| 671 | + { |
| 672 | + "schemas": [USER_SCHEMA, ENTERPRISE_USER_SCHEMA], |
| 673 | + "id": "1", |
| 674 | + "userName": "bjensen", |
| 675 | + ENTERPRISE_USER_SCHEMA: {"employeeNumber": 3}, |
| 676 | + } |
| 677 | + ] |
| 678 | + }, |
| 679 | + Context.RESOURCE_QUERY_RESPONSE, |
| 680 | + f"Resources[0].{ENTERPRISE_USER_SCHEMA}:employeeNumber", |
| 681 | + ), |
| 682 | + ( |
| 683 | + BulkResponse[User], |
| 684 | + { |
| 685 | + "Operations": [ |
| 686 | + { |
| 687 | + "method": "POST", |
| 688 | + "status": 201, |
| 689 | + "location": "https://example.com/v2/Users/1", |
| 690 | + "response": {"schemas": [USER_SCHEMA], "userName": 3}, |
| 691 | + } |
| 692 | + ] |
| 693 | + }, |
| 694 | + Context.BULK_RESPONSE, |
| 695 | + "Operations[0].response.userName", |
| 696 | + ), |
| 697 | + ], |
| 698 | +) |
| 699 | +def test_the_detail_of_an_error_locates_it_with_an_attribute_path( |
| 700 | + model, payload, scim_ctx, path |
| 701 | +): |
| 702 | + """Sub-attributes follow a dot, extension attributes their schema URN and a colon, values their index. |
| 703 | +
|
| 704 | + The members of the unions pydantic tried are not part of the payload, and |
| 705 | + are left out. |
| 706 | + """ |
| 707 | + assert first_error_detail(model, payload, scim_ctx) == ( |
| 708 | + f"Input should be a valid string: {path}" |
| 709 | + ) |
| 710 | + |
| 711 | + |
| 712 | +def test_the_detail_of_an_error_at_the_root_of_a_payload_carries_no_path(): |
| 713 | + """An error on the payload itself has no attribute to point at.""" |
| 714 | + with pytest.raises(ValidationError) as exc_info: |
| 715 | + User.model_validate("not an object", scim_ctx=Context.RESOURCE_CREATION_REQUEST) |
| 716 | + error = Error.from_validation_errors(exc_info.value)[0] |
| 717 | + assert error.detail == "Input should be a valid dictionary or instance of User" |
| 718 | + |
| 719 | + |
| 720 | +@pytest.mark.parametrize("key", ["Operations", "operations", "OPERATIONS"]) |
| 721 | +def test_message_attributes_keep_their_case_insensitive_names(key): |
| 722 | + """The Operations of a message are named as RFC 7644 spells them, and read whatever their case.""" |
| 723 | + patch = PatchOp[User].model_validate( |
| 724 | + {key: [{"op": "replace", "path": "nickName", "value": "Babs"}]}, |
| 725 | + scim_ctx=Context.RESOURCE_PATCH_REQUEST, |
| 726 | + ) |
| 727 | + assert patch.operations[0].value == "Babs" |
| 728 | + assert "Operations" in patch.model_dump(scim_ctx=Context.RESOURCE_PATCH_REQUEST) |
0 commit comments