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()
示例#2
0
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
示例#4
0
 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)
示例#5
0
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
示例#6
0
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
示例#8
0
    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)
示例#9
0
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)
示例#10
0
 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())
示例#11
0
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")
示例#12
0
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))
示例#14
0
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'])