def setUp(self): super(ProfileViewsTestCase, self).setUp() self.user = db_user.get_or_create(1, 'iliekcomputers') db_user.agree_to_gdpr(self.user['musicbrainz_id']) self.weirduser = db_user.get_or_create(2, 'weird\\user name') db_user.agree_to_gdpr(self.weirduser['musicbrainz_id']) self.service = SpotifyService()
def music_services_details(): spotify_service = SpotifyService() spotify_user = spotify_service.get_user(current_user.id) if spotify_user: permissions = set(spotify_user["scopes"]) if permissions == set(SPOTIFY_IMPORT_PERMISSIONS): current_spotify_permissions = "import" elif permissions == set(SPOTIFY_LISTEN_PERMISSIONS): current_spotify_permissions = "listen" else: current_spotify_permissions = "both" else: current_spotify_permissions = "disable" youtube_service = YoutubeService() youtube_user = youtube_service.get_user(current_user.id) current_youtube_permissions = "listen" if youtube_user else "disable" return render_template( 'user/music_services.html', spotify_user=spotify_user, current_spotify_permissions=current_spotify_permissions, youtube_user=youtube_user, current_youtube_permissions=current_youtube_permissions)
def process_all_spotify_users(): """ Get a batch of users to be processed and import their Spotify plays. Returns: (success, failure) where success: the number of users whose plays were successfully imported. failure: the number of users for whom we faced errors while importing. """ global _listens_imported_since_start, _metric_submission_time service = SpotifyService() try: users = service.get_active_users_to_process() except DatabaseException as e: current_app.logger.error('Cannot get list of users due to error %s', str(e), exc_info=True) return 0, 0 if not users: return 0, 0 current_app.logger.info('Process %d users...' % len(users)) success = 0 failure = 0 for u in users: try: _listens_imported_since_start += process_one_user(u, service) success += 1 except ExternalServiceError as e: current_app.logger.critical( 'spotify_reader could not import listens: %s', str(e), exc_info=True) failure += 1 except Exception as e: current_app.logger.critical( 'spotify_reader could not import listens: %s', str(e), exc_info=True) failure += 1 if time.monotonic() > _metric_submission_time: _metric_submission_time += METRIC_UPDATE_INTERVAL metrics.set("spotify_reader", imported_listens=_listens_imported_since_start) current_app.logger.info('Processed %d users successfully!', success) current_app.logger.info('Encountered errors while processing %d users.', failure) return success, failure
def setUp(self): super(SpotifyServiceTestCase, self).setUp() self.user_id = db_user.create(312, 'spotify_user') self.service = SpotifyService() self.service.add_new_user( self.user_id, { 'access_token': 'old-token', 'refresh_token': 'old-refresh-token', 'expires_in': 3600, 'scope': 'user-read-currently-playing user-read-recently-played' }) self.spotify_user = self.service.get_user(self.user_id)
def process_all_spotify_users(): """ Get a batch of users to be processed and import their Spotify plays. Returns: (success, failure) where success: the number of users whose plays were successfully imported. failure: the number of users for whom we faced errors while importing. """ service = SpotifyService() try: users = service.get_active_users_to_process() except DatabaseException as e: current_app.logger.error('Cannot get list of users due to error %s', str(e), exc_info=True) return 0, 0 if not users: return 0, 0 current_app.logger.info('Process %d users...' % len(users)) success = 0 failure = 0 for u in users: try: process_one_user(u, service) success += 1 except ExternalServiceError as e: current_app.logger.critical( 'spotify_reader could not import listens: %s', str(e), exc_info=True) failure += 1 except Exception as e: current_app.logger.critical( 'spotify_reader could not import listens: %s', str(e), exc_info=True) failure += 1 current_app.logger.info('Processed %d users successfully!', success) current_app.logger.info('Encountered errors while processing %d users.', failure) return success, failure
def get_current_spotify_user(): """Returns the spotify access token and permissions for the current authenticated user. If the user is not authenticated or has not linked to a Spotify account, returns empty dict.""" if not current_user.is_authenticated: return {} user = SpotifyService().get_user(current_user.id) if user is None: return {} return {"access_token": user["access_token"], "permission": user["scopes"]}
def test_user_oauth_token_has_expired(self): service = SpotifyService() # has expired user = { 'token_expires': datetime.datetime(2021, 5, 12, 3, 0, 40, tzinfo=datetime.timezone.utc) } assert service.user_oauth_token_has_expired(user) is True # expires within the 5 minute threshold user = { 'token_expires': datetime.datetime(2021, 5, 12, 3, 24, 40, tzinfo=datetime.timezone.utc) } assert service.user_oauth_token_has_expired(user) is True # hasn't expired user = { 'token_expires': datetime.datetime(2021, 5, 12, 4, 1, 40, tzinfo=datetime.timezone.utc) } assert service.user_oauth_token_has_expired(user) is False
def test_spotipy_methods_are_called_with_correct_params( self, mock_spotipy): mock_spotipy.return_value.current_user_playing_track.return_value = None with listenbrainz.webserver.create_app().app_context(): SpotifyService().update_latest_listen_ts( self.user['id'], int(datetime(2014, 5, 13, 16, 53, 20).timestamp())) spotify_read_listens.process_all_spotify_users() mock_spotipy.return_value.current_user_playing_track.assert_called_once( ) mock_spotipy.return_value.current_user_recently_played.assert_called_once_with( limit=50, after=1400000000000)
def _get_service_or_raise_404(name: str) -> ExternalService: """Returns the music service for the given name and raise 404 if service is not found Args: name (str): Name of the service """ try: service = ExternalServiceType[name.upper()] if service == ExternalServiceType.YOUTUBE: return YoutubeService() elif service == ExternalServiceType.SPOTIFY: return SpotifyService() except KeyError: raise NotFound("Service %s is invalid." % name)
def process_one_user(self, mock_refresh_user_token): mock_refresh_user_token.side_effect = ExternalServiceInvalidGrantError expired_token_spotify_user = dict( user_id=1, musicbrainz_id='spotify_user', musicbrainz_row_id=312, access_token='old-token', token_expires=int(time.time()), refresh_token='old-refresh-token', last_updated=None, latest_listened_at=None, scopes=['user-read-recently-played'], ) with self.assertRaises(ExternalServiceInvalidGrantError): spotify_read_listens.process_one_user(expired_token_spotify_user, SpotifyService())
def process_one_user(user: dict, service: SpotifyService) -> int: """ Get recently played songs for this user and submit them to ListenBrainz. Args: user (spotify.Spotify): the user whose plays are to be imported. service (listenbrainz.domain.spotify.SpotifyService): service to process users Raises: spotify.SpotifyAPIError: if we encounter errors from the Spotify API. spotify.SpotifyListenBrainzError: if we encounter a rate limit, even after retrying. or if we get errors while submitting the data to ListenBrainz Returns: the number of recently played listens imported for the user """ try: if service.user_oauth_token_has_expired(user): user = service.refresh_access_token(user['user_id'], user['refresh_token']) listens = [] latest_listened_at = None # If there is no playback, currently_playing will be None. # There are two playing types, track and episode. We use only the # track type. Therefore, when the user's playback type is not a track, # Spotify will set the item field to null which becomes None after # parsing the JSON. Due to these reasons, we cannot simplify the # checks below. currently_playing = get_user_currently_playing(user) if currently_playing is not None: currently_playing_item = currently_playing.get('item', None) if currently_playing_item is not None: current_app.logger.debug( 'Received a currently playing track for %s', str(user)) listens, latest_listened_at = parse_and_validate_spotify_plays( [currently_playing_item], LISTEN_TYPE_PLAYING_NOW) if listens: submit_listens_to_listenbrainz( user, listens, listen_type=LISTEN_TYPE_PLAYING_NOW) recently_played = get_user_recently_played(user) if recently_played is not None and 'items' in recently_played: listens, latest_listened_at = parse_and_validate_spotify_plays( recently_played['items'], LISTEN_TYPE_IMPORT) current_app.logger.debug('Received %d tracks for %s', len(listens), str(user)) # if we don't have any new listens, return. we don't check whether the listens list is empty here # because it will empty in both cases where we don't receive any listens and when we receive only # bad listens. so instead we check latest_listened_at which is None only in case when we received # nothing from spotify. if latest_listened_at is None: service.update_user_import_status(user['user_id']) return 0 submit_listens_to_listenbrainz(user, listens, listen_type=LISTEN_TYPE_IMPORT) # we've succeeded so update the last_updated and latest_listened_at field for this user service.update_latest_listen_ts(user['user_id'], latest_listened_at) current_app.logger.info('imported %d listens for %s' % (len(listens), str(user['musicbrainz_id']))) return len(listens) except ExternalServiceInvalidGrantError: error_message = "It seems like you've revoked permission for us to read your spotify account" service.update_user_import_status(user_id=user['user_id'], error=error_message) if not current_app.config['TESTING']: notify_error(user['musicbrainz_id'], error_message) # user has revoked authorization through spotify ui or deleted their spotify account etc. # in any of these cases, we should delete the user's token from. service.revoke_user(user['user_id']) raise ExternalServiceError("User has revoked spotify authorization") except ExternalServiceAPIError as e: # if it is an error from the Spotify API, show the error message to the user service.update_user_import_status(user_id=user['user_id'], error=str(e)) if not current_app.config['TESTING']: notify_error(user['musicbrainz_id'], str(e)) raise ExternalServiceError("Could not refresh user token from spotify")
def make_api_request(user: dict, endpoint: str, **kwargs): """ Make an request to the Spotify API for particular user at specified endpoint with args. Args: user: the user whose plays are to be imported. endpoint: the name of Spotipy function which makes request to the required API endpoint Returns: the response from the spotify API Raises: ExternalServiceAPIError: if we encounter errors from the Spotify API. ExternalServiceError: if we encounter a rate limit, even after retrying. """ retries = 10 delay = 1 tried_to_refresh_token = False while retries > 0: try: spotipy_client = spotipy.Spotify(auth=user['access_token']) spotipy_call = getattr(spotipy_client, endpoint) recently_played = spotipy_call(**kwargs) break except (AttributeError, TypeError): current_app.logger.critical( "Invalid spotipy endpoint or arguments:", exc_info=True) return None except SpotifyException as e: retries -= 1 if e.http_status == 429: # Rate Limit Problems -- the client handles these, but it can still give up # after a certain number of retries, so we look at the header and try the # request again, if the error is raised try: time_to_sleep = int(e.headers.get('Retry-After', delay)) except ValueError: time_to_sleep = delay current_app.logger.warn( 'Encountered a rate limit, sleeping %d seconds and trying again...', time_to_sleep) time.sleep(time_to_sleep) delay += 1 if retries == 0: raise ExternalServiceError('Encountered a rate limit.') elif e.http_status in (400, 403): current_app.logger.critical( 'Error from the Spotify API for user %s: %s', user['musicbrainz_id'], str(e), exc_info=True) raise ExternalServiceAPIError( 'Error from the Spotify API while getting listens: %s', str(e)) elif e.http_status >= 500 and e.http_status < 600: # these errors are not our fault, most probably. so just log them and retry. current_app.logger.error( 'Error while trying to get listens for user %s: %s', user['musicbrainz_id'], str(e), exc_info=True) if retries == 0: raise ExternalServiceAPIError( 'Error from the spotify API while getting listens: %s', str(e)) elif e.http_status == 401: # if we get 401 Unauthorized from Spotify, that means our token might have expired. # In that case, try to refresh the token, if there is an error even while refreshing # give up and report to the user. # We only try to refresh the token once, if we still get 401 after that, we give up. if not tried_to_refresh_token: user = SpotifyService().refresh_access_token( user['user_id'], user['refresh_token']) tried_to_refresh_token = True else: raise ExternalServiceAPIError( 'Could not authenticate with Spotify, please unlink and link your account again.' ) elif e.http_status == 404: current_app.logger.error( "404 while trying to get listens for user %s", str(user), exc_info=True) if retries == 0: raise ExternalServiceError( "404 while trying to get listens for user %s" % str(user)) except Exception as e: retries -= 1 current_app.logger.error( 'Unexpected error while getting listens: %s', str(e), exc_info=True) if retries == 0: raise ExternalServiceError( 'Unexpected error while getting listens: %s' % str(e)) return recently_played
class ProfileViewsTestCase(IntegrationTestCase): def setUp(self): super(ProfileViewsTestCase, self).setUp() self.user = db_user.get_or_create(1, 'iliekcomputers') db_user.agree_to_gdpr(self.user['musicbrainz_id']) self.weirduser = db_user.get_or_create(2, 'weird\\user name') db_user.agree_to_gdpr(self.weirduser['musicbrainz_id']) self.service = SpotifyService() def test_profile_view(self): """Tests the user info view and makes sure auth token is present there""" self.temporary_login(self.user['login_id']) response = self.client.get(url_for('profile.info', user_name=self.user['musicbrainz_id'])) self.assertTemplateUsed('profile/info.html') self.assert200(response) self.assertIn(self.user['auth_token'], response.data.decode('utf-8')) def test_reset_import_timestamp(self): # we do a get request first to put the CSRF token in the flask global context # so that we can access it for using in the post request in the next step val = int(time.time()) listens_importer.update_latest_listened_at(self.user['id'], ExternalServiceType.LASTFM, val) self.temporary_login(self.user['login_id']) response = self.client.get(url_for('profile.reset_latest_import_timestamp')) self.assertTemplateUsed('profile/resetlatestimportts.html') self.assert200(response) response = self.client.post( url_for('profile.reset_latest_import_timestamp'), data={'csrf_token': g.csrf_token} ) self.assertStatus(response, 302) # should have redirected to the info page self.assertRedirects(response, url_for('profile.info')) ts = listens_importer.get_latest_listened_at(self.user['id'], ExternalServiceType.LASTFM) self.assertEqual(int(ts.strftime('%s')), 0) def test_user_info_not_logged_in(self): """Tests user info view when not logged in""" profile_info_url = url_for('profile.info') response = self.client.get(profile_info_url) self.assertStatus(response, 302) self.assertRedirects(response, url_for('login.index', next=profile_info_url)) def test_delete_listens(self): """Tests delete listens end point""" self.temporary_login(self.user['login_id']) # we do a get request first to put the CSRF token in the flask global context # so that we can access it for using in the post request in the next step delete_listens_url = url_for('profile.delete_listens') response = self.client.get(delete_listens_url) self.assert200(response) response = self.client.post(delete_listens_url, data={'csrf_token': g.csrf_token}) self.assertMessageFlashed("Successfully deleted listens for %s." % self.user['musicbrainz_id'], 'info') self.assertRedirects(response, url_for('user.profile', user_name=self.user['musicbrainz_id'])) def test_delete_listens_not_logged_in(self): """Tests delete listens view when not logged in""" delete_listens_url = url_for('profile.delete_listens') response = self.client.get(delete_listens_url) self.assertStatus(response, 302) self.assertRedirects(response, url_for('login.index', next=delete_listens_url)) response = self.client.post(delete_listens_url) self.assertStatus(response, 302) self.assertRedirects(response, url_for('login.index', next=delete_listens_url)) def test_delete_listens_csrf_token_not_provided(self): """Tests delete listens end point when auth token is missing""" self.temporary_login(self.user['login_id']) delete_listens_url = url_for('profile.delete_listens') response = self.client.get(delete_listens_url) self.assert200(response) response = self.client.post(delete_listens_url) self.assertMessageFlashed('Cannot delete listens due to error during authentication, please try again later.', 'error') self.assertRedirects(response, url_for('profile.info')) def test_delete_listens_invalid_csrf_token(self): """Tests delete listens end point when auth token is invalid""" self.temporary_login(self.user['login_id']) delete_listens_url = url_for('profile.delete_listens') response = self.client.get(delete_listens_url) self.assert200(response) response = self.client.post(delete_listens_url, data={'csrf_token': 'invalid-auth-token'}) self.assertMessageFlashed('Cannot delete listens due to error during authentication, please try again later.', 'error') self.assertRedirects(response, url_for('profile.info')) def test_music_services_details(self): self.temporary_login(self.user['login_id']) r = self.client.get(url_for('profile.music_services_details')) self.assert200(r) r = self.client.post(url_for('profile.music_services_disconnect', service_name='spotify')) self.assertStatus(r, 302) self.assertIsNone(self.service.get_user(self.user['id'])) @patch('listenbrainz.domain.spotify.SpotifyService.fetch_access_token') def test_spotify_callback(self, mock_fetch_access_token): mock_fetch_access_token.return_value = { 'access_token': 'token', 'refresh_token': 'refresh', 'expires_in': 3600, 'scope': '', } self.temporary_login(self.user['login_id']) r = self.client.get(url_for('profile.music_services_callback', service_name='spotify', code='code')) self.assertStatus(r, 302) mock_fetch_access_token.assert_called_once_with('code') user = self.service.get_user(self.user['id']) self.assertEqual(self.user['id'], user['user_id']) self.assertEqual('token', user['access_token']) self.assertEqual('refresh', user['refresh_token']) r = self.client.get(url_for('profile.music_services_callback', service_name='spotify')) self.assert400(r) def test_spotify_refresh_token_logged_out(self): r = self.client.post(url_for('profile.refresh_service_token', service_name='spotify')) self.assert401(r) def test_spotify_refresh_token_no_token(self): self.temporary_login(self.user['login_id']) r = self.client.post(url_for('profile.refresh_service_token', service_name='spotify')) self.assert404(r) def _create_spotify_user(self, expired): offset = -1000 if expired else 1000 expires = int(time.time()) + offset db_oauth.save_token(user_id=self.user['id'], service=ExternalServiceType.SPOTIFY, access_token='old-token', refresh_token='old-refresh-token', token_expires_ts=expires, record_listens=False, scopes=['user-read-recently-played', 'some-other-permission']) @patch('listenbrainz.domain.spotify.SpotifyService.refresh_access_token') def test_spotify_refresh_token_which_has_not_expired(self, mock_refresh_access_token): self.temporary_login(self.user['login_id']) self._create_spotify_user(expired=False) r = self.client.post(url_for('profile.refresh_service_token', service_name='spotify')) self.assert200(r) mock_refresh_access_token.assert_not_called() self.assertDictEqual(r.json, {'access_token': 'old-token'}) @requests_mock.Mocker() def test_spotify_refresh_token_which_has_expired(self, mock_requests): self.temporary_login(self.user['login_id']) self._create_spotify_user(expired=True) mock_requests.post(OAUTH_TOKEN_URL, status_code=200, json={ 'access_token': 'new-token', 'refresh_token': 'refreshtokentoken', 'expires_in': 3600, 'scope': 'user-read-recently-played some-other-permission', }) r = self.client.post(url_for('profile.refresh_service_token', service_name='spotify')) self.assert200(r) self.assertDictEqual(r.json, {'access_token': 'new-token'}) @patch('listenbrainz.domain.spotify.SpotifyService.refresh_access_token') def test_spotify_refresh_token_which_has_been_revoked(self, mock_refresh_user_token): self.temporary_login(self.user['login_id']) self._create_spotify_user(expired=True) mock_refresh_user_token.side_effect = ExternalServiceInvalidGrantError response = self.client.post(url_for('profile.refresh_service_token', service_name='spotify')) self.assertEqual(response.json, {'code': 404, 'error': 'User has revoked authorization to Spotify'}) @patch('listenbrainz.listenstore.timescale_listenstore.TimescaleListenStore.fetch_listens') def test_export_streaming(self, mock_fetch_listens): self.temporary_login(self.user['login_id']) # Three example listens, with only basic data for the purpose of this test. # In each listen, one of {release_artist, release_msid, recording_msid} # is missing. listens = [ Listen( timestamp=1539509881, artist_msid='61746abb-76a5-465d-aee7-c4c42d61b7c4', recording_msid='6c617681-281e-4dae-af59-8e00f93c4376', data={ 'artist_name': 'Massive Attack', 'track_name': 'The Spoils', 'additional_info': {}, }, ), Listen( timestamp=1539441702, release_msid='0c1d2dc3-3704-4e75-92f9-940801a1eebd', recording_msid='7ad53fd7-5b40-4e13-b680-52716fb86d5f', data={ 'artist_name': 'Snow Patrol', 'track_name': 'Lifening', 'additional_info': {}, }, ), Listen( timestamp=1539441531, release_msid='7816411a-2cc6-4e43-b7a1-60ad093c2c31', artist_msid='7e2c6fe4-3e3f-496e-961d-dce04a44f01b', data={ 'artist_name': 'Muse', 'track_name': 'Drones', 'additional_info': {}, }, ), ] # We expect three calls to fetch_listens, and we return two, one, and # zero listens in the batch. This tests that we fetch all batches. mock_fetch_listens.side_effect = [(listens[0:2], 0, 0), (listens[2:3], 0, 0), ([], 0, 0)] r = self.client.post(url_for('profile.export_data')) self.assert200(r) # r.json returns None, so we decode the response manually. results = ujson.loads(r.data.decode('utf-8')) self.assertDictEqual(results[0], { 'inserted_at': 0, 'listened_at': 1539509881, 'recording_msid': '6c617681-281e-4dae-af59-8e00f93c4376', 'user_name': None, 'track_metadata': { 'artist_name': 'Massive Attack', 'track_name': 'The Spoils', 'additional_info': { 'artist_msid': '61746abb-76a5-465d-aee7-c4c42d61b7c4', 'release_msid': None, }, }, }) self.assertDictEqual(results[1], { 'inserted_at': 0, 'listened_at': 1539441702, 'recording_msid': '7ad53fd7-5b40-4e13-b680-52716fb86d5f', 'user_name': None, 'track_metadata': { 'artist_name': 'Snow Patrol', 'track_name': 'Lifening', 'additional_info': { 'artist_msid': None, 'release_msid': '0c1d2dc3-3704-4e75-92f9-940801a1eebd', }, }, }) self.assertDictEqual(results[2], { 'inserted_at': 0, 'listened_at': 1539441531, 'recording_msid': None, 'user_name': None, 'track_metadata': { 'artist_name': 'Muse', 'track_name': 'Drones', 'additional_info': { 'artist_msid': '7e2c6fe4-3e3f-496e-961d-dce04a44f01b', 'release_msid': '7816411a-2cc6-4e43-b7a1-60ad093c2c31', }, }, }) @patch('listenbrainz.db.feedback.get_feedback_for_user') def test_export_feedback_streaming(self, mock_fetch_feedback): self.temporary_login(self.user['login_id']) # Three example feedback, with only basic data for the purpose of this test. feedback = [ Feedback( recording_msid='6c617681-281e-4dae-af59-8e00f93c4376', score=1, user_id=1, ), Feedback( recording_msid='7ad53fd7-5b40-4e13-b680-52716fb86d5f', score=1, user_id=1, ), Feedback( recording_msid='7816411a-2cc6-4e43-b7a1-60ad093c2c31', score=-1, user_id=1, ), ] # We expect three calls to get_feedback_for_user, and we return two, one, and # zero feedback in the batch. This tests that we fetch all batches. mock_fetch_feedback.side_effect = [feedback[0:2], feedback[2:3], []] r = self.client.post(url_for('profile.export_feedback')) self.assert200(r) # r.json returns None, so we decode the response manually. results = ujson.loads(r.data.decode('utf-8')) self.assertDictEqual(results[0], { 'recording_mbid': None, 'recording_msid': '6c617681-281e-4dae-af59-8e00f93c4376', 'score': 1, 'user_id': None, 'created': None, 'track_metadata': None, }) self.assertDictEqual(results[1], { 'recording_mbid': None, 'recording_msid': '7ad53fd7-5b40-4e13-b680-52716fb86d5f', 'score': 1, 'user_id': None, 'created': None, 'track_metadata': None, }) self.assertDictEqual(results[2], { 'recording_mbid': None, 'recording_msid': '7816411a-2cc6-4e43-b7a1-60ad093c2c31', 'score': -1, 'user_id': None, 'created': None, 'track_metadata': None, }) def test_export_feedback_streaming_not_logged_in(self): export_feedback_url = url_for('profile.export_feedback') response = self.client.post(export_feedback_url) self.assertStatus(response, 302) self.assertRedirects(response, url_for('login.index', next=export_feedback_url))
class SpotifyServiceTestCase(IntegrationTestCase): def setUp(self): super(SpotifyServiceTestCase, self).setUp() self.user_id = db_user.create(312, 'spotify_user') self.service = SpotifyService() self.service.add_new_user( self.user_id, { 'access_token': 'old-token', 'refresh_token': 'old-refresh-token', 'expires_in': 3600, 'scope': 'user-read-currently-playing user-read-recently-played' }) self.spotify_user = self.service.get_user(self.user_id) def test_get_active_users(self): user_id_1 = db_user.create(333, 'user-1') user_id_2 = db_user.create(666, 'user-2') user_id_3 = db_user.create(999, 'user-3') self.service.add_new_user( user_id_2, { 'access_token': 'access-token', 'refresh_token': 'refresh-token', 'expires_in': 3600, 'scope': 'streaming', }) self.service.add_new_user( user_id_3, { 'access_token': 'access-token999', 'refresh_token': 'refresh-token999', 'expires_in': 3600, 'scope': 'user-read-currently-playing user-read-recently-played', }) self.service.update_user_import_status( user_id_3, error="add an error and check this user doesn't get selected") self.service.add_new_user( user_id_1, { 'access_token': 'access-token333', 'refresh_token': 'refresh-token333', 'expires_in': 3600, 'scope': 'user-read-currently-playing user-read-recently-played', }) self.service.update_latest_listen_ts(user_id_1, int(time.time())) active_users = self.service.get_active_users_to_process() self.assertEqual(len(active_users), 2) self.assertEqual(active_users[0]['user_id'], user_id_1) self.assertEqual(active_users[1]['user_id'], self.user_id) def test_update_latest_listened_at(self): t = int(time.time()) self.service.update_latest_listen_ts(self.user_id, t) user = self.service.get_user_connection_details(self.user_id) self.assertEqual(datetime.fromtimestamp(t, tz=timezone.utc), user['latest_listened_at']) # apparently, requests_mocker does not follow the usual order in which decorators are applied. :-( @requests_mock.Mocker() @mock.patch('time.time') def test_refresh_user_token(self, mock_requests, mock_time): mock_time.return_value = 0 mock_requests.post(OAUTH_TOKEN_URL, status_code=200, json={ 'access_token': 'tokentoken', 'refresh_token': 'refreshtokentoken', 'expires_in': 3600, 'scope': '', }) user = self.service.refresh_access_token( self.user_id, self.spotify_user['refresh_token']) self.assertEqual(self.user_id, user['user_id']) self.assertEqual('tokentoken', user['access_token']) self.assertEqual('refreshtokentoken', user['refresh_token']) self.assertEqual(datetime.fromtimestamp(3600, tz=timezone.utc), user['token_expires']) @requests_mock.Mocker() @mock.patch('time.time') def test_refresh_user_token_only_access(self, mock_requests, mock_time): mock_time.return_value = 0 mock_requests.post(OAUTH_TOKEN_URL, status_code=200, json={ 'access_token': 'tokentoken', 'expires_in': 3600, 'scope': '', }) user = self.service.refresh_access_token( self.user_id, self.spotify_user['refresh_token']) self.assertEqual(self.user_id, user['user_id']) self.assertEqual('tokentoken', user['access_token']) self.assertEqual('old-refresh-token', user['refresh_token']) self.assertEqual(datetime.fromtimestamp(3600, tz=timezone.utc), user['token_expires']) @requests_mock.Mocker() def test_refresh_user_token_bad(self, mock_requests): mock_requests.post(OAUTH_TOKEN_URL, status_code=400, json={ 'error': 'invalid request', 'error_description': 'invalid refresh token', }) with self.assertRaises(ExternalServiceAPIError): self.service.refresh_access_token( self.user_id, self.spotify_user['refresh_token']) # apparently, requests_mocker does not follow the usual order in which decorators are applied. :-( @requests_mock.Mocker() def test_refresh_user_token_revoked(self, mock_requests): mock_requests.post(OAUTH_TOKEN_URL, status_code=400, json={ 'error': 'invalid_grant', 'error_description': 'Refresh token revoked', }) with self.assertRaises(ExternalServiceInvalidGrantError): self.service.refresh_access_token( self.user_id, self.spotify_user['refresh_token']) def test_remove_user(self): self.service.remove_user(self.user_id) self.assertIsNone(self.service.get_user(self.user_id)) def test_get_user(self): user = self.service.get_user(self.user_id) self.assertIsInstance(user, dict) self.assertEqual(user['user_id'], self.user_id) self.assertEqual(user['musicbrainz_id'], 'spotify_user') self.assertEqual(user['access_token'], 'old-token') self.assertEqual(user['refresh_token'], 'old-refresh-token') self.assertIsNotNone(user['last_updated']) @mock.patch('time.time') def test_add_new_user(self, mock_time): mock_time.return_value = 0 self.service.remove_user(self.user_id) self.service.add_new_user( self.user_id, { 'access_token': 'access-token', 'refresh_token': 'refresh-token', 'expires_in': 3600, 'scope': '', }) user = self.service.get_user(self.user_id) self.assertEqual(self.user_id, user['user_id']) self.assertEqual('access-token', user['access_token']) self.assertEqual('refresh-token', user['refresh_token'])