diff --git a/cuenca/__init__.py b/cuenca/__init__.py index 568f4eb3..49a06c40 100644 --- a/cuenca/__init__.py +++ b/cuenca/__init__.py @@ -26,6 +26,7 @@ 'LoginToken', 'Otp', 'OperatorLogin', + 'OperatorOtpChallenge', 'PasswordReset', 'Platform', 'Questionnaires', @@ -82,6 +83,7 @@ LimitedWallet, LoginToken, OperatorLogin, + OperatorOtpChallenge, Otp, PasswordReset, PhoneVerificationAssociations, diff --git a/cuenca/resources/__init__.py b/cuenca/resources/__init__.py index bbde7b5a..53cd4098 100644 --- a/cuenca/resources/__init__.py +++ b/cuenca/resources/__init__.py @@ -26,6 +26,7 @@ 'LoginToken', 'Otp', 'OperatorLogin', + 'OperatorOtpChallenge', 'PasswordReset', 'Platform', 'PhoneVerificationAssociation', @@ -76,6 +77,7 @@ from .limited_wallets import LimitedWallet from .login_tokens import LoginToken from .operator_logins import OperatorLogin +from .operator_otp_challenges import OperatorOtpChallenge from .otps import Otp from .password_resets import PasswordReset from .phone_verification_associations import PhoneVerificationAssociations @@ -127,6 +129,7 @@ LimitedWallet, LoginToken, OperatorLogin, + OperatorOtpChallenge, PasswordReset, Questionnaires, Saving, diff --git a/cuenca/resources/operator_otp_challenges.py b/cuenca/resources/operator_otp_challenges.py new file mode 100644 index 00000000..d9f3ab31 --- /dev/null +++ b/cuenca/resources/operator_otp_challenges.py @@ -0,0 +1,34 @@ +import datetime as dt +from typing import ClassVar + +from pydantic import ConfigDict + +from ..http import Session, session as global_session +from .base import Creatable + + +class OperatorOtpChallenge(Creatable): + """One-shot email OTP challenge for an operator session.""" + + _resource: ClassVar = 'operator-otp-challenges' + + expires_at: dt.datetime + email_hint: str + + model_config = ConfigDict( + json_schema_extra={ + 'example': { + 'id': 'OCWqY5cvkISJOxHyEKjAKf8w', + 'expires_at': '2026-09-18T22:10:00+00:00', + 'email_hint': 'l***@example.com', + } + } + ) + + @classmethod + def create( + cls, + *, + session: Session = global_session, + ) -> 'OperatorOtpChallenge': + return cls._create(session=session) diff --git a/tests/resources/test_operator_otp_challenges.py b/tests/resources/test_operator_otp_challenges.py new file mode 100644 index 00000000..e9ef8b66 --- /dev/null +++ b/tests/resources/test_operator_otp_challenges.py @@ -0,0 +1,48 @@ +import datetime as dt +from unittest.mock import MagicMock, patch + +import pytest + +from cuenca import OperatorOtpChallenge +from cuenca.http.client import Session + + +@pytest.fixture +def session() -> Session: + configured_session = Session() + configured_session.configure('api_key', 'api_secret', sandbox=True) + return configured_session + + +@patch('cuenca.http.client.requests.Session.request') +def test_operator_otp_challenge_create( + mock_request: MagicMock, + session: Session, +): + mock_request.return_value.ok = True + mock_request.return_value.content = ( + b'{"id":"OCWqY5cvkISJOxHyEKjAKf8w",' + b'"expires_at":"2026-09-18T22:10:00+00:00",' + b'"email_hint":"l***@example.com"}' + ) + + challenge = OperatorOtpChallenge.create(session=session) + + assert challenge.id == 'OCWqY5cvkISJOxHyEKjAKf8w' + assert challenge.expires_at == dt.datetime( + 2026, + 9, + 18, + 22, + 10, + tzinfo=dt.timezone.utc, + ) + assert challenge.email_hint == 'l***@example.com' + + mock_request.assert_called_once() + _, kwargs = mock_request.call_args + assert kwargs['method'] == 'post' + assert kwargs['url'] == ( + 'https://sandbox.cuenca.com/operator-otp-challenges' + ) + assert kwargs['json'] == {}