From ed9cd1a1924ea268c5d3c4fd848e8b706a8c7f25 Mon Sep 17 00:00:00 2001 From: Juliette Trejo Ceballos Date: Fri, 25 Sep 2026 11:58:31 -0600 Subject: [PATCH 1/7] Add OperatorOtp resource for generating email OTPs in operator portal Implements POST /operator-otps endpoint mapping for cuenca-python SDK. Allows operators to generate additional OTP challenges for sensitive actions after completing OperatorLogin authentication. - New OperatorOtp class with create() method - Follows existing cuenca-python patterns (Creatable mixin) - Requires active operator Session (X-Cuenca-SessionId header) - Complete test coverage (100%) - Exported in public API Co-Authored-By: Claude Haiku 4.5 --- cuenca/__init__.py | 2 + cuenca/resources/__init__.py | 3 ++ cuenca/resources/operator_otps.py | 45 ++++++++++++++++++++++ tests/resources/test_operator_otps.py | 54 +++++++++++++++++++++++++++ 4 files changed, 104 insertions(+) create mode 100644 cuenca/resources/operator_otps.py create mode 100644 tests/resources/test_operator_otps.py diff --git a/cuenca/__init__.py b/cuenca/__init__.py index 568f4eb3..ee8d209d 100644 --- a/cuenca/__init__.py +++ b/cuenca/__init__.py @@ -26,6 +26,7 @@ 'LoginToken', 'Otp', 'OperatorLogin', + 'OperatorOtp', 'PasswordReset', 'Platform', 'Questionnaires', @@ -82,6 +83,7 @@ LimitedWallet, LoginToken, OperatorLogin, + OperatorOtp, Otp, PasswordReset, PhoneVerificationAssociations, diff --git a/cuenca/resources/__init__.py b/cuenca/resources/__init__.py index bbde7b5a..f85b677c 100644 --- a/cuenca/resources/__init__.py +++ b/cuenca/resources/__init__.py @@ -26,6 +26,7 @@ 'LoginToken', 'Otp', 'OperatorLogin', + 'OperatorOtp', 'PasswordReset', 'Platform', 'PhoneVerificationAssociation', @@ -76,6 +77,7 @@ from .limited_wallets import LimitedWallet from .login_tokens import LoginToken from .operator_logins import OperatorLogin +from .operator_otps import OperatorOtp from .otps import Otp from .password_resets import PasswordReset from .phone_verification_associations import PhoneVerificationAssociations @@ -127,6 +129,7 @@ LimitedWallet, LoginToken, OperatorLogin, + OperatorOtp, PasswordReset, Questionnaires, Saving, diff --git a/cuenca/resources/operator_otps.py b/cuenca/resources/operator_otps.py new file mode 100644 index 00000000..335c8693 --- /dev/null +++ b/cuenca/resources/operator_otps.py @@ -0,0 +1,45 @@ +import datetime as dt +from typing import Annotated, ClassVar + +from cuenca_validations.types import LogConfig +from pydantic import ConfigDict + +from ..http import Session, session as global_session +from .base import Creatable + + +class OperatorOtp(Creatable): + """Email OTP challenge for authorized actions in operator portal. + + Requires an active operator Session (from completed OperatorLogin). + ``POST /operator-otps`` creates an email OTP challenge and emails + a 6-digit code to the operator. Use the code with the ``X-Cuenca-OTP`` + header in protected endpoints. + + Expires in 5 minutes. + """ + + _resource: ClassVar = 'operator-otps' + + id: Annotated[str, LogConfig(masked=True, unmasked_chars_length=4)] + expires_at: dt.datetime + + model_config = ConfigDict( + json_schema_extra={ + 'example': { + 'id': 'OCWqY5cvkISJOxHyEKjAKf8w', + 'expires_at': '2026-09-25T12:34:56Z', + } + } + ) + + @classmethod + def create(cls, *, session: Session = global_session) -> 'OperatorOtp': + """Create an email OTP challenge for the current operator Session. + + Requires: X-Cuenca-SessionId header set (from OperatorLogin.update). + A 6-digit code is sent via email to the operator's registered address. + + Returns: OperatorOtp with challenge id and expiration timestamp. + """ + return cls._create(session=session) diff --git a/tests/resources/test_operator_otps.py b/tests/resources/test_operator_otps.py new file mode 100644 index 00000000..f2682d03 --- /dev/null +++ b/tests/resources/test_operator_otps.py @@ -0,0 +1,54 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from cuenca import OperatorOtp +from cuenca.http.client import Session + + +@pytest.fixture +def session() -> Session: + s = Session() + s.configure('api_key', 'api_secret', sandbox=True) + return s + + +@patch('cuenca.http.client.requests.Session.request') +def test_operator_otp_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-25T12:34:56"}' + ) + + otp = OperatorOtp.create(session=session) + + assert otp.id == 'OCWqY5cvkISJOxHyEKjAKf8w' + assert otp.expires_at is not None + + mock_request.assert_called_once() + _, kwargs = mock_request.call_args + assert kwargs['method'] == 'post' + assert kwargs['url'] == 'https://sandbox.cuenca.com/operator-otps' + assert kwargs['json'] == {} + + +@patch('cuenca.http.client.requests.Session.request') +def test_operator_otp_create_with_active_session( + mock_request: MagicMock, session: Session +): + """Verify that create() works when called with an active session.""" + mock_request.return_value.ok = True + mock_request.return_value.content = ( + b'{"id":"OCWqY5cvkISJOxHyEKjAKf8w",' + b'"expires_at":"2026-09-25T12:34:56"}' + ) + + # Set session header (normally done by OperatorLogin.update/complete) + session.headers['X-Cuenca-SessionId'] = 'SEWqY5cvkISJOxHyEKjAKf8w' + + otp = OperatorOtp.create(session=session) + + assert otp.id == 'OCWqY5cvkISJOxHyEKjAKf8w' + # Verify session header is preserved + assert session.headers['X-Cuenca-SessionId'] == 'SEWqY5cvkISJOxHyEKjAKf8w' From d8dc3f56a037c5ef6e7d3ab58d71d92891d94a36 Mon Sep 17 00:00:00 2001 From: Juliette Trejo Ceballos Date: Fri, 25 Sep 2026 14:32:01 -0600 Subject: [PATCH 2/7] Update cuenca-validations to version 2.1.48 and enhance OperatorLogin resource --- cuenca/resources/operator_logins.py | 58 ++++++++++++++++---- requirements.txt | 2 +- setup.py | 2 +- tests/resources/test_operator_logins.py | 73 ++++++++++++++++++++++--- 4 files changed, 112 insertions(+), 23 deletions(-) diff --git a/cuenca/resources/operator_logins.py b/cuenca/resources/operator_logins.py index ed0b7c9d..42f7d5bc 100644 --- a/cuenca/resources/operator_logins.py +++ b/cuenca/resources/operator_logins.py @@ -1,7 +1,11 @@ -from typing import Annotated, ClassVar +import datetime as dt +from typing import Annotated, ClassVar, Optional from cuenca_validations.types import LogConfig, OperatorRole -from cuenca_validations.types.requests import OperatorLoginRequest +from cuenca_validations.types.requests import ( + OperatorLoginRequest, + OperatorLoginUpdateRequest, +) from pydantic import ConfigDict from ..http import Session, session as global_session @@ -9,26 +13,33 @@ class OperatorLogin(Creatable): - """Authenticate a company operator (portal personas morales). + """Two-step operator portal login (Cuenca Empresas). - Authed endpoint: ``POST /operator-login``. Response ``id`` is the - Session.id; use it as ``X-Cuenca-SessionId`` on subsequent requests. + ``POST /operator-logins`` validates email/password and emails an OTP. + ``PATCH /operator-logins/{id}`` verifies the OTP and returns a Session. + + Both routes are auth_exempt. Set ``X-Cuenca-SessionId`` only after + ``update`` / ``complete`` (response ``id`` is ``SE*``). """ - _resource: ClassVar = 'operator-login' + _resource: ClassVar = 'operator-logins' id: Annotated[str, LogConfig(masked=True, unmasked_chars_length=4)] operator_id: str - role: OperatorRole - legal_person_id: str + # Present after create (OTP challenge) + expires_at: Optional[dt.datetime] = None + email_hint: Optional[str] = None + # Present after update/complete (session) + role: Optional[OperatorRole] = None + legal_person_id: Optional[str] = None model_config = ConfigDict( json_schema_extra={ 'example': { - 'id': 'SEWqY5cvkISJOxHyEKjAKf8w', + 'id': 'OLWqY5cvkISJOxHyEKjAKf8w', 'operator_id': 'OPWqY5cvkISJOxHyEKjAKf8w', - 'role': 'authorizer', - 'legal_person_id': 'USWqY5cvkISJOxHyEKjAKf8w', + 'expires_at': '2026-09-21T20:15:22Z', + 'email_hint': 'ma****@aceros.com', } } ) @@ -42,10 +53,33 @@ def create( session: Session = global_session, ) -> 'OperatorLogin': req = OperatorLoginRequest(email=email, password=password) - login = cls._create( + return cls._create( session=session, email=str(req.email), password=req.password.get_secret_value(), ) + + @classmethod + def update( + cls, + login_id: str, + code: str, + *, + session: Session = global_session, + ) -> 'OperatorLogin': + req = OperatorLoginUpdateRequest(code=code) + resp = session.patch(f'/{cls._resource}/{login_id}', req.model_dump()) + login = cls(**resp) session.headers['X-Cuenca-SessionId'] = login.id return login + + @classmethod + def complete( + cls, + login_id: str, + code: str, + *, + session: Session = global_session, + ) -> 'OperatorLogin': + """Alias of ``update`` — verify OTP and create the Session.""" + return cls.update(login_id, code, session=session) diff --git a/requirements.txt b/requirements.txt index a1e0e0ed..e5b97f35 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ requests==2.32.3 -cuenca-validations==2.1.46 +cuenca-validations==2.1.48 pydantic-extra-types==2.10.2 diff --git a/setup.py b/setup.py index 7a36412a..ca783317 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ python_requires='>=3.9', install_requires=[ 'requests>=2.32.0', - 'cuenca-validations>=2.1.46', + 'cuenca-validations>=2.1.48', 'pydantic-extra-types>=2.10.0', ], classifiers=[ diff --git a/tests/resources/test_operator_logins.py b/tests/resources/test_operator_logins.py index 02d0353c..0d5cc0bb 100644 --- a/tests/resources/test_operator_logins.py +++ b/tests/resources/test_operator_logins.py @@ -18,10 +18,10 @@ def session() -> Session: def test_operator_login_create(mock_request: MagicMock, session: Session): mock_request.return_value.ok = True mock_request.return_value.content = ( - b'{"id":"SEWqY5cvkISJOxHyEKjAKf8w",' + b'{"id":"OLWqY5cvkISJOxHyEKjAKf8w",' b'"operator_id":"OPWqY5cvkISJOxHyEKjAKf8w",' - b'"role":"operator",' - b'"legal_person_id":"USWqY5cvkISJOxHyEKjAKf8w"}' + b'"expires_at":"2026-09-21T20:15:22",' + b'"email_hint":"ma****@aceros.com"}' ) login = OperatorLogin.create( @@ -30,18 +30,73 @@ def test_operator_login_create(mock_request: MagicMock, session: Session): session=session, ) - assert login.id == 'SEWqY5cvkISJOxHyEKjAKf8w' + assert login.id == 'OLWqY5cvkISJOxHyEKjAKf8w' assert login.operator_id == 'OPWqY5cvkISJOxHyEKjAKf8w' - assert login.role == OperatorRole.operator - assert login.legal_person_id == 'USWqY5cvkISJOxHyEKjAKf8w' - assert session.headers['X-Cuenca-SessionId'] == login.id - assert 'X-Cuenca-LoginId' not in session.headers + assert login.email_hint == 'ma****@aceros.com' + assert login.expires_at is not None + assert login.role is None + assert login.legal_person_id is None + assert 'X-Cuenca-SessionId' not in session.headers mock_request.assert_called_once() _, kwargs = mock_request.call_args assert kwargs['method'] == 'post' - assert kwargs['url'] == 'https://sandbox.cuenca.com/operator-login' + assert kwargs['url'] == 'https://sandbox.cuenca.com/operator-logins' assert kwargs['json'] == { 'email': 'maria@aceros.com', 'password': 'supersecret', } + + +@patch('cuenca.http.client.requests.Session.request') +def test_operator_login_update(mock_request: MagicMock, session: Session): + mock_request.return_value.ok = True + mock_request.return_value.content = ( + b'{"id":"SEWqY5cvkISJOxHyEKjAKf8w",' + b'"operator_id":"OPWqY5cvkISJOxHyEKjAKf8w",' + b'"role":"operator",' + b'"legal_person_id":"USWqY5cvkISJOxHyEKjAKf8w"}' + ) + + login = OperatorLogin.update( + 'OLWqY5cvkISJOxHyEKjAKf8w', + '123456', + session=session, + ) + + assert login.id == 'SEWqY5cvkISJOxHyEKjAKf8w' + assert login.operator_id == 'OPWqY5cvkISJOxHyEKjAKf8w' + assert login.role == OperatorRole.operator + assert login.legal_person_id == 'USWqY5cvkISJOxHyEKjAKf8w' + assert session.headers['X-Cuenca-SessionId'] == login.id + assert 'X-Cuenca-LoginId' not in session.headers + + _, kwargs = mock_request.call_args + assert kwargs['method'] == 'patch' + assert kwargs['url'] == ( + 'https://sandbox.cuenca.com/operator-logins/' + 'OLWqY5cvkISJOxHyEKjAKf8w' + ) + assert kwargs['json'] == {'code': '123456'} + + +@patch('cuenca.http.client.requests.Session.request') +def test_operator_login_complete_alias( + mock_request: MagicMock, session: Session +): + mock_request.return_value.ok = True + mock_request.return_value.content = ( + b'{"id":"SEWqY5cvkISJOxHyEKjAKf8w",' + b'"operator_id":"OPWqY5cvkISJOxHyEKjAKf8w",' + b'"role":"authorizer",' + b'"legal_person_id":"USWqY5cvkISJOxHyEKjAKf8w"}' + ) + + login = OperatorLogin.complete( + 'OLWqY5cvkISJOxHyEKjAKf8w', + '654321', + session=session, + ) + + assert login.role == OperatorRole.authorizer + assert session.headers['X-Cuenca-SessionId'] == login.id From 6b87f5f5d7526dadf8b61750bb96c795a545ceb6 Mon Sep 17 00:00:00 2001 From: Juliette Trejo Ceballos Date: Fri, 25 Sep 2026 15:02:46 -0600 Subject: [PATCH 3/7] Add OperatorLoginResponse and OperatorLoginSessionResponse models to enhance operator login functionality --- cuenca/resources/operator_logins.py | 48 ++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/cuenca/resources/operator_logins.py b/cuenca/resources/operator_logins.py index 42f7d5bc..94bc2063 100644 --- a/cuenca/resources/operator_logins.py +++ b/cuenca/resources/operator_logins.py @@ -6,12 +6,58 @@ OperatorLoginRequest, OperatorLoginUpdateRequest, ) -from pydantic import ConfigDict +from pydantic import BaseModel, ConfigDict from ..http import Session, session as global_session from .base import Creatable +class OperatorLoginResponse(BaseModel): + """POST /operator-logins response (OTP challenge pending). + + Requires cuenca-validations >= 2.1.48. + """ + + id: str + operator_id: str + expires_at: dt.datetime + email_hint: str + + model_config = ConfigDict( + json_schema_extra={ + 'example': { + 'id': 'OLWqY5cvkISJOxHyEKjAKf8w', + 'operator_id': 'OPWqY5cvkISJOxHyEKjAKf8w', + 'expires_at': '2026-09-21T20:15:22Z', + 'email_hint': 'ma****@aceros.com', + } + }, + ) + + +class OperatorLoginSessionResponse(BaseModel): + """PATCH /operator-logins/{id} response (session after OTP). + + Requires cuenca-validations >= 2.1.48. + """ + + id: str + operator_id: str + role: OperatorRole + legal_person_id: str + + model_config = ConfigDict( + json_schema_extra={ + 'example': { + 'id': 'SEWqY5cvkISJOxHyEKjAKf8w', + 'operator_id': 'OPWqY5cvkISJOxHyEKjAKf8w', + 'role': 'authorizer', + 'legal_person_id': 'USWqY5cvkISJOxHyEKjAKf8w', + } + }, + ) + + class OperatorLogin(Creatable): """Two-step operator portal login (Cuenca Empresas). From 211509052693148cbb72aa6d5d0b53b12da95296 Mon Sep 17 00:00:00 2001 From: Juliette Trejo Ceballos Date: Mon, 28 Sep 2026 17:14:45 -0600 Subject: [PATCH 4/7] Update cuenca-validations dependency to 2.1.47 Align with PR cuenca-validations#429 that bumps to 2.1.47. Co-Authored-By: Claude Haiku 4.5 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e5b97f35..4ff1053f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ requests==2.32.3 -cuenca-validations==2.1.48 +cuenca-validations==2.1.47 pydantic-extra-types==2.10.2 From 93219b582e8fc2e0d72621ffa3f386bed9334a7d Mon Sep 17 00:00:00 2001 From: Juliette Trejo Ceballos Date: Mon, 28 Sep 2026 17:18:59 -0600 Subject: [PATCH 5/7] Update cuenca-validations dependency to 2.1.47 in setup.py Co-Authored-By: Claude Haiku 4.5 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ca783317..e608be18 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ python_requires='>=3.9', install_requires=[ 'requests>=2.32.0', - 'cuenca-validations>=2.1.48', + 'cuenca-validations>=2.1.47', 'pydantic-extra-types>=2.10.0', ], classifiers=[ From b1ad3fd47eedd2962d6c063104219b7963fe88a3 Mon Sep 17 00:00:00 2001 From: Juliette Trejo Ceballos Date: Thu, 1 Oct 2026 16:08:33 -0600 Subject: [PATCH 6/7] Update cuenca-validations to 2.1.48.dev2 and add OperatorToken resource --- cuenca/__init__.py | 2 + cuenca/resources/__init__.py | 3 + cuenca/resources/operator_tokens.py | 61 ++++++++++++++++++ requirements.txt | 2 +- setup.py | 2 +- tests/resources/test_operator_tokens.py | 82 +++++++++++++++++++++++++ 6 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 cuenca/resources/operator_tokens.py create mode 100644 tests/resources/test_operator_tokens.py diff --git a/cuenca/__init__.py b/cuenca/__init__.py index ee8d209d..9ccb40fd 100644 --- a/cuenca/__init__.py +++ b/cuenca/__init__.py @@ -27,6 +27,7 @@ 'Otp', 'OperatorLogin', 'OperatorOtp', + 'OperatorToken', 'PasswordReset', 'Platform', 'Questionnaires', @@ -84,6 +85,7 @@ LoginToken, OperatorLogin, OperatorOtp, + OperatorToken, Otp, PasswordReset, PhoneVerificationAssociations, diff --git a/cuenca/resources/__init__.py b/cuenca/resources/__init__.py index f85b677c..1980046e 100644 --- a/cuenca/resources/__init__.py +++ b/cuenca/resources/__init__.py @@ -27,6 +27,7 @@ 'Otp', 'OperatorLogin', 'OperatorOtp', + 'OperatorToken', 'PasswordReset', 'Platform', 'PhoneVerificationAssociation', @@ -78,6 +79,7 @@ from .login_tokens import LoginToken from .operator_logins import OperatorLogin from .operator_otps import OperatorOtp +from .operator_tokens import OperatorToken from .otps import Otp from .password_resets import PasswordReset from .phone_verification_associations import PhoneVerificationAssociations @@ -130,6 +132,7 @@ LoginToken, OperatorLogin, OperatorOtp, + OperatorToken, PasswordReset, Questionnaires, Saving, diff --git a/cuenca/resources/operator_tokens.py b/cuenca/resources/operator_tokens.py new file mode 100644 index 00000000..e1e68352 --- /dev/null +++ b/cuenca/resources/operator_tokens.py @@ -0,0 +1,61 @@ +import datetime as dt +from typing import Annotated, ClassVar, Optional, Union + +from cuenca_validations.types import LogConfig, OperatorRole +from pydantic import ConfigDict + +from ..http import Session, session as global_session +from .base import Creatable + + +class OperatorToken(Creatable): + """One-time invitation token to set an operator password. + + ``POST /operator_tokens`` creates the invite in Authed and returns the + plaintext ``token`` once (for the magic-link email). Does not send email. + """ + + _resource: ClassVar = 'operator_tokens' + + id: str + operator_id: str + email: str + legal_person_id: str + role: Union[OperatorRole, str] + token: Annotated[str, LogConfig(masked=True, unmasked_chars_length=4)] + created_at: Optional[dt.datetime] = None + expires_at: Optional[dt.datetime] = None + used_at: Optional[dt.datetime] = None + + model_config = ConfigDict( + json_schema_extra={ + 'example': { + 'id': 'OTWqY5cvkISJOxHyEKjAKf8w', + 'operator_id': 'OPWqY5cvkISJOxHyEKjAKf8w', + 'email': 'maria.lopez@aceros.com', + 'legal_person_id': 'USWqY5cvkISJOxHyEKjAKf8w', + 'role': 'operator', + 'token': 'plaintext-invite-secret', + 'expires_at': '2026-09-24T21:30:56Z', + } + } + ) + + @classmethod + def create( + cls, + operator_id: str, + email: str, + legal_person_id: str, + role: Union[OperatorRole, str], + *, + session: Session = global_session, + ) -> 'OperatorToken': + role_value = role.value if isinstance(role, OperatorRole) else role + return cls._create( + session=session, + operator_id=operator_id, + email=email, + legal_person_id=legal_person_id, + role=role_value, + ) diff --git a/requirements.txt b/requirements.txt index 4ff1053f..e33efdc3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ requests==2.32.3 -cuenca-validations==2.1.47 +cuenca-validations==2.1.48.dev2 pydantic-extra-types==2.10.2 diff --git a/setup.py b/setup.py index e608be18..e2137621 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ python_requires='>=3.9', install_requires=[ 'requests>=2.32.0', - 'cuenca-validations>=2.1.47', + 'cuenca-validations==2.1.48.dev2', 'pydantic-extra-types>=2.10.0', ], classifiers=[ diff --git a/tests/resources/test_operator_tokens.py b/tests/resources/test_operator_tokens.py new file mode 100644 index 00000000..da89113a --- /dev/null +++ b/tests/resources/test_operator_tokens.py @@ -0,0 +1,82 @@ +from unittest.mock import MagicMock, patch + +import pytest +from cuenca_validations.types import OperatorRole + +from cuenca import OperatorToken +from cuenca.http.client import Session + + +@pytest.fixture +def session() -> Session: + s = Session() + s.configure('api_key', 'api_secret', sandbox=True) + return s + + +@patch('cuenca.http.client.requests.Session.request') +def test_operator_token_create(mock_request: MagicMock, session: Session): + mock_request.return_value.ok = True + mock_request.return_value.content = ( + b'{"id":"OTWqY5cvkISJOxHyEKjAKf8w",' + b'"operator_id":"OPWqY5cvkISJOxHyEKjAKf8w",' + b'"email":"maria.lopez@aceros.com",' + b'"legal_person_id":"USWqY5cvkISJOxHyEKjAKf8w",' + b'"role":"operator",' + b'"token":"plaintext-invite-secret",' + b'"expires_at":"2026-09-24T21:30:56"}' + ) + + token = OperatorToken.create( + 'OPWqY5cvkISJOxHyEKjAKf8w', + 'maria.lopez@aceros.com', + 'USWqY5cvkISJOxHyEKjAKf8w', + OperatorRole.operator, + session=session, + ) + + assert token.id == 'OTWqY5cvkISJOxHyEKjAKf8w' + assert token.operator_id == 'OPWqY5cvkISJOxHyEKjAKf8w' + assert token.email == 'maria.lopez@aceros.com' + assert token.legal_person_id == 'USWqY5cvkISJOxHyEKjAKf8w' + assert token.role == OperatorRole.operator + assert token.token == 'plaintext-invite-secret' + assert token.expires_at is not None + + mock_request.assert_called_once() + _, kwargs = mock_request.call_args + assert kwargs['method'] == 'post' + assert kwargs['url'] == 'https://sandbox.cuenca.com/operator_tokens' + assert kwargs['json'] == { + 'operator_id': 'OPWqY5cvkISJOxHyEKjAKf8w', + 'email': 'maria.lopez@aceros.com', + 'legal_person_id': 'USWqY5cvkISJOxHyEKjAKf8w', + 'role': 'operator', + } + + +@patch('cuenca.http.client.requests.Session.request') +def test_operator_token_create_accepts_role_str( + mock_request: MagicMock, session: Session +): + mock_request.return_value.ok = True + mock_request.return_value.content = ( + b'{"id":"OTWqY5cvkISJOxHyEKjAKf8w",' + b'"operator_id":"OPWqY5cvkISJOxHyEKjAKf8w",' + b'"email":"maria.lopez@aceros.com",' + b'"legal_person_id":"USWqY5cvkISJOxHyEKjAKf8w",' + b'"role":"authorizer",' + b'"token":"plaintext-invite-secret"}' + ) + + token = OperatorToken.create( + 'OPWqY5cvkISJOxHyEKjAKf8w', + 'maria.lopez@aceros.com', + 'USWqY5cvkISJOxHyEKjAKf8w', + 'authorizer', + session=session, + ) + + assert token.role == OperatorRole.authorizer + _, kwargs = mock_request.call_args + assert kwargs['json']['role'] == 'authorizer' From d9d640d9d50d6dadd1f38b60be92a29d565f6d00 Mon Sep 17 00:00:00 2001 From: Juliette Trejo Ceballos Date: Thu, 1 Oct 2026 17:29:17 -0600 Subject: [PATCH 7/7] Update version to 2.2.5.dev0 to indicate a development release --- cuenca/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cuenca/version.py b/cuenca/version.py index cc14a095..ad5488e0 100644 --- a/cuenca/version.py +++ b/cuenca/version.py @@ -1,3 +1,3 @@ -__version__ = '2.2.5' +__version__ = '2.2.5.dev0' CLIENT_VERSION = __version__ API_VERSION = '2020-03-19'