Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Changelog
Added
^^^^^
- Allow ``Path`` objects in Pydantic validation methods.
- :meth:`SCIMException.from_error <scim2_models.SCIMException.from_error>` to create an exception from a SCIM :class:`~scim2_models.Error` object.

[0.6.0] - 2026-01-25
--------------------
Expand Down
29 changes: 29 additions & 0 deletions scim2_models/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,21 @@ def as_pydantic_error(self) -> PydanticCustomError:
{"scim_type": self.scim_type, "status": self.status, **self.context},
)

@classmethod
def from_error(cls, error: "Error") -> "SCIMException":
"""Create an exception from a SCIM Error object.

:param error: The SCIM Error object to convert.
:return: The appropriate SCIMException subclass instance.
"""
from .messages.error import Error

if not isinstance(error, Error):
raise TypeError(f"Expected Error, got {type(error).__name__}")

exception_class = _SCIM_TYPE_TO_EXCEPTION.get(error.scim_type or "", cls)
return exception_class(detail=error.detail)


class InvalidFilterException(SCIMException):
"""The specified filter syntax was invalid.
Expand Down Expand Up @@ -263,3 +278,17 @@ class SensitiveException(SCIMException):
"The specified request cannot be completed, due to the passing of sensitive "
"information in a request URI"
)


_SCIM_TYPE_TO_EXCEPTION: dict[str, type[SCIMException]] = {
"invalidFilter": InvalidFilterException,
"tooMany": TooManyException,
"uniqueness": UniquenessException,
"mutability": MutabilityException,
"invalidSyntax": InvalidSyntaxException,
"invalidPath": InvalidPathException,
"noTarget": NoTargetException,
"invalidValue": InvalidValueException,
"invalidVers": InvalidVersionException,
"sensitive": SensitiveException,
}
2 changes: 1 addition & 1 deletion scim2_models/resources/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ class Type(str, Enum):

primary: bool | None = None
"""A Boolean value indicating the 'primary' or preferred attribute value
for this attribute, e.g., the preferred photo or thumbnail."""
for this attribute, e.g., the preferred address."""


class Entitlement(ComplexAttribute):
Expand Down
110 changes: 110 additions & 0 deletions tests/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,3 +319,113 @@ def test_path_not_found_is_invalid_path():
exc = PathNotFoundException()
assert isinstance(exc, InvalidPathException)
assert exc.scim_type == "invalidPath"


def test_from_error_invalid_filter():
"""from_error() creates InvalidFilterException from Error with scim_type invalidFilter."""
error = Error(status=400, scim_type="invalidFilter", detail="Bad filter")
exc = SCIMException.from_error(error)
assert isinstance(exc, InvalidFilterException)
assert exc.detail == "Bad filter"


def test_from_error_too_many():
"""from_error() creates TooManyException from Error with scim_type tooMany."""
error = Error(status=400, scim_type="tooMany", detail="Too many results")
exc = SCIMException.from_error(error)
assert isinstance(exc, TooManyException)
assert exc.detail == "Too many results"


def test_from_error_uniqueness():
"""from_error() creates UniquenessException from Error with scim_type uniqueness."""
error = Error(status=409, scim_type="uniqueness", detail="Duplicate userName")
exc = SCIMException.from_error(error)
assert isinstance(exc, UniquenessException)
assert exc.detail == "Duplicate userName"


def test_from_error_mutability():
"""from_error() creates MutabilityException from Error with scim_type mutability."""
error = Error(status=400, scim_type="mutability", detail="Cannot modify id")
exc = SCIMException.from_error(error)
assert isinstance(exc, MutabilityException)
assert exc.detail == "Cannot modify id"


def test_from_error_invalid_syntax():
"""from_error() creates InvalidSyntaxException from Error with scim_type invalidSyntax."""
error = Error(status=400, scim_type="invalidSyntax", detail="Malformed JSON")
exc = SCIMException.from_error(error)
assert isinstance(exc, InvalidSyntaxException)
assert exc.detail == "Malformed JSON"


def test_from_error_invalid_path():
"""from_error() creates InvalidPathException from Error with scim_type invalidPath."""
error = Error(status=400, scim_type="invalidPath", detail="Bad path")
exc = SCIMException.from_error(error)
assert isinstance(exc, InvalidPathException)
assert exc.detail == "Bad path"


def test_from_error_no_target():
"""from_error() creates NoTargetException from Error with scim_type noTarget."""
error = Error(status=400, scim_type="noTarget", detail="No match")
exc = SCIMException.from_error(error)
assert isinstance(exc, NoTargetException)
assert exc.detail == "No match"


def test_from_error_invalid_value():
"""from_error() creates InvalidValueException from Error with scim_type invalidValue."""
error = Error(status=400, scim_type="invalidValue", detail="Missing required")
exc = SCIMException.from_error(error)
assert isinstance(exc, InvalidValueException)
assert exc.detail == "Missing required"


def test_from_error_invalid_version():
"""from_error() creates InvalidVersionException from Error with scim_type invalidVers."""
error = Error(status=400, scim_type="invalidVers", detail="Unsupported version")
exc = SCIMException.from_error(error)
assert isinstance(exc, InvalidVersionException)
assert exc.detail == "Unsupported version"


def test_from_error_sensitive():
"""from_error() creates SensitiveException from Error with scim_type sensitive."""
error = Error(status=400, scim_type="sensitive", detail="Sensitive data in URI")
exc = SCIMException.from_error(error)
assert isinstance(exc, SensitiveException)
assert exc.detail == "Sensitive data in URI"


def test_from_error_unknown_scim_type():
"""from_error() creates base SCIMException for unknown scim_type."""
error = Error(status=400, scim_type="unknownType", detail="Unknown error")
exc = SCIMException.from_error(error)
assert type(exc) is SCIMException
assert exc.detail == "Unknown error"


def test_from_error_no_scim_type():
"""from_error() creates base SCIMException when scim_type is None."""
error = Error(status=500, detail="Internal error")
exc = SCIMException.from_error(error)
assert type(exc) is SCIMException
assert exc.detail == "Internal error"


def test_from_error_no_detail():
"""from_error() uses default detail when Error has no detail."""
error = Error(status=400, scim_type="invalidFilter")
exc = SCIMException.from_error(error)
assert isinstance(exc, InvalidFilterException)
assert exc.detail == InvalidFilterException._default_detail


def test_from_error_type_error():
"""from_error() raises TypeError for non-Error input."""
with pytest.raises(TypeError, match="Expected Error"):
SCIMException.from_error("not an error")
Loading