def __init__(self, application: "CuraApplication", parent=None) -> None: super().__init__(parent) self._application = application self._error_message = None # type: Optional[Message] self._logged_in = False self._callback_port = 32118 self._oauth_root = "https://account.ultimaker.com" self._cloud_api_root = "https://api.ultimaker.com" self._oauth_settings = OAuth2Settings( OAUTH_SERVER_URL=self._oauth_root, CALLBACK_PORT=self._callback_port, CALLBACK_URL="http://localhost:{}/callback".format( self._callback_port), CLIENT_ID="um---------------ultimaker_cura_drive_plugin", CLIENT_SCOPES="user.read drive.backups.read drive.backups.write", AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data", AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format( self._oauth_root), AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root)) self._authorization_service = AuthorizationService( self._oauth_settings)
def test_loginAndLogout() -> None: preferences = Preferences() authorization_service = AuthorizationService(OAUTH_SETTINGS, preferences) authorization_service.onAuthenticationError.emit = MagicMock() authorization_service.onAuthStateChanged.emit = MagicMock() authorization_service.initialize() # Let the service think there was a successful response with patch.object(AuthorizationHelpers, "parseJWT", return_value=UserProfile()): authorization_service._onAuthStateChanged(SUCCESSFUL_AUTH_RESPONSE) # Ensure that the error signal was not triggered assert authorization_service.onAuthenticationError.emit.call_count == 0 # Since we said that it went right this time, validate that we got a signal. assert authorization_service.onAuthStateChanged.emit.call_count == 1 assert authorization_service.getUserProfile() is not None assert authorization_service.getAccessToken() == "beep" # Check that we stored the authentication data, so next time the user won't have to log in again. assert preferences.getValue("test/auth_data") is not None # We're logged in now, also check if logging out works authorization_service.deleteAuthData() assert authorization_service.onAuthStateChanged.emit.call_count == 2 assert authorization_service.getUserProfile() is None # Ensure the data is gone after we logged out. assert preferences.getValue("test/auth_data") == "{}"
def __init__(self, application: "CuraApplication", parent=None) -> None: super().__init__(parent) self._application = application self._error_message = None # type: Optional[Message] self._logged_in = False self._callback_port = 32118 self._oauth_root = UltimakerCloudAuthentication.CuraCloudAccountAPIRoot self._oauth_settings = OAuth2Settings( OAUTH_SERVER_URL=self._oauth_root, CALLBACK_PORT=self._callback_port, CALLBACK_URL="http://localhost:{}/callback".format( self._callback_port), CLIENT_ID="um----------------------------ultimaker_cura", CLIENT_SCOPES= "account.user.read drive.backup.read drive.backup.write packages.download " "packages.rating.read packages.rating.write connect.cluster.read connect.cluster.write " "cura.printjob.read cura.printjob.write cura.mesh.read cura.mesh.write", AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data", AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format( self._oauth_root), AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root)) self._authorization_service = AuthorizationService( self._oauth_settings)
def test__parseJWTFailOnRefresh(): authorization_service = AuthorizationService(OAUTH_SETTINGS, Preferences()) with patch.object(AuthorizationService, "getUserProfile", return_value=UserProfile()): authorization_service._storeAuthData(SUCCESSFUL_AUTH_RESPONSE) with patch.object(AuthorizationHelpers, "getAccessTokenUsingRefreshToken", return_value=FAILED_AUTH_RESPONSE): assert authorization_service._parseJWT() is None
def test__parseJWTNoRefreshToken(): authorization_service = AuthorizationService(OAUTH_SETTINGS, Preferences()) with patch.object(AuthorizationService, "getUserProfile", return_value=UserProfile()): authorization_service._storeAuthData(NO_REFRESH_AUTH_RESPONSE) assert authorization_service._parseJWT() is None
def test_initialize(): original_preference = MagicMock() initialize_preferences = MagicMock() authorization_service = AuthorizationService(OAUTH_SETTINGS, original_preference) authorization_service.initialize(initialize_preferences) assert initialize_preferences.addPreference.called original_preference.addPreference.assert_not_called()
def test_initialize(): original_preference = MagicMock() initialize_preferences = MagicMock() authorization_service = AuthorizationService(OAUTH_SETTINGS, original_preference) authorization_service.initialize(initialize_preferences) initialize_preferences.addPreference.assert_called_once() original_preference.addPreference.assert_not_called()
def test__generate_auth_url() -> None: preferences = Preferences() authorization_service = AuthorizationService(OAUTH_SETTINGS, preferences) query_parameters_dict = { "client_id": "", "redirect_uri": OAUTH_SETTINGS.CALLBACK_URL, "scope": OAUTH_SETTINGS.CLIENT_SCOPES, "response_type": "code" } auth_url = authorization_service._generate_auth_url( query_parameters_dict, force_browser_logout=False) assert MYCLOUD_LOGOFF_URL + "&next=" not in auth_url auth_url = authorization_service._generate_auth_url( query_parameters_dict, force_browser_logout=True) assert MYCLOUD_LOGOFF_URL + "&next=" in auth_url
def test_wrongServerResponses() -> None: authorization_service = AuthorizationService(OAUTH_SETTINGS, Preferences()) authorization_service.initialize() authorization_service._onAuthStateChanged(MALFORMED_AUTH_RESPONSE) def callback(profile): assert profile is None authorization_service.getUserProfile(callback)
def test_wrongServerResponses() -> None: authorization_service = AuthorizationService(OAUTH_SETTINGS, Preferences()) authorization_service.initialize() with patch.object(AuthorizationHelpers, "parseJWT", return_value=UserProfile()): authorization_service._onAuthStateChanged(MALFORMED_AUTH_RESPONSE) assert authorization_service.getUserProfile() is None
def __init__(self, application: "CuraApplication", parent=None) -> None: super().__init__(parent) self._application = application self._new_cloud_printers_detected = False self._error_message = None # type: Optional[Message] self._logged_in = False self._sync_state = SyncState.IDLE self._manual_sync_enabled = False self._update_packages_enabled = False self._update_packages_action = None # type: Optional[Callable] self._last_sync_str = "-" self._callback_port = 32118 self._oauth_root = UltimakerCloudConstants.CuraCloudAccountAPIRoot self._oauth_settings = OAuth2Settings( OAUTH_SERVER_URL=self._oauth_root, CALLBACK_PORT=self._callback_port, CALLBACK_URL="http://localhost:{}/callback".format( self._callback_port), CLIENT_ID="um----------------------------ultimaker_cura", CLIENT_SCOPES= "account.user.read drive.backup.read drive.backup.write packages.download " "packages.rating.read packages.rating.write connect.cluster.read connect.cluster.write " "cura.printjob.read cura.printjob.write cura.mesh.read cura.mesh.write", AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data", AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format( self._oauth_root), AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root)) self._authorization_service = AuthorizationService( self._oauth_settings) # Create a timer for automatic account sync self._update_timer = QTimer() self._update_timer.setInterval(int(self.SYNC_INTERVAL * 1000)) # The timer is restarted explicitly after an update was processed. This prevents 2 concurrent updates self._update_timer.setSingleShot(True) self._update_timer.timeout.connect(self.sync) self._sync_services = {} # type: Dict[str, int] """contains entries "service_name" : SyncState"""
def test_refreshAccessTokenFailed(): authorization_service = AuthorizationService(OAUTH_SETTINGS, Preferences()) authorization_service.initialize() with patch.object(AuthorizationService, "getUserProfile", return_value=UserProfile()): authorization_service._storeAuthData(SUCCESSFUL_AUTH_RESPONSE) authorization_service.onAuthStateChanged.emit = MagicMock() with patch.object(AuthorizationHelpers, "getAccessTokenUsingRefreshToken", return_value=FAILED_AUTH_RESPONSE): authorization_service.refreshAccessToken() assert authorization_service.onAuthStateChanged.emit.called_with(False)
def test__parseJWTSucceedOnRefresh(): """ Tries to refresh the authentication token using a valid refresh token. The request should succeed. """ authorization_service = AuthorizationService(OAUTH_SETTINGS, Preferences()) authorization_service.initialize() with patch.object(AuthorizationService, "getUserProfile", return_value=UserProfile()): authorization_service._storeAuthData(EXPIRED_AUTH_RESPONSE) mock_callback = Mock() # To log the final profile response. mock_reply_success = Mock( ) # The reply should be a failure when using the expired access token, but succeed when using the refresh token. mock_reply_success.error = Mock( return_value=QNetworkReply.NetworkError.NoError) mock_reply_failure = Mock() mock_reply_failure.error = Mock( return_value=QNetworkReply.NetworkError.AuthenticationRequiredError) http_mock = Mock() def mock_get(url, headers_dict, callback, error_callback): if (headers_dict == {"Authorization": "Bearer beep"}): callback(mock_reply_success) else: callback(mock_reply_failure) http_mock.get = mock_get http_mock.readJSON = Mock(return_value={ "data": { "user_id": "user_idea", "username": "******" } }) def mock_refresh(self, refresh_token, callback): # Refreshing gives a valid token. callback(SUCCESSFUL_AUTH_RESPONSE) with patch( "cura.OAuth2.AuthorizationHelpers.AuthorizationHelpers.getAccessTokenUsingRefreshToken", mock_refresh): with patch( "UM.TaskManagement.HttpRequestManager.HttpRequestManager.getInstance", MagicMock(return_value=http_mock)): authorization_service._parseJWT(mock_callback) mock_callback.assert_called_once() profile_reply = mock_callback.call_args_list[0][0][0] assert profile_reply.user_id == "user_idea" assert profile_reply.username == "Ghostkeeper"
def test__parseJWTSucceedOnRefresh(): authorization_service = AuthorizationService(OAUTH_SETTINGS, Preferences()) authorization_service.initialize() with patch.object(AuthorizationService, "getUserProfile", return_value=UserProfile()): authorization_service._storeAuthData(SUCCESSFUL_AUTH_RESPONSE) with patch.object(AuthorizationHelpers, "getAccessTokenUsingRefreshToken", return_value=SUCCESSFUL_AUTH_RESPONSE): with patch.object(AuthorizationHelpers, "parseJWT", MagicMock(return_value = None)) as mocked_parseJWT: authorization_service._parseJWT() mocked_parseJWT.assert_called_with("beep")
def test__parseJWTFailOnRefresh(): """ Tries to refresh the authentication token using an invalid refresh token. The request should fail. """ authorization_service = AuthorizationService(OAUTH_SETTINGS, Preferences()) with patch.object(AuthorizationService, "getUserProfile", return_value=UserProfile()): authorization_service._storeAuthData(SUCCESSFUL_AUTH_RESPONSE) mock_callback = Mock() # To log the final profile response. mock_reply = Mock( ) # The response that the request should give, containing an error about it failing to authenticate. mock_reply.error = Mock( return_value=QNetworkReply.NetworkError.AuthenticationRequiredError ) # The reply is 403: Authentication required, meaning the server responded with a "Can't do that, Dave". http_mock = Mock() http_mock.get = lambda url, headers_dict, callback, error_callback: callback( mock_reply) http_mock.post = lambda url, data, headers_dict, callback, error_callback: callback( mock_reply) with patch( "UM.TaskManagement.HttpRequestManager.HttpRequestManager.readJSON", Mock( return_value={"error_description": "Mock a failed request!"})): with patch( "UM.TaskManagement.HttpRequestManager.HttpRequestManager.getInstance", MagicMock(return_value=http_mock)): authorization_service._parseJWT(mock_callback) mock_callback.assert_called_once_with(None)
def test__parseJWTNoRefreshToken(): """ Tests parsing the user profile if there is no refresh token stored, but there is a normal authentication token. The request for the user profile using the authentication token should still work normally. """ authorization_service = AuthorizationService(OAUTH_SETTINGS, Preferences()) with patch.object(AuthorizationService, "getUserProfile", return_value=UserProfile()): authorization_service._storeAuthData(NO_REFRESH_AUTH_RESPONSE) mock_callback = Mock() # To log the final profile response. mock_reply = Mock( ) # The user profile that the service should respond with. mock_reply.error = Mock(return_value=QNetworkReply.NetworkError.NoError) http_mock = Mock() http_mock.get = lambda url, headers_dict, callback, error_callback: callback( mock_reply) http_mock.readJSON = Mock(return_value={ "data": { "user_id": "id_ego_or_superego", "username": "******" } }) with patch( "UM.TaskManagement.HttpRequestManager.HttpRequestManager.getInstance", MagicMock(return_value=http_mock)): authorization_service._parseJWT(mock_callback) mock_callback.assert_called_once() profile_reply = mock_callback.call_args_list[0][0][0] assert profile_reply.user_id == "id_ego_or_superego" assert profile_reply.username == "Ghostkeeper"
def test__parseJWTSucceedOnRefresh(): authorization_service = AuthorizationService(OAUTH_SETTINGS, Preferences()) authorization_service.initialize() with patch.object(AuthorizationService, "getUserProfile", return_value=UserProfile()): authorization_service._storeAuthData(SUCCESSFUL_AUTH_RESPONSE) with patch.object(AuthorizationHelpers, "getAccessTokenUsingRefreshToken", return_value=SUCCESSFUL_AUTH_RESPONSE): with patch.object(AuthorizationHelpers, "parseJWT", MagicMock(return_value=None)) as mocked_parseJWT: authorization_service._parseJWT() mocked_parseJWT.assert_called_with("beep")
def test_cleanAuthService() -> None: """ Ensure that when setting up an AuthorizationService, no data is set. """ authorization_service = AuthorizationService(OAUTH_SETTINGS, Preferences()) authorization_service.initialize() mock_callback = Mock() authorization_service.getUserProfile(mock_callback) mock_callback.assert_called_once_with(None) assert authorization_service.getAccessToken() is None
def test_failedLogin() -> None: authorization_service = AuthorizationService(OAUTH_SETTINGS, Preferences()) authorization_service.onAuthenticationError.emit = MagicMock() authorization_service.onAuthStateChanged.emit = MagicMock() authorization_service.initialize() # Let the service think there was a failed response authorization_service._onAuthStateChanged(FAILED_AUTH_RESPONSE) # Check that the error signal was triggered assert authorization_service.onAuthenticationError.emit.call_count == 1 # Since nothing changed, this should still be 0. assert authorization_service.onAuthStateChanged.emit.call_count == 0 # Validate that there is no user profile or token assert authorization_service.getUserProfile() is None assert authorization_service.getAccessToken() is None
def test_localAuthServer(webbrowser_open, start_auth_server, stop_auth_server) -> None: preferences = Preferences() authorization_service = AuthorizationService(OAUTH_SETTINGS, preferences) authorization_service.startAuthorizationFlow() assert webbrowser_open.call_count == 1 # Ensure that the Authorization service tried to start the server. assert start_auth_server.call_count == 1 assert stop_auth_server.call_count == 0 authorization_service._onAuthStateChanged(FAILED_AUTH_RESPONSE) # Ensure that it stopped the server. assert stop_auth_server.call_count == 1
def test_refreshAccessTokenFailed(): """ Test if the authentication is reset once the refresh token fails to refresh access. """ authorization_service = AuthorizationService(OAUTH_SETTINGS, Preferences()) authorization_service.initialize() def mock_refresh(self, refresh_token, callback): # Refreshing gives a valid token. callback(FAILED_AUTH_RESPONSE) mock_reply = Mock( ) # The response that the request should give, containing an error about it failing to authenticate. mock_reply.error = Mock( return_value=QNetworkReply.NetworkError.AuthenticationRequiredError ) # The reply is 403: Authentication required, meaning the server responded with a "Can't do that, Dave". http_mock = Mock() http_mock.get = lambda url, headers_dict, callback, error_callback: callback( mock_reply) http_mock.post = lambda url, data, headers_dict, callback, error_callback: callback( mock_reply) with patch( "UM.TaskManagement.HttpRequestManager.HttpRequestManager.readJSON", Mock( return_value={"error_description": "Mock a failed request!"})): with patch( "UM.TaskManagement.HttpRequestManager.HttpRequestManager.getInstance", MagicMock(return_value=http_mock)): authorization_service._storeAuthData(SUCCESSFUL_AUTH_RESPONSE) authorization_service.onAuthStateChanged.emit = MagicMock() with patch( "cura.OAuth2.AuthorizationHelpers.AuthorizationHelpers.getAccessTokenUsingRefreshToken", mock_refresh): authorization_service.refreshAccessToken() assert authorization_service.onAuthStateChanged.emit.called_with( False)
def __init__(self, application: "CuraApplication", parent = None) -> None: super().__init__(parent) self._application = application self._error_message = None # type: Optional[Message] self._logged_in = False self._callback_port = 32118 self._oauth_root = UltimakerCloudAuthentication.CuraCloudAccountAPIRoot self._oauth_settings = OAuth2Settings( OAUTH_SERVER_URL= self._oauth_root, CALLBACK_PORT=self._callback_port, CALLBACK_URL="http://localhost:{}/callback".format(self._callback_port), CLIENT_ID="um----------------------------ultimaker_cura", CLIENT_SCOPES="account.user.read drive.backup.read drive.backup.write packages.download " "packages.rating.read packages.rating.write connect.cluster.read connect.cluster.write " "cura.printjob.read cura.printjob.write cura.mesh.read cura.mesh.write", AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data", AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format(self._oauth_root), AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root) ) self._authorization_service = AuthorizationService(self._oauth_settings)
def test_loginAndLogout() -> None: preferences = Preferences() authorization_service = AuthorizationService(OAUTH_SETTINGS, preferences) authorization_service.onAuthenticationError.emit = MagicMock() authorization_service.onAuthStateChanged.emit = MagicMock() authorization_service.initialize() # Let the service think there was a succesfull response with patch.object(AuthorizationHelpers, "parseJWT", return_value=UserProfile()): authorization_service._onAuthStateChanged(SUCCESFULL_AUTH_RESPONSE) # Ensure that the error signal was not triggered assert authorization_service.onAuthenticationError.emit.call_count == 0 # Since we said that it went right this time, validate that we got a signal. assert authorization_service.onAuthStateChanged.emit.call_count == 1 assert authorization_service.getUserProfile() is not None assert authorization_service.getAccessToken() == "beep" # Check that we stored the authentication data, so next time the user won't have to log in again. assert preferences.getValue("test/auth_data") is not None # We're logged in now, also check if logging out works authorization_service.deleteAuthData() assert authorization_service.onAuthStateChanged.emit.call_count == 2 assert authorization_service.getUserProfile() is None # Ensure the data is gone after we logged out. assert preferences.getValue("test/auth_data") == "{}"
class Account(QObject): """The account API provides a version-proof bridge to use Ultimaker Accounts Usage: .. code-block:: python from cura.API import CuraAPI api = CuraAPI() api.account.login() api.account.logout() api.account.userProfile # Who is logged in """ # The interval in which sync services are automatically triggered SYNC_INTERVAL = 30.0 # seconds Q_ENUMS(SyncState) loginStateChanged = pyqtSignal(bool) """Signal emitted when user logged in or out""" accessTokenChanged = pyqtSignal() syncRequested = pyqtSignal() """Sync services may connect to this signal to receive sync triggers. Services should be resilient to receiving a signal while they are still syncing, either by ignoring subsequent signals or restarting a sync. See setSyncState() for providing user feedback on the state of your service. """ lastSyncDateTimeChanged = pyqtSignal() syncStateChanged = pyqtSignal(int) # because SyncState is an int Enum manualSyncEnabledChanged = pyqtSignal(bool) def __init__(self, application: "CuraApplication", parent=None) -> None: super().__init__(parent) self._application = application self._new_cloud_printers_detected = False self._error_message = None # type: Optional[Message] self._logged_in = False self._sync_state = SyncState.IDLE self._manual_sync_enabled = False self._last_sync_str = "-" self._callback_port = 32118 self._oauth_root = UltimakerCloudConstants.CuraCloudAccountAPIRoot self._oauth_settings = OAuth2Settings( OAUTH_SERVER_URL=self._oauth_root, CALLBACK_PORT=self._callback_port, CALLBACK_URL="http://localhost:{}/callback".format( self._callback_port), CLIENT_ID="um----------------------------ultimaker_cura", CLIENT_SCOPES= "account.user.read drive.backup.read drive.backup.write packages.download " "packages.rating.read packages.rating.write connect.cluster.read connect.cluster.write " "cura.printjob.read cura.printjob.write cura.mesh.read cura.mesh.write", AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data", AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format( self._oauth_root), AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root)) self._authorization_service = AuthorizationService( self._oauth_settings) # Create a timer for automatic account sync self._update_timer = QTimer() self._update_timer.setInterval(int(self.SYNC_INTERVAL * 1000)) # The timer is restarted explicitly after an update was processed. This prevents 2 concurrent updates self._update_timer.setSingleShot(True) self._update_timer.timeout.connect(self.syncRequested) self._sync_services = {} # type: Dict[str, int] """contains entries "service_name" : SyncState""" def initialize(self) -> None: self._authorization_service.initialize( self._application.getPreferences()) self._authorization_service.onAuthStateChanged.connect( self._onLoginStateChanged) self._authorization_service.onAuthenticationError.connect( self._onLoginStateChanged) self._authorization_service.accessTokenChanged.connect( self._onAccessTokenChanged) self._authorization_service.loadAuthDataFromPreferences() def setSyncState(self, service_name: str, state: int) -> None: """ Can be used to register sync services and update account sync states Contract: A sync service is expected exit syncing state in all cases, within reasonable time Example: `setSyncState("PluginSyncService", SyncState.SYNCING)` :param service_name: A unique name for your service, such as `plugins` or `backups` :param state: One of SyncState """ prev_state = self._sync_state self._sync_services[service_name] = state if any(val == SyncState.SYNCING for val in self._sync_services.values()): self._sync_state = SyncState.SYNCING self._setManualSyncEnabled(False) elif any(val == SyncState.ERROR for val in self._sync_services.values()): self._sync_state = SyncState.ERROR self._setManualSyncEnabled(True) else: self._sync_state = SyncState.SUCCESS self._setManualSyncEnabled(False) if self._sync_state != prev_state: self.syncStateChanged.emit(self._sync_state) if self._sync_state == SyncState.SUCCESS: self._last_sync_str = datetime.now().strftime("%d/%m/%Y %H:%M") self.lastSyncDateTimeChanged.emit() if self._sync_state != SyncState.SYNCING: # schedule new auto update after syncing completed (for whatever reason) if not self._update_timer.isActive(): self._update_timer.start() def _onAccessTokenChanged(self): self.accessTokenChanged.emit() @property def is_staging(self) -> bool: """Indication whether the given authentication is applied against staging or not.""" return "staging" in self._oauth_root @pyqtProperty(bool, notify=loginStateChanged) def isLoggedIn(self) -> bool: return self._logged_in def _onLoginStateChanged(self, logged_in: bool = False, error_message: Optional[str] = None) -> None: if error_message: if self._error_message: self._error_message.hide() self._error_message = Message(error_message, title=i18n_catalog.i18nc( "@info:title", "Login failed")) self._error_message.show() self._logged_in = False self.loginStateChanged.emit(False) if self._update_timer.isActive(): self._update_timer.stop() return if self._logged_in != logged_in: self._logged_in = logged_in self.loginStateChanged.emit(logged_in) if logged_in: self._setManualSyncEnabled(False) self._sync() else: if self._update_timer.isActive(): self._update_timer.stop() def _sync(self) -> None: """Signals all sync services to start syncing This can be considered a forced sync: even when a sync is currently running, a sync will be requested. """ if self._update_timer.isActive(): self._update_timer.stop() elif self._sync_state == SyncState.SYNCING: Logger.warning( "Starting a new sync while previous sync was not completed\n{}", str(self._sync_services)) self.syncRequested.emit() def _setManualSyncEnabled(self, enabled: bool) -> None: if self._manual_sync_enabled != enabled: self._manual_sync_enabled = enabled self.manualSyncEnabledChanged.emit(enabled) @pyqtSlot() @pyqtSlot(bool) def login(self, force_logout_before_login: bool = False) -> None: """ Initializes the login process. If the user is logged in already and force_logout_before_login is true, Cura will logout from the account before initiating the authorization flow. If the user is logged in and force_logout_before_login is false, the function will return, as there is nothing to do. :param force_logout_before_login: Optional boolean parameter :return: None """ if self._logged_in: if force_logout_before_login: self.logout() else: # Nothing to do, user already logged in. return self._authorization_service.startAuthorizationFlow( force_logout_before_login) @pyqtProperty(str, notify=loginStateChanged) def userName(self): user_profile = self._authorization_service.getUserProfile() if not user_profile: return None return user_profile.username @pyqtProperty(str, notify=loginStateChanged) def profileImageUrl(self): user_profile = self._authorization_service.getUserProfile() if not user_profile: return None return user_profile.profile_image_url @pyqtProperty(str, notify=accessTokenChanged) def accessToken(self) -> Optional[str]: return self._authorization_service.getAccessToken() @pyqtProperty("QVariantMap", notify=loginStateChanged) def userProfile(self) -> Optional[Dict[str, Optional[str]]]: """None if no user is logged in otherwise the logged in user as a dict containing containing user_id, username and profile_image_url """ user_profile = self._authorization_service.getUserProfile() if not user_profile: return None return user_profile.__dict__ @pyqtProperty(str, notify=lastSyncDateTimeChanged) def lastSyncDateTime(self) -> str: return self._last_sync_str @pyqtProperty(bool, notify=manualSyncEnabledChanged) def manualSyncEnabled(self) -> bool: return self._manual_sync_enabled @pyqtSlot() @pyqtSlot(bool) def sync(self, user_initiated: bool = False) -> None: if user_initiated: self._setManualSyncEnabled(False) self._sync() @pyqtSlot() def popupOpened(self) -> None: self._setManualSyncEnabled(True) self._sync_state = SyncState.IDLE self.syncStateChanged.emit(self._sync_state) @pyqtSlot() def logout(self) -> None: if not self._logged_in: return # Nothing to do, user isn't logged in. self._authorization_service.deleteAuthData()
def test_userProfileException(): authorization_service = AuthorizationService(OAUTH_SETTINGS, Preferences()) authorization_service.initialize() authorization_service._parseJWT = MagicMock(side_effect=requests.exceptions.ConnectionError) assert authorization_service.getUserProfile() is None
def test_loginAndLogout() -> None: preferences = Preferences() authorization_service = AuthorizationService(OAUTH_SETTINGS, preferences) authorization_service.onAuthenticationError.emit = MagicMock() authorization_service.onAuthStateChanged.emit = MagicMock() authorization_service.initialize() mock_reply = Mock( ) # The user profile that the service should respond with. mock_reply.error = Mock(return_value=QNetworkReply.NetworkError.NoError) http_mock = Mock() http_mock.get = lambda url, headers_dict, callback, error_callback: callback( mock_reply) http_mock.readJSON = Mock( return_value={"data": { "user_id": "di_resu", "username": "******" }}) # Let the service think there was a successful response with patch( "UM.TaskManagement.HttpRequestManager.HttpRequestManager.getInstance", MagicMock(return_value=http_mock)): authorization_service._onAuthStateChanged(SUCCESSFUL_AUTH_RESPONSE) # Ensure that the error signal was not triggered assert authorization_service.onAuthenticationError.emit.call_count == 0 # Since we said that it went right this time, validate that we got a signal. assert authorization_service.onAuthStateChanged.emit.call_count == 1 with patch( "UM.TaskManagement.HttpRequestManager.HttpRequestManager.getInstance", MagicMock(return_value=http_mock)): def callback(profile): assert profile is not None authorization_service.getUserProfile(callback) assert authorization_service.getAccessToken() == "beep" # Check that we stored the authentication data, so next time the user won't have to log in again. assert preferences.getValue("test/auth_data") is not None # We're logged in now, also check if logging out works authorization_service.deleteAuthData() assert authorization_service.onAuthStateChanged.emit.call_count == 2 with patch( "UM.TaskManagement.HttpRequestManager.HttpRequestManager.getInstance", MagicMock(return_value=http_mock)): def callback(profile): assert profile is None authorization_service.getUserProfile(callback) # Ensure the data is gone after we logged out. assert preferences.getValue("test/auth_data") == "{}"
def test_refreshAccesTokenWithoutData(): authorization_service = AuthorizationService(OAUTH_SETTINGS, Preferences()) authorization_service.initialize() authorization_service.onAuthStateChanged.emit = MagicMock() authorization_service.refreshAccessToken() authorization_service.onAuthStateChanged.emit.assert_not_called()
def test_userProfileException(): authorization_service = AuthorizationService(OAUTH_SETTINGS, Preferences()) authorization_service.initialize() authorization_service._parseJWT = MagicMock( side_effect=requests.exceptions.ConnectionError) assert authorization_service.getUserProfile() is None
def test_storeAuthData(get_user_profile) -> None: preferences = Preferences() authorization_service = AuthorizationService(OAUTH_SETTINGS, preferences) authorization_service.initialize() # Write stuff to the preferences. authorization_service._storeAuthData(SUCCESSFUL_AUTH_RESPONSE) preference_value = preferences.getValue(OAUTH_SETTINGS.AUTH_DATA_PREFERENCE_KEY) # Check that something was actually put in the preferences assert preference_value is not None and preference_value != {} # Create a second auth service, so we can load the data. second_auth_service = AuthorizationService(OAUTH_SETTINGS, preferences) second_auth_service.initialize() second_auth_service.loadAuthDataFromPreferences() assert second_auth_service.getAccessToken() == SUCCESSFUL_AUTH_RESPONSE.access_token
def test_cleanAuthService() -> None: # Ensure that when setting up an AuthorizationService, no data is set. authorization_service = AuthorizationService(OAUTH_SETTINGS, Preferences()) authorization_service.initialize() assert authorization_service.getUserProfile() is None assert authorization_service.getAccessToken() is None
class Account(QObject): # Signal emitted when user logged in or out. loginStateChanged = pyqtSignal(bool) accessTokenChanged = pyqtSignal() def __init__(self, application: "CuraApplication", parent=None) -> None: super().__init__(parent) self._application = application self._error_message = None # type: Optional[Message] self._logged_in = False self._callback_port = 32118 self._oauth_root = UltimakerCloudAuthentication.CuraCloudAccountAPIRoot self._oauth_settings = OAuth2Settings( OAUTH_SERVER_URL=self._oauth_root, CALLBACK_PORT=self._callback_port, CALLBACK_URL="http://localhost:{}/callback".format( self._callback_port), CLIENT_ID="um----------------------------ultimaker_cura", CLIENT_SCOPES= "account.user.read drive.backup.read drive.backup.write packages.download " "packages.rating.read packages.rating.write connect.cluster.read connect.cluster.write " "cura.printjob.read cura.printjob.write cura.mesh.read cura.mesh.write", AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data", AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format( self._oauth_root), AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root)) self._authorization_service = AuthorizationService( self._oauth_settings) def initialize(self) -> None: self._authorization_service.initialize( self._application.getPreferences()) self._authorization_service.onAuthStateChanged.connect( self._onLoginStateChanged) self._authorization_service.onAuthenticationError.connect( self._onLoginStateChanged) self._authorization_service.accessTokenChanged.connect( self._onAccessTokenChanged) self._authorization_service.loadAuthDataFromPreferences() def _onAccessTokenChanged(self): self.accessTokenChanged.emit() ## Returns a boolean indicating whether the given authentication is applied against staging or not. @property def is_staging(self) -> bool: return "staging" in self._oauth_root @pyqtProperty(bool, notify=loginStateChanged) def isLoggedIn(self) -> bool: return self._logged_in def _onLoginStateChanged(self, logged_in: bool = False, error_message: Optional[str] = None) -> None: if error_message: if self._error_message: self._error_message.hide() self._error_message = Message(error_message, title=i18n_catalog.i18nc( "@info:title", "Login failed")) self._error_message.show() self._logged_in = False self.loginStateChanged.emit(False) return if self._logged_in != logged_in: self._logged_in = logged_in self.loginStateChanged.emit(logged_in) @pyqtSlot() def login(self) -> None: if self._logged_in: # Nothing to do, user already logged in. return self._authorization_service.startAuthorizationFlow() @pyqtProperty(str, notify=loginStateChanged) def userName(self): user_profile = self._authorization_service.getUserProfile() if not user_profile: return None return user_profile.username @pyqtProperty(str, notify=loginStateChanged) def profileImageUrl(self): user_profile = self._authorization_service.getUserProfile() if not user_profile: return None return user_profile.profile_image_url @pyqtProperty(str, notify=accessTokenChanged) def accessToken(self) -> Optional[str]: return self._authorization_service.getAccessToken() # Get the profile of the logged in user # @returns None if no user is logged in, a dict containing user_id, username and profile_image_url @pyqtProperty("QVariantMap", notify=loginStateChanged) def userProfile(self) -> Optional[Dict[str, Optional[str]]]: user_profile = self._authorization_service.getUserProfile() if not user_profile: return None return user_profile.__dict__ @pyqtSlot() def logout(self) -> None: if not self._logged_in: return # Nothing to do, user isn't logged in. self._authorization_service.deleteAuthData()
def test_storeAuthData(get_user_profile) -> None: preferences = Preferences() authorization_service = AuthorizationService(OAUTH_SETTINGS, preferences) authorization_service.initialize() # Write stuff to the preferences. authorization_service._storeAuthData(SUCCESFULL_AUTH_RESPONSE) preference_value = preferences.getValue( OAUTH_SETTINGS.AUTH_DATA_PREFERENCE_KEY) # Check that something was actually put in the preferences assert preference_value is not None and preference_value != {} # Create a second auth service, so we can load the data. second_auth_service = AuthorizationService(OAUTH_SETTINGS, preferences) second_auth_service.initialize() second_auth_service.loadAuthDataFromPreferences() assert second_auth_service.getAccessToken( ) == SUCCESFULL_AUTH_RESPONSE.access_token
class Account(QObject): # Signal emitted when user logged in or out. loginStateChanged = pyqtSignal(bool) def __init__(self, application: "CuraApplication", parent = None) -> None: super().__init__(parent) self._application = application self._error_message = None # type: Optional[Message] self._logged_in = False self._callback_port = 32118 self._oauth_root = UltimakerCloudAuthentication.CuraCloudAccountAPIRoot self._oauth_settings = OAuth2Settings( OAUTH_SERVER_URL= self._oauth_root, CALLBACK_PORT=self._callback_port, CALLBACK_URL="http://localhost:{}/callback".format(self._callback_port), CLIENT_ID="um----------------------------ultimaker_cura", CLIENT_SCOPES="account.user.read drive.backup.read drive.backup.write packages.download " "packages.rating.read packages.rating.write connect.cluster.read connect.cluster.write " "cura.printjob.read cura.printjob.write cura.mesh.read cura.mesh.write", AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data", AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format(self._oauth_root), AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root) ) self._authorization_service = AuthorizationService(self._oauth_settings) def initialize(self) -> None: self._authorization_service.initialize(self._application.getPreferences()) self._authorization_service.onAuthStateChanged.connect(self._onLoginStateChanged) self._authorization_service.onAuthenticationError.connect(self._onLoginStateChanged) self._authorization_service.loadAuthDataFromPreferences() ## Returns a boolean indicating whether the given authentication is applied against staging or not. @property def is_staging(self) -> bool: return "staging" in self._oauth_root @pyqtProperty(bool, notify=loginStateChanged) def isLoggedIn(self) -> bool: return self._logged_in def _onLoginStateChanged(self, logged_in: bool = False, error_message: Optional[str] = None) -> None: if error_message: if self._error_message: self._error_message.hide() self._error_message = Message(error_message, title = i18n_catalog.i18nc("@info:title", "Login failed")) self._error_message.show() self._logged_in = False self.loginStateChanged.emit(False) return if self._logged_in != logged_in: self._logged_in = logged_in self.loginStateChanged.emit(logged_in) @pyqtSlot() def login(self) -> None: if self._logged_in: # Nothing to do, user already logged in. return self._authorization_service.startAuthorizationFlow() @pyqtProperty(str, notify=loginStateChanged) def userName(self): user_profile = self._authorization_service.getUserProfile() if not user_profile: return None return user_profile.username @pyqtProperty(str, notify = loginStateChanged) def profileImageUrl(self): user_profile = self._authorization_service.getUserProfile() if not user_profile: return None return user_profile.profile_image_url @pyqtProperty(str, notify=loginStateChanged) def accessToken(self) -> Optional[str]: return self._authorization_service.getAccessToken() # Get the profile of the logged in user # @returns None if no user is logged in, a dict containing user_id, username and profile_image_url @pyqtProperty("QVariantMap", notify = loginStateChanged) def userProfile(self) -> Optional[Dict[str, Optional[str]]]: user_profile = self._authorization_service.getUserProfile() if not user_profile: return None return user_profile.__dict__ @pyqtSlot() def logout(self) -> None: if not self._logged_in: return # Nothing to do, user isn't logged in. self._authorization_service.deleteAuthData()