diff --git a/doc/changelog.rst b/doc/changelog.rst index e08ea62..ee1df0e 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -7,6 +7,7 @@ Changelog Added ^^^^^ - Allow ``Path`` objects in Pydantic validation methods. +- :meth:`SCIMException.from_error ` to create an exception from a SCIM :class:`~scim2_models.Error` object. [0.6.0] - 2026-01-25 -------------------- diff --git a/scim2_models/exceptions.py b/scim2_models/exceptions.py index 7f9d9da..89eebea 100644 --- a/scim2_models/exceptions.py +++ b/scim2_models/exceptions.py @@ -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. @@ -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, +} diff --git a/scim2_models/resources/user.py b/scim2_models/resources/user.py index f43af52..1228662 100644 --- a/scim2_models/resources/user.py +++ b/scim2_models/resources/user.py @@ -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): diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 5c5080a..4f5b734 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -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")