diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eb5689f01..d90df5712 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -51,6 +51,7 @@ repos: hooks: - id: pyupgrade args: [--py38-plus] + language_version: python3.13 - repo: local hooks: - id: check-no-native-http diff --git a/src/snowflake/connector/aio/_network.py b/src/snowflake/connector/aio/_network.py index c34db7800..e0693dff2 100644 --- a/src/snowflake/connector/aio/_network.py +++ b/src/snowflake/connector/aio/_network.py @@ -77,7 +77,7 @@ SQLSTATE_CONNECTION_REJECTED, SQLSTATE_CONNECTION_WAS_NOT_ESTABLISHED, ) -from ..time_util import TimeoutBackoffCtx +from ..time_util import DEFAULT_MASTER_VALIDITY_IN_SECONDS, TimeoutBackoffCtx from ._description import CLIENT_NAME from ._session_manager import ( SessionManager, @@ -143,7 +143,7 @@ def __init__( session_manager: SessionManager | None = None, ): super().__init__(host, port, protocol, inject_client_pause, connection) - self._lock_token = asyncio.Lock() + self._token_async_lock = asyncio.Lock() if session_manager is None: session_manager = ( @@ -155,16 +155,53 @@ def __init__( ) self._session_manager = session_manager - async def close(self) -> None: - if hasattr(self, "_token"): - del self._token - if hasattr(self, "_master_token"): - del self._master_token - if hasattr(self, "_id_token"): - del self._id_token - if hasattr(self, "_mfa_token"): - del self._mfa_token + @property + def id_token(self): + return super().id_token + + @id_token.setter + def id_token(self, value) -> None: + raise TypeError("Use set_id_token_async() in async connections.") + + @property + def mfa_token(self) -> str | None: + return super().mfa_token + + @mfa_token.setter + def mfa_token(self, value: str) -> None: + raise TypeError("Use set_mfa_token_async() in async connections.") + + @property + def master_validity_in_seconds(self) -> int: + return super().master_validity_in_seconds + + @master_validity_in_seconds.setter + def master_validity_in_seconds(self, value) -> None: + raise TypeError( + "Use set_master_validity_in_seconds_async() in async connections." + ) + async def set_id_token_async(self, value) -> None: + async with self._token_async_lock: + with self._lock_token: + self._token_state = self._get_token_state().copy(id_token=value) + + async def set_mfa_token_async(self, value: str) -> None: + async with self._token_async_lock: + with self._lock_token: + self._token_state = self._get_token_state().copy(mfa_token=value) + + async def set_master_validity_in_seconds_async(self, value) -> None: + async with self._token_async_lock: + with self._lock_token: + target = value if value else DEFAULT_MASTER_VALIDITY_IN_SECONDS + self._token_state = self._get_token_state().copy( + master_validity_in_seconds=target + ) + + async def close(self) -> None: + async with self._token_async_lock: + self._remove_token_state() await self._session_manager.close() async def request( @@ -182,7 +219,8 @@ async def request( logger.debug("%s %s", method.upper(), url) if body is None: body = {} - if self.master_token is None and self.token is None: + state = self._get_token_state() + if state.master_token is None and state.session_token is None: Error.errorhandler_wrapper( self._connection, None, @@ -225,7 +263,7 @@ async def request( url, headers, json.dumps(body, cls=SnowflakeRestfulJsonEncoder), - token=self.token, + token=state.session_token, _no_results=_no_results, timeout=timeout, _include_retry_params=_include_retry_params, @@ -235,10 +273,15 @@ async def request( return await self._get_request( url, headers, - token=self.token, + token=state.session_token, timeout=timeout, ) + # TODO(future): Decide legacy vs new token flow and serialization model. Current gaps: + # - Legacy consumers/tests still read connection._token (and friends) which are set at init, but not kept in sync after renewals; either mirror updates here (and delete on close) or audit/remove those reads to rely solely on _TokenState-backed properties. + # - Mutations are serialized via _token_async_lock + _lock_token; sync setters are disabled to avoid blocking the loop, but if we want to keep them, we need a coherent locking story. + # - The race we aim to avoid: mixed token snapshots across concurrent requests/renew/close; consider passing token snapshots via a context var instead of shared mutable state. + # - Post-close: we currently allow recreating state via _get_token_state(); if we prefer hard-fail after close, add an explicit closed guard instead of AttributeError. async def update_tokens( self, session_token, @@ -248,21 +291,26 @@ async def update_tokens( mfa_token=None, ) -> None: """Updates session and master tokens and optionally temporary credential.""" - async with self._lock_token: - self._token = session_token - self._master_token = master_token - self._id_token = id_token - self._mfa_token = mfa_token - self._master_validity_in_seconds = master_validity_in_seconds + async with self._token_async_lock: + with self._lock_token: + new_state = self._get_token_state().copy( + session_token=session_token, + master_token=master_token, + master_validity_in_seconds=master_validity_in_seconds, + id_token=id_token, + mfa_token=mfa_token, + ) + self._token_state = new_state async def _renew_session(self): """Renew a session and master token.""" return await self._token_request(REQUEST_TYPE_RENEW) async def _token_request(self, request_type): + state = self._get_token_state() logger.debug( "updating session. master_token: {}".format( - "****" if self.master_token else None + "****" if state.master_token else None ) ) headers = { @@ -278,9 +326,9 @@ async def _token_request(self, request_type): # NOTE: ensure an empty key if master token is not set. # This avoids HTTP 400. - header_token = self.master_token or "" + header_token = state.master_token or "" body = { - "oldSessionToken": self.token, + "oldSessionToken": state.session_token, "requestType": request_type, } ret = await self._post_request( @@ -331,6 +379,7 @@ async def _token_request(self, request_type): ) async def _heartbeat(self) -> Any | dict[Any, Any] | None: + state = self._get_token_state() headers = { HTTP_HEADER_CONTENT_TYPE: CONTENT_TYPE_APPLICATION_JSON, HTTP_HEADER_ACCEPT: CONTENT_TYPE_APPLICATION_JSON, @@ -345,7 +394,7 @@ async def _heartbeat(self) -> Any | dict[Any, Any] | None: url, headers, None, - token=self.token, + token=state.session_token, ) if not ret.get("success"): logger.error("Failed to heartbeat. code: %s, url: %s", ret.get("code"), url) @@ -353,7 +402,8 @@ async def _heartbeat(self) -> Any | dict[Any, Any] | None: async def delete_session(self, retry: bool = False) -> None: """Deletes the session.""" - if self.master_token is None: + state = self._get_token_state() + if state.master_token is None: Error.errorhandler_wrapper( self._connection, None, @@ -385,7 +435,7 @@ async def delete_session(self, retry: bool = False) -> None: url, headers, json.dumps(body, cls=SnowflakeRestfulJsonEncoder), - token=self.token, + token=state.session_token, timeout=5, no_retry=True, ) @@ -441,10 +491,11 @@ async def _get_request( ) ) if ret.get("success"): + refreshed_state = self._get_token_state() return await self._get_request( url, headers, - token=self.token, + token=refreshed_state.session_token, is_fetch_query_status=is_fetch_query_status, ) @@ -499,8 +550,13 @@ async def _post_request( ) ) if ret.get("success"): + refreshed_state = self._get_token_state() return await self._post_request( - url, headers, body, token=self.token, timeout=timeout + url, + headers, + body, + token=refreshed_state.session_token, + timeout=timeout, ) if isinstance(ret.get("data"), dict) and ret["data"].get("queryId"): @@ -516,10 +572,11 @@ async def _post_request( # ping pong result_url = ret["data"]["getResultUrl"] logger.debug("ping pong starting...") + refreshed_state = self._get_token_state() ret = await self._get_request( result_url, headers, - token=self.token, + token=refreshed_state.session_token, timeout=timeout, is_fetch_query_status=bool( re.match(r"^/queries/.+/result$", result_url) diff --git a/src/snowflake/connector/network.py b/src/snowflake/connector/network.py index 4a8faa3ab..cffec7466 100644 --- a/src/snowflake/connector/network.py +++ b/src/snowflake/connector/network.py @@ -7,6 +7,7 @@ import re import time import uuid +from dataclasses import dataclass, replace from threading import Lock from typing import TYPE_CHECKING, Any, Generator @@ -190,6 +191,20 @@ PAT_WITH_EXTERNAL_SESSION = "PAT_WITH_EXTERNAL_SESSION" +@dataclass(frozen=True) +class _TokenState: + session_token: str | None = None + master_token: str | None = None + master_validity_in_seconds: int | None = None + id_token: str | None = None + mfa_token: str | None = None + personal_access_token: str | None = None + external_session_id: str | None = None + + def copy(self, **kwargs: Any) -> _TokenState: + return replace(self, **kwargs) + + def is_retryable_http_code(code: int) -> bool: """Decides whether code is a retryable HTTP issue.""" return 500 <= code < 600 or code in ( @@ -334,6 +349,7 @@ def __init__( ) ) self._session_manager = session_manager + self._token_state = _TokenState() self._lock_token = Lock() # OCSP mode (OCSPMode.FAIL_OPEN by default) @@ -363,50 +379,56 @@ def __init__( # This is to address the issue where requests hangs _ = "dummy".encode("idna").decode("utf-8") + def _get_token_state(self) -> _TokenState: + return self._token_state if hasattr(self, "_token_state") else _TokenState() + @property def token(self) -> str | None: - return self._token if hasattr(self, "_token") else None + return self._get_token_state().session_token @property def external_session_id(self) -> str | None: - return ( - self._external_session_id if hasattr(self, "_external_session_id") else None - ) + return self._get_token_state().external_session_id @property def master_token(self) -> str | None: - return self._master_token if hasattr(self, "_master_token") else None + return self._get_token_state().master_token @property def master_validity_in_seconds(self) -> int: + state = self._get_token_state() return ( - self._master_validity_in_seconds - if hasattr(self, "_master_validity_in_seconds") - and self._master_validity_in_seconds + state.master_validity_in_seconds + if state.master_validity_in_seconds else DEFAULT_MASTER_VALIDITY_IN_SECONDS ) @master_validity_in_seconds.setter def master_validity_in_seconds(self, value) -> None: - self._master_validity_in_seconds = ( - value if value else DEFAULT_MASTER_VALIDITY_IN_SECONDS - ) + with self._lock_token: + self._token_state = self._get_token_state().copy( + master_validity_in_seconds=( + value if value else DEFAULT_MASTER_VALIDITY_IN_SECONDS + ) + ) @property def id_token(self): - return getattr(self, "_id_token", None) + return self._get_token_state().id_token @id_token.setter def id_token(self, value) -> None: - self._id_token = value + with self._lock_token: + self._token_state = self._get_token_state().copy(id_token=value) @property def mfa_token(self) -> str | None: - return getattr(self, "_mfa_token", None) + return self._get_token_state().mfa_token @mfa_token.setter def mfa_token(self, value: str) -> None: - self._mfa_token = value + with self._lock_token: + self._token_state = self._get_token_state().copy(mfa_token=value) @property def server_url(self) -> str: @@ -420,16 +442,13 @@ def session_manager(self) -> SessionManager: def sessions_map(self) -> dict[str, SessionPool]: return self.session_manager.sessions_map - def close(self) -> None: - if hasattr(self, "_token"): - del self._token - if hasattr(self, "_master_token"): - del self._master_token - if hasattr(self, "_id_token"): - del self._id_token - if hasattr(self, "_mfa_token"): - del self._mfa_token + def _remove_token_state(self): + with self._lock_token: + if hasattr(self, "_token_state"): + del self._token_state + def close(self) -> None: + self._remove_token_state() self.session_manager.close() def request( @@ -445,7 +464,8 @@ def request( ): if body is None: body = {} - if self.master_token is None and self.token is None: + state = self._get_token_state() + if state.master_token is None and state.session_token is None: Error.errorhandler_wrapper( self._connection, None, @@ -488,8 +508,8 @@ def request( url, headers, json.dumps(body, cls=SnowflakeRestfulJsonEncoder), - token=self.token, - external_session_id=self.external_session_id, + token=state.session_token, + external_session_id=state.external_session_id, _no_results=_no_results, timeout=timeout, _include_retry_params=_include_retry_params, @@ -499,8 +519,8 @@ def request( return self._get_request( url, headers, - token=self.token, - external_session_id=self.external_session_id, + token=state.session_token, + external_session_id=state.external_session_id, timeout=timeout, ) @@ -514,11 +534,14 @@ def update_tokens( ) -> None: """Updates session and master tokens and optionally temporary credential.""" with self._lock_token: - self._token = session_token - self._master_token = master_token - self._id_token = id_token - self._mfa_token = mfa_token - self._master_validity_in_seconds = master_validity_in_seconds + new_state = self._get_token_state().copy( + session_token=session_token, + master_token=master_token, + master_validity_in_seconds=master_validity_in_seconds, + id_token=id_token, + mfa_token=mfa_token, + ) + self._token_state = new_state def set_pat_and_external_session( self, @@ -527,18 +550,22 @@ def set_pat_and_external_session( ) -> None: """Updates session and master tokens and optionally temporary credential.""" with self._lock_token: - self._personal_access_token = personal_access_token - self._token = personal_access_token - self._external_session_id = external_session_id + new_state = self._get_token_state().copy( + session_token=personal_access_token, + personal_access_token=personal_access_token, + external_session_id=external_session_id, + ) + self._token_state = new_state def _renew_session(self): """Renew a session and master token.""" return self._token_request(REQUEST_TYPE_RENEW) def _token_request(self, request_type): + state = self._get_token_state() logger.debug( "updating session. master_token: {}".format( - "****" if self.master_token else None + "****" if state.master_token else None ) ) headers = { @@ -554,9 +581,9 @@ def _token_request(self, request_type): # NOTE: ensure an empty key if master token is not set. # This avoids HTTP 400. - header_token = self.master_token or "" + header_token = state.master_token or "" body = { - "oldSessionToken": self.token, + "oldSessionToken": state.session_token, "requestType": request_type, } ret = self._post_request( @@ -607,6 +634,7 @@ def _token_request(self, request_type): ) def _heartbeat(self) -> Any | dict[Any, Any] | None: + state = self._get_token_state() headers = { HTTP_HEADER_CONTENT_TYPE: CONTENT_TYPE_APPLICATION_JSON, HTTP_HEADER_ACCEPT: CONTENT_TYPE_APPLICATION_JSON, @@ -621,7 +649,7 @@ def _heartbeat(self) -> Any | dict[Any, Any] | None: url, headers, None, - token=self.token, + token=state.session_token, ) if not ret.get("success"): logger.error("Failed to heartbeat. code: %s, url: %s", ret.get("code"), url) @@ -629,7 +657,8 @@ def _heartbeat(self) -> Any | dict[Any, Any] | None: def delete_session(self, retry: bool = False) -> None: """Deletes the session.""" - if self.master_token is None: + state = self._get_token_state() + if state.master_token is None: Error.errorhandler_wrapper( self._connection, None, @@ -661,7 +690,7 @@ def delete_session(self, retry: bool = False) -> None: url, headers, json.dumps(body, cls=SnowflakeRestfulJsonEncoder), - token=self.token, + token=state.session_token, timeout=5, no_retry=True, ) @@ -722,10 +751,11 @@ def _get_request( ) ) if ret.get("success"): + refreshed_state = self._get_token_state() return self._get_request( url, headers, - token=self.token, + token=refreshed_state.session_token, is_fetch_query_status=is_fetch_query_status, ) @@ -787,8 +817,14 @@ def _post_request( ) ) if ret.get("success"): + refreshed_state = self._get_token_state() return self._post_request( - url, headers, body, token=self.token, timeout=timeout + url, + headers, + body, + token=refreshed_state.session_token, + external_session_id=refreshed_state.external_session_id, + timeout=timeout, ) if isinstance(ret.get("data"), dict) and ret["data"].get("queryId"): @@ -804,10 +840,11 @@ def _post_request( # ping pong result_url = ret["data"]["getResultUrl"] logger.debug("ping pong starting...") + refreshed_state = self._get_token_state() ret = self._get_request( result_url, headers, - token=self.token, + token=refreshed_state.session_token, timeout=timeout, is_fetch_query_status=bool( re.match(r"^/queries/.+/result$", result_url) diff --git a/test/integ/aio_it/test_connection_async.py b/test/integ/aio_it/test_connection_async.py index 049ade547..e8fd0583e 100644 --- a/test/integ/aio_it/test_connection_async.py +++ b/test/integ/aio_it/test_connection_async.py @@ -122,8 +122,8 @@ async def test_with_tokens(conn_cnx): timezone="UTC", ) as initial_cnx: assert initial_cnx, "invalid initial cnx" - master_token = initial_cnx.rest._master_token - session_token = initial_cnx.rest._token + master_token = initial_cnx.rest.master_token + session_token = initial_cnx.rest.token token_cnx = await create_connection( "default", session_token=session_token, master_token=master_token ) @@ -146,8 +146,8 @@ async def test_with_tokens_expired(conn_cnx): timezone="UTC", ) as initial_cnx: assert initial_cnx, "invalid initial cnx" - master_token = initial_cnx._rest._master_token - session_token = initial_cnx._rest._token + master_token = initial_cnx._rest.master_token + session_token = initial_cnx._rest.token with pytest.raises(ProgrammingError): async with conn_cnx( diff --git a/test/integ/test_connection.py b/test/integ/test_connection.py index 48f0700e4..fe60b623c 100644 --- a/test/integ/test_connection.py +++ b/test/integ/test_connection.py @@ -129,8 +129,8 @@ def test_with_tokens(conn_cnx): timezone="UTC", ) as initial_cnx: assert initial_cnx, "invalid initial cnx" - master_token = initial_cnx.rest._master_token - session_token = initial_cnx.rest._token + master_token = initial_cnx.rest.master_token + session_token = initial_cnx.rest.token token_cnx = create_connection( "default", session_token=session_token, master_token=master_token ) @@ -153,8 +153,8 @@ def test_with_tokens_expired(conn_cnx): timezone="UTC", ) as initial_cnx: assert initial_cnx, "invalid initial cnx" - master_token = initial_cnx._rest._master_token - session_token = initial_cnx._rest._token + master_token = initial_cnx._rest.master_token + session_token = initial_cnx._rest.token with pytest.raises(ProgrammingError): token_cnx = create_connection( @@ -1518,7 +1518,7 @@ def _assert_log_bytes_within_tolerance(actual_bytes, expected_bytes, tolerance): """Assert that log bytes are within acceptable tolerance.""" assert actual_bytes == pytest.approx(expected_bytes, rel=tolerance), ( f"Log bytes {actual_bytes} is not approximately equal to expected {expected_bytes} " - f"within {tolerance*100}% tolerance. " + f"within {tolerance * 100}% tolerance. " f"This may indicate unwanted logs being produced or changes in logging behavior." ) diff --git a/test/unit/aio/test_renew_session_async.py b/test/unit/aio/test_renew_session_async.py index b6a5841e2..fd3c08c12 100644 --- a/test/unit/aio/test_renew_session_async.py +++ b/test/unit/aio/test_renew_session_async.py @@ -9,6 +9,8 @@ from test.unit.aio.mock_utils import mock_connection from unittest.mock import Mock, PropertyMock +import pytest + from snowflake.connector.aio._network import SnowflakeRestful @@ -24,8 +26,7 @@ async def test_renew_session(): rest = SnowflakeRestful( host="testaccount.snowflakecomputing.com", port=443, connection=connection ) - rest._token = OLD_SESSION_TOKEN - rest._master_token = OLD_MASTER_TOKEN + await rest.update_tokens(OLD_SESSION_TOKEN, OLD_MASTER_TOKEN) # inject a fake method (success) async def fake_request_exec(**_): @@ -54,11 +55,38 @@ async def fake_request_exec(**_): assert rest._connection.errorhandler.called # error # no master token - del rest._master_token + await rest.update_tokens(OLD_SESSION_TOKEN, None) await rest._renew_session() assert rest._connection.errorhandler.called # error +async def test_token_state_snapshot_preserves_attributes_async(): + connection = mock_connection() + rest = SnowflakeRestful( + host="testaccount.snowflakecomputing.com", port=443, connection=connection + ) + + await rest.update_tokens("legacy-session", "legacy-master") + + state = rest._get_token_state() + assert state.session_token == "legacy-session" + assert state.master_token == "legacy-master" + + await rest.update_tokens( + "new-session", + "new-master", + master_validity_in_seconds=33, + id_token="id-token", + ) + + updated_state = rest._get_token_state() + assert updated_state.session_token == "new-session" + assert updated_state.master_token == "new-master" + assert rest.master_validity_in_seconds == 33 + assert rest.token == "new-session" + assert rest.master_token == "new-master" + + async def test_mask_token_when_renew_session(caplog): caplog.set_level(logging.DEBUG) OLD_SESSION_TOKEN = "old_session_token" @@ -72,8 +100,7 @@ async def test_mask_token_when_renew_session(caplog): rest = SnowflakeRestful( host="testaccount.snowflakecomputing.com", port=443, connection=connection ) - rest._token = OLD_SESSION_TOKEN - rest._master_token = OLD_MASTER_TOKEN + await rest.update_tokens(OLD_SESSION_TOKEN, OLD_MASTER_TOKEN) # inject a fake method (success) async def fake_request_exec(**_): @@ -105,3 +132,25 @@ async def fake_request_exec(**_): assert "new_master_token" not in caplog.text assert "old_session_token" not in caplog.text assert "old_master_token" not in caplog.text + + +async def test_sync_setters_blocked_and_async_setters_work(): + connection = mock_connection() + rest = SnowflakeRestful( + host="testaccount.snowflakecomputing.com", port=443, connection=connection + ) + + with pytest.raises(TypeError): + rest.id_token = "id-token" + with pytest.raises(TypeError): + rest.mfa_token = "mfa-token" + with pytest.raises(TypeError): + rest.master_validity_in_seconds = 10 + + await rest.set_id_token_async("id-token") + await rest.set_mfa_token_async("mfa-token") + await rest.set_master_validity_in_seconds_async(55) + + assert rest.id_token == "id-token" + assert rest.mfa_token == "mfa-token" + assert rest.master_validity_in_seconds == 55 diff --git a/test/unit/test_network.py b/test/unit/test_network.py index b9bb02966..38295ca98 100644 --- a/test/unit/test_network.py +++ b/test/unit/test_network.py @@ -97,8 +97,7 @@ def test_fetch_auth(): rest = SnowflakeRestful( host="test.snowflakecomputing.com", port=443, connection=connection ) - rest._token = "test-token" - rest._master_token = "test-master-token" + rest.update_tokens("test-token", "test-master-token") captured_auth = None @@ -143,3 +142,31 @@ def mock_request(**kwargs): ) assert isinstance(captured_auth, PATWithExternalSessionAuth) assert captured_auth.external_session_id == "dummy-external-session-id" + + +def test_token_state_snapshot_preserves_attributes(): + connection = mock_connection() + rest = SnowflakeRestful( + host="testaccount.snowflakecomputing.com", port=443, connection=connection + ) + + rest.update_tokens("legacy-session", "legacy-master") + + state = rest._get_token_state() + assert state.session_token == "legacy-session" + assert state.master_token == "legacy-master" + + rest.update_tokens( + "new-session", + "new-master", + master_validity_in_seconds=42, + id_token="id-token", + mfa_token="mfa-token", + ) + + updated_state = rest._get_token_state() + assert updated_state.session_token == "new-session" + assert updated_state.master_token == "new-master" + assert rest.master_validity_in_seconds == 42 + assert rest.token == "new-session" + assert rest.master_token == "new-master" diff --git a/test/unit/test_renew_session.py b/test/unit/test_renew_session.py index bfc5bf624..4bce0576d 100644 --- a/test/unit/test_renew_session.py +++ b/test/unit/test_renew_session.py @@ -21,8 +21,7 @@ def test_renew_session(): rest = SnowflakeRestful( host="testaccount.snowflakecomputing.com", port=443, connection=connection ) - rest._token = OLD_SESSION_TOKEN - rest._master_token = OLD_MASTER_TOKEN + rest.update_tokens(OLD_SESSION_TOKEN, OLD_MASTER_TOKEN) # inject a fake method (success) def fake_request_exec(**_): @@ -51,7 +50,12 @@ def fake_request_exec(**_): assert rest._connection.errorhandler.called # error # no master token - del rest._master_token + del rest._token_state + rest._renew_session() + assert rest._connection.errorhandler.called # error + + # no master token + rest.update_tokens(OLD_SESSION_TOKEN, None) rest._renew_session() assert rest._connection.errorhandler.called # error @@ -69,8 +73,7 @@ def test_mask_token_when_renew_session(caplog): rest = SnowflakeRestful( host="testaccount.snowflakecomputing.com", port=443, connection=connection ) - rest._token = OLD_SESSION_TOKEN - rest._master_token = OLD_MASTER_TOKEN + rest.update_tokens(OLD_SESSION_TOKEN, OLD_MASTER_TOKEN) # inject a fake method (success) def fake_request_exec(**_):