Skip to content
Draft
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
16 changes: 13 additions & 3 deletions scim2_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
from scim2_models import Schema
from scim2_models import SearchRequest
from scim2_models import ServiceProviderConfig
from scim2_models import get_model_by_payload

from scim2_client.errors import InvalidCursorError
from scim2_client.errors import RequestPayloadValidationError
from scim2_client.errors import ResponsePayloadValidationError
from scim2_client.errors import SCIMClientError
Expand Down Expand Up @@ -326,7 +328,7 @@ def check_response(
if response_payload is None:
return None

actual_type = Resource.get_by_payload(
actual_type = get_model_by_payload(
expected_types, response_payload, with_extensions=False
)

Expand All @@ -345,6 +347,14 @@ def check_response(
try:
return actual_type.model_validate(response_payload, scim_ctx=scim_ctx)
except ValidationError as exc:
cursor_errors = [
e for e in exc.errors() if e["type"] == "scim_invalidCursor"
]
if cursor_errors:
scim_exc = InvalidCursorError()
if sys.version_info >= (3, 11): # pragma: no cover
scim_exc.add_note(str(exc))
raise scim_exc from exc
scim_exc = ResponsePayloadValidationError()
if sys.version_info >= (3, 11): # pragma: no cover
scim_exc.add_note(str(exc))
Expand Down Expand Up @@ -373,7 +383,7 @@ def _prepare_create_request(
resource_model = resource.__class__

else:
resource_model = Resource.get_by_payload(self.resource_models, resource)
resource_model = get_model_by_payload(self.resource_models, resource)
if not resource_model:
raise SCIMRequestError(
"Cannot guess resource type from the payload"
Expand Down Expand Up @@ -554,7 +564,7 @@ def _prepare_replace_request(
resource_model = resource.__class__

else:
resource_model = Resource.get_by_payload(self.resource_models, resource)
resource_model = get_model_by_payload(self.resource_models, resource)
if not resource_model:
raise SCIMRequestError(
"Cannot guess resource type from the payload",
Expand Down
27 changes: 27 additions & 0 deletions scim2_client/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,33 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(message, *args, **kwargs)


class InvalidCursorError(SCIMRequestError):
"""Error raised when an invalid cursor has been passed to SCIMClient."""

def __init__(self, *args: Any, **kwargs: Any) -> None:
message = kwargs.pop(
"message",
"Cursor value is invalid.",
)
super().__init__(message, *args, **kwargs)


class InvalidCountError(SCIMRequestError):
"""Count value is invalid."""

def __init__(self, *args: Any, **kwargs: Any) -> None:
message = kwargs.pop("message", "Invalid count")
super().__init__(message, *args, **kwargs)


class ExpiredCursorError(SCIMRequestError):
"""Cursor has expired."""

def __init__(self, *args: Any, **kwargs: Any) -> None:
message = kwargs.pop("message", "Expired cursor")
super().__init__(message, *args, **kwargs)


class SCIMResponseError(SCIMClientError):
"""Base exception for errors happening during response payload validation."""

Expand Down
49 changes: 49 additions & 0 deletions tests/test_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from scim2_client.errors import ExpiredCursorError
from scim2_client.errors import InvalidCountError
from scim2_client.errors import InvalidCursorError
from scim2_client.errors import SCIMRequestError


def test_invalid_cursor_error_default_message():
exc = InvalidCursorError()
assert exc.message == "Cursor value is invalid."
assert str(exc) == "Cursor value is invalid."
assert isinstance(exc, SCIMRequestError)


def test_invalid_cursor_error_custom_message():
exc = InvalidCursorError(message="custom cursor issue")
assert exc.message == "custom cursor issue"
assert str(exc) == "custom cursor issue"


def test_invalid_count_error_default_message():
exc = InvalidCountError()
assert exc.message == "Invalid count"
assert str(exc) == "Invalid count"
assert isinstance(exc, SCIMRequestError)


def test_invalid_count_error_custom_message():
exc = InvalidCountError(message="custom count issue")
assert exc.message == "custom count issue"
assert str(exc) == "custom count issue"


def test_expired_cursor_error_default_message():
exc = ExpiredCursorError()
assert exc.message == "Expired cursor"
assert str(exc) == "Expired cursor"
assert isinstance(exc, SCIMRequestError)


def test_expired_cursor_error_custom_message():
exc = ExpiredCursorError(message="custom expiry issue")
assert exc.message == "custom expiry issue"
assert str(exc) == "custom expiry issue"


def test_cursor_and_count_errors_carry_source():
source = {"cursor": "abc"}
exc = InvalidCursorError(source=source)
assert exc.source is source
25 changes: 25 additions & 0 deletions tests/test_query.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import datetime

import pytest
from scim2_models import Context
from scim2_models import Error
from scim2_models import Group
from scim2_models import ListResponse
Expand All @@ -13,6 +14,7 @@
from scim2_models import User

from scim2_client import SCIMRequestError
from scim2_client.errors import InvalidCursorError
from scim2_client.errors import RequestNetworkError
from scim2_client.errors import ResponsePayloadValidationError
from scim2_client.errors import SCIMClientError
Expand Down Expand Up @@ -322,6 +324,29 @@ def test_user_with_invalid_id(sync_client):
response = sync_client.query(User, "unknown", raise_scim_errors=False)
assert response == Error(detail="Resource unknown not found", status=404)

def test_cursor_errors(sync_client):
"""Test that a nextCursor with reserved characters raises InvalidCursorError."""
payload = {
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
"totalResults": 1,
"nextCursor": "invalid%cursor",
"Resources": [
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "2819c223-7f76-453a-919d-413861904646",
"userName": "bjensen@example.com",
}
],
}
with pytest.raises(InvalidCursorError, match="Cursor value is invalid."):
sync_client.check_response(
payload=payload,
status_code=200,
headers={"content-type": "application/scim+json"},
expected_types=[ListResponse[User]],
scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
)


def test_raise_scim_errors(sync_client):
"""Test that querying an user with an invalid id raises an exception."""
Expand Down
Loading