Exemplo n.º 1
0
def fxa_verified(data):
    """Add new FxA users to an SFMC data extension"""
    # used to be handled by the fxa_register view
    email = data['email']
    fxa_id = data['uid']
    locale = data.get('locale')
    subscribe = data.get('marketingOptIn')
    metrics = data.get('metricsContext', {})

    if not locale:
        statsd.incr('fxa_verified.ignored.no_locale')
        return

    # if we're not using the sandbox ignore testing domains
    if email_is_testing(email):
        return

    lang = get_best_language(get_accept_languages(locale))
    if not lang:
        return

    _update_fxa_info(email, lang, fxa_id)

    if subscribe:
        upsert_user.delay(
            SUBSCRIBE, {
                'email': email,
                'lang': lang,
                'newsletters': settings.FXA_REGISTER_NEWSLETTER,
                'source_url': fxa_source_url(metrics),
            })
Exemplo n.º 2
0
def fxa_verified(data):
    """Add new FxA users to an SFMC data extension"""
    # used to be handled by the fxa_register view
    email = data['email']
    fxa_id = data['uid']
    create_date = data.get('createDate')
    if create_date:
        create_date = datetime.fromtimestamp(create_date)

    locale = data.get('locale')
    subscribe = data.get('marketingOptIn')
    metrics = data.get('metricsContext', {})
    service = data.get('service', '')

    if not locale:
        statsd.incr('fxa_verified.ignored.no_locale')
        return

    # if we're not using the sandbox ignore testing domains
    if email_is_testing(email):
        return

    lang = get_best_language(get_accept_languages(locale))
    if not lang:
        return

    _update_fxa_info(email, lang, fxa_id, service, create_date)

    if subscribe:
        upsert_user.delay(SUBSCRIBE, {
            'email': email,
            'lang': lang,
            'newsletters': settings.FXA_REGISTER_NEWSLETTER,
            'source_url': fxa_source_url(metrics),
        })
Exemplo n.º 3
0
def fxa_register(request):
    if settings.FXA_EVENTS_QUEUE_ENABLE:
        # When this setting is true these requests will be handled by
        # a queue via which we receive various events from FxA. See process_fxa_queue.py.
        # This is still here to avoid errors during the transition to said queue.
        # TODO remove after complete transistion to queue
        return HttpResponseJSON({'status': 'ok'})

    if not has_valid_api_key(request):
        return HttpResponseJSON(
            {
                'status': 'error',
                'desc': 'fxa-register requires a valid API-key',
                'code': errors.BASKET_AUTH_ERROR,
            }, 401)

    data = request.POST.dict()
    if 'email' not in data:
        return HttpResponseJSON(
            {
                'status': 'error',
                'desc': 'fxa-register requires an email address',
                'code': errors.BASKET_USAGE_ERROR,
            }, 401)

    email = process_email(data['email'])
    if not email:
        return invalid_email_response()

    if 'fxa_id' not in data:
        return HttpResponseJSON(
            {
                'status': 'error',
                'desc': 'fxa-register requires a Firefox Account ID',
                'code': errors.BASKET_USAGE_ERROR,
            }, 401)
    if 'accept_lang' not in data:
        return HttpResponseJSON(
            {
                'status': 'error',
                'desc': 'fxa-register requires accept_lang',
                'code': errors.BASKET_USAGE_ERROR,
            }, 401)

    lang = get_best_language(get_accept_languages(data['accept_lang']))
    if lang is None:
        return HttpResponseJSON(
            {
                'status': 'error',
                'desc': 'invalid language',
                'code': errors.BASKET_INVALID_LANGUAGE,
            }, 400)

    update_fxa_info.delay(email, lang, data['fxa_id'])
    return HttpResponseJSON({'status': 'ok'})
Exemplo n.º 4
0
def fxa_verified(data):
    """Add new FxA users to an SFMC data extension"""
    # used to be handled by the fxa_register view
    email = data['email']
    fxa_id = data['uid']
    create_date = data.get('createDate')
    if create_date:
        create_date = datetime.fromtimestamp(create_date)

    locale = data.get('locale')
    subscribe = data.get('marketingOptIn')
    newsletters = data.get('newsletters')
    metrics = data.get('metricsContext', {})
    service = data.get('service', '')
    country = data.get('countryCode', '')

    if not locale:
        statsd.incr('fxa_verified.ignored.no_locale')
        return

    # if we're not using the sandbox ignore testing domains
    if email_is_testing(email):
        return

    lang = get_best_language(get_accept_languages(locale))
    if not lang:
        return

    _update_fxa_info(email, lang, fxa_id, service, create_date)

    add_news = None
    if newsletters:
        if settings.FXA_REGISTER_NEWSLETTER not in newsletters:
            newsletters.append(settings.FXA_REGISTER_NEWSLETTER)

        add_news = ','.join(newsletters)
    elif subscribe:
        add_news = settings.FXA_REGISTER_NEWSLETTER

    if add_news:
        upsert_user.delay(
            SUBSCRIBE, {
                'email': email,
                'lang': lang,
                'newsletters': add_news,
                'source_url': fxa_source_url(metrics),
                'country': country,
            })
    else:
        record_source_url(email, fxa_source_url(metrics), 'fxa-no-optin')
Exemplo n.º 5
0
Arquivo: views.py Projeto: pmac/basket
def fxa_register(request):
    if settings.FXA_EVENTS_QUEUE_ENABLE:
        # When this setting is true these requests will be handled by
        # a queue via which we receive various events from FxA. See process_fxa_queue.py.
        # This is still here to avoid errors during the transition to said queue.
        # TODO remove after complete transistion to queue
        return HttpResponseJSON({'status': 'ok'})

    if not has_valid_api_key(request):
        return HttpResponseJSON({
            'status': 'error',
            'desc': 'fxa-register requires a valid API-key',
            'code': errors.BASKET_AUTH_ERROR,
        }, 401)

    data = request.POST.dict()
    if 'email' not in data:
        return HttpResponseJSON({
            'status': 'error',
            'desc': 'fxa-register requires an email address',
            'code': errors.BASKET_USAGE_ERROR,
        }, 401)

    email = process_email(data['email'])
    if not email:
        return invalid_email_response()

    if 'fxa_id' not in data:
        return HttpResponseJSON({
            'status': 'error',
            'desc': 'fxa-register requires a Firefox Account ID',
            'code': errors.BASKET_USAGE_ERROR,
        }, 401)
    if 'accept_lang' not in data:
        return HttpResponseJSON({
            'status': 'error',
            'desc': 'fxa-register requires accept_lang',
            'code': errors.BASKET_USAGE_ERROR,
        }, 401)

    lang = get_best_language(get_accept_languages(data['accept_lang']))
    if lang is None:
        return HttpResponseJSON({
            'status': 'error',
            'desc': 'invalid language',
            'code': errors.BASKET_INVALID_LANGUAGE,
        }, 400)

    update_fxa_info.delay(email, lang, data['fxa_id'])
    return HttpResponseJSON({'status': 'ok'})
Exemplo n.º 6
0
def fxa_verified(data):
    """Add new FxA users to SFDC"""
    # if we're not using the sandbox ignore testing domains
    if email_is_testing(data["email"]):
        return

    lang = get_best_language(get_accept_languages(data.get("locale")))
    if not lang or lang not in newsletter_languages():
        lang = "other"

    email = data["email"]
    fxa_id = data["uid"]
    create_date = data.get("createDate", data.get("ts"))
    newsletters = data.get("newsletters")
    metrics = data.get("metricsContext", {})
    new_data = {
        "email": email,
        "source_url": fxa_source_url(metrics),
        "country": data.get("countryCode", ""),
        "fxa_lang": data.get("locale"),
        "fxa_service": data.get("service", ""),
        "fxa_id": fxa_id,
        "optin": True,
        "format": "H",
    }
    if create_date:
        new_data["fxa_create_date"] = iso_format_unix_timestamp(create_date)

    newsletters = newsletters or []
    newsletters.append(settings.FXA_REGISTER_NEWSLETTER)
    new_data["newsletters"] = newsletters

    user_data = get_fxa_user_data(fxa_id, email)
    # don't overwrite the user's language if already set
    if not (user_data and user_data.get("lang")):
        new_data["lang"] = lang

    upsert_contact(SUBSCRIBE, new_data, user_data)
Exemplo n.º 7
0
def fxa_callback(request):
    # remove state from session to prevent multiple attempts
    error_url = f"https://{settings.FXA_EMAIL_PREFS_DOMAIN}/newsletter/fxa-error/"
    sess_state = request.session.pop("fxa_state", None)
    if sess_state is None:
        statsd.incr("news.views.fxa_callback.error.no_state")
        return HttpResponseRedirect(error_url)

    code = request.GET.get("code")
    state = request.GET.get("state")
    if not (code and state):
        statsd.incr("news.views.fxa_callback.error.no_code_state")
        return HttpResponseRedirect(error_url)

    if sess_state != state:
        statsd.incr("news.views.fxa_callback.error.no_state_match")
        return HttpResponseRedirect(error_url)

    fxa_oauth, fxa_profile = get_fxa_clients()
    try:
        access_token = fxa_oauth.trade_code(
            code, ttl=settings.FXA_OAUTH_TOKEN_TTL)["access_token"]
        user_profile = fxa_profile.get_profile(access_token)
    except Exception:
        statsd.incr("news.views.fxa_callback.error.fxa_comm")
        sentry_sdk.capture_exception()
        return HttpResponseRedirect(error_url)

    email = user_profile["email"]
    try:
        user_data = get_user_data(email=email)
    except SalesforceError:
        statsd.incr("news.views.fxa_callback.error.get_user_data")
        sentry_sdk.capture_exception()
        return HttpResponseRedirect(error_url)

    if user_data:
        token = user_data["token"]
    else:
        new_user_data = {
            "email":
            email,
            "optin":
            True,
            "format":
            "H",
            "newsletters": [settings.FXA_REGISTER_NEWSLETTER],
            "source_url":
            f"{settings.FXA_REGISTER_SOURCE_URL}?utm_source=basket-fxa-oauth",
        }
        locale = user_profile.get("locale")
        if locale:
            new_user_data["fxa_lang"] = locale
            lang = get_best_language(get_accept_languages(locale))
            if lang not in newsletter_languages():
                lang = "other"

            new_user_data["lang"] = lang

        try:
            token = upsert_contact(SUBSCRIBE, new_user_data, None)[0]
        except SalesforceError:
            statsd.incr("news.views.fxa_callback.error.upsert_contact")
            sentry_sdk.capture_exception()
            return HttpResponseRedirect(error_url)

    redirect_to = (
        f"https://{settings.FXA_EMAIL_PREFS_DOMAIN}/newsletter/existing/{token}/?fxa=1"
    )
    return HttpResponseRedirect(redirect_to)
Exemplo n.º 8
0
def update_user_task(request,
                     api_call_type,
                     data=None,
                     optin=False,
                     sync=False):
    """Call the update_user task async with the right parameters.

    If sync==True, be sure to include the token in the response.
    Otherwise, basket can just do everything in the background.
    """
    data = data or request.POST.dict()

    newsletters = parse_newsletters_csv(data.get("newsletters"))
    if newsletters:
        if api_call_type == SUBSCRIBE:
            all_newsletters = (newsletter_and_group_slugs() +
                               get_transactional_message_ids())
        else:
            all_newsletters = newsletter_slugs()

        private_newsletters = newsletter_private_slugs()

        for nl in newsletters:
            if nl not in all_newsletters:
                return HttpResponseJSON(
                    {
                        "status": "error",
                        "desc": "invalid newsletter",
                        "code": errors.BASKET_INVALID_NEWSLETTER,
                    },
                    400,
                )

            if api_call_type != UNSUBSCRIBE and nl in private_newsletters:
                if not is_authorized(request, data.get("email")):
                    return HttpResponseJSON(
                        {
                            "status": "error",
                            "desc":
                            "private newsletter subscription requires a valid API key or OAuth",
                            "code": errors.BASKET_AUTH_ERROR,
                        },
                        401,
                    )

    if "lang" in data:
        if not language_code_is_valid(data["lang"]):
            data["lang"] = "en"
    elif "accept_lang" in data:
        lang = get_best_language(get_accept_languages(data["accept_lang"]))
        if lang:
            data["lang"] = lang
            del data["accept_lang"]
    # if lang not provided get the best one from the accept-language header
    else:
        lang = get_best_request_lang(request)
        if lang:
            data["lang"] = lang

    # now ensure that if we do have a lang that it's a supported one
    if "lang" in data:
        data["lang"] = get_best_supported_lang(data["lang"])

    email = data.get("email")
    token = data.get("token")
    if not (email or token):
        return HttpResponseJSON(
            {
                "status": "error",
                "desc": MSG_EMAIL_OR_TOKEN_REQUIRED,
                "code": errors.BASKET_USAGE_ERROR,
            },
            400,
        )

    if optin:
        data["optin"] = True

    if api_call_type == SUBSCRIBE and email and data.get("newsletters"):
        # only rate limit here so we don't rate limit errors.
        if is_ratelimited(
                request,
                group="basket.news.views.update_user_task.subscribe",
                key=lambda x, y: "%s-%s" % (data["newsletters"], email),
                rate=EMAIL_SUBSCRIBE_RATE_LIMIT,
                increment=True,
        ):
            raise Ratelimited()

    if api_call_type == SET and token and data.get("newsletters"):
        # only rate limit here so we don't rate limit errors.
        if is_ratelimited(
                request,
                group="basket.news.views.update_user_task.set",
                key=lambda x, y: "%s-%s" % (data["newsletters"], token),
                rate=EMAIL_SUBSCRIBE_RATE_LIMIT,
                increment=True,
        ):
            raise Ratelimited()

    if sync:
        statsd.incr("news.views.subscribe.sync")
        if settings.MAINTENANCE_MODE and not settings.MAINTENANCE_READ_ONLY:
            # save what we can
            upsert_user.delay(api_call_type, data, start_time=time())
            # have to error since we can't return a token
            return HttpResponseJSON(
                {
                    "status": "error",
                    "desc": "sync is not available in maintenance mode",
                    "code": errors.BASKET_NETWORK_FAILURE,
                },
                400,
            )

        try:
            user_data = get_user_data(email=email, token=token)
        except NewsletterException as e:
            return newsletter_exception_response(e)

        if not user_data:
            if not email:
                # must have email to create a user
                return HttpResponseJSON(
                    {
                        "status": "error",
                        "desc": MSG_EMAIL_OR_TOKEN_REQUIRED,
                        "code": errors.BASKET_USAGE_ERROR,
                    },
                    400,
                )

        token, created = upsert_contact(api_call_type, data, user_data)
        return HttpResponseJSON({
            "status": "ok",
            "token": token,
            "created": created
        })
    else:
        upsert_user.delay(api_call_type, data, start_time=time())
        return HttpResponseJSON({"status": "ok"})
Exemplo n.º 9
0
Arquivo: views.py Projeto: pmac/basket
def update_user_task(request, api_call_type, data=None, optin=False, sync=False):
    """Call the update_user task async with the right parameters.

    If sync==True, be sure to include the token in the response.
    Otherwise, basket can just do everything in the background.
    """
    data = data or request.POST.dict()

    newsletters = parse_newsletters_csv(data.get('newsletters'))
    if newsletters:
        if api_call_type == SUBSCRIBE:
            all_newsletters = newsletter_and_group_slugs() + get_transactional_message_ids()
        else:
            all_newsletters = newsletter_slugs()

        private_newsletters = newsletter_private_slugs()

        for nl in newsletters:
            if nl not in all_newsletters:
                return HttpResponseJSON({
                    'status': 'error',
                    'desc': 'invalid newsletter',
                    'code': errors.BASKET_INVALID_NEWSLETTER,
                }, 400)

            if api_call_type != UNSUBSCRIBE and nl in private_newsletters:
                if not has_valid_api_key(request):
                    return HttpResponseJSON({
                        'status': 'error',
                        'desc': 'private newsletter subscription requires a valid API key',
                        'code': errors.BASKET_AUTH_ERROR,
                    }, 401)

    if 'lang' in data:
        if not language_code_is_valid(data['lang']):
            data['lang'] = 'en'
    elif 'accept_lang' in data:
        lang = get_best_language(get_accept_languages(data['accept_lang']))
        if lang:
            data['lang'] = lang
            del data['accept_lang']
        else:
            data['lang'] = 'en'
    # if lang not provided get the best one from the accept-language header
    else:
        data['lang'] = get_best_request_lang(request) or 'en'

    email = data.get('email')
    token = data.get('token')
    if not (email or token):
        return HttpResponseJSON({
            'status': 'error',
            'desc': MSG_EMAIL_OR_TOKEN_REQUIRED,
            'code': errors.BASKET_USAGE_ERROR,
        }, 400)

    if optin:
        data['optin'] = True

    if api_call_type == SUBSCRIBE and email and data.get('newsletters'):
        # only rate limit here so we don't rate limit errors.
        if is_ratelimited(request, group='basket.news.views.update_user_task.subscribe',
                          key=lambda x, y: '%s-%s' % (data['newsletters'], email),
                          rate=EMAIL_SUBSCRIBE_RATE_LIMIT, increment=True):
            raise Ratelimited()

    if api_call_type == SET and token and data.get('newsletters'):
        # only rate limit here so we don't rate limit errors.
        if is_ratelimited(request, group='basket.news.views.update_user_task.set',
                          key=lambda x, y: '%s-%s' % (data['newsletters'], token),
                          rate=EMAIL_SUBSCRIBE_RATE_LIMIT, increment=True):
            raise Ratelimited()

    if sync:
        statsd.incr('news.views.subscribe.sync')
        if settings.MAINTENANCE_MODE and not settings.MAINTENANCE_READ_ONLY:
            # save what we can
            upsert_user.delay(api_call_type, data, start_time=time())
            # have to error since we can't return a token
            return HttpResponseJSON({
                'status': 'error',
                'desc': 'sync is not available in maintenance mode',
                'code': errors.BASKET_NETWORK_FAILURE,
            }, 400)

        try:
            user_data = get_user_data(email=email, token=token)
        except NewsletterException as e:
            return newsletter_exception_response(e)

        if not user_data:
            if not email:
                # must have email to create a user
                return HttpResponseJSON({
                    'status': 'error',
                    'desc': MSG_EMAIL_OR_TOKEN_REQUIRED,
                    'code': errors.BASKET_USAGE_ERROR,
                }, 400)

        token, created = upsert_contact(api_call_type, data, user_data)
        return HttpResponseJSON({
            'status': 'ok',
            'token': token,
            'created': created,
        })
    else:
        upsert_user.delay(api_call_type, data, start_time=time())
        return HttpResponseJSON({
            'status': 'ok',
        })
Exemplo n.º 10
0
def update_user_task(request, api_call_type, data=None, optin=False, sync=False):
    """Call the update_user task async with the right parameters.

    If sync==True, be sure to include the token in the response.
    Otherwise, basket can just do everything in the background.
    """
    data = data or request.POST.dict()

    newsletters = parse_newsletters_csv(data.get('newsletters'))
    if newsletters:
        if api_call_type == SUBSCRIBE:
            all_newsletters = newsletter_and_group_slugs() + get_transactional_message_ids()
        else:
            all_newsletters = newsletter_slugs()

        private_newsletters = newsletter_private_slugs()

        for nl in newsletters:
            if nl not in all_newsletters:
                return HttpResponseJSON({
                    'status': 'error',
                    'desc': 'invalid newsletter',
                    'code': errors.BASKET_INVALID_NEWSLETTER,
                }, 400)

            if api_call_type != UNSUBSCRIBE and nl in private_newsletters:
                if not is_authorized(request, data.get('email')):
                    return HttpResponseJSON({
                        'status': 'error',
                        'desc': 'private newsletter subscription requires a valid API key or OAuth',
                        'code': errors.BASKET_AUTH_ERROR,
                    }, 401)

    if 'lang' in data:
        if not language_code_is_valid(data['lang']):
            data['lang'] = 'en'
    elif 'accept_lang' in data:
        lang = get_best_language(get_accept_languages(data['accept_lang']))
        if lang:
            data['lang'] = lang
            del data['accept_lang']
    # if lang not provided get the best one from the accept-language header
    else:
        lang = get_best_request_lang(request)
        if lang:
            data['lang'] = lang

    email = data.get('email')
    token = data.get('token')
    if not (email or token):
        return HttpResponseJSON({
            'status': 'error',
            'desc': MSG_EMAIL_OR_TOKEN_REQUIRED,
            'code': errors.BASKET_USAGE_ERROR,
        }, 400)

    if optin:
        data['optin'] = True

    if api_call_type == SUBSCRIBE and email and data.get('newsletters'):
        # only rate limit here so we don't rate limit errors.
        if is_ratelimited(request, group='basket.news.views.update_user_task.subscribe',
                          key=lambda x, y: '%s-%s' % (data['newsletters'], email),
                          rate=EMAIL_SUBSCRIBE_RATE_LIMIT, increment=True):
            raise Ratelimited()

    if api_call_type == SET and token and data.get('newsletters'):
        # only rate limit here so we don't rate limit errors.
        if is_ratelimited(request, group='basket.news.views.update_user_task.set',
                          key=lambda x, y: '%s-%s' % (data['newsletters'], token),
                          rate=EMAIL_SUBSCRIBE_RATE_LIMIT, increment=True):
            raise Ratelimited()

    if sync:
        statsd.incr('news.views.subscribe.sync')
        if settings.MAINTENANCE_MODE and not settings.MAINTENANCE_READ_ONLY:
            # save what we can
            upsert_user.delay(api_call_type, data, start_time=time())
            # have to error since we can't return a token
            return HttpResponseJSON({
                'status': 'error',
                'desc': 'sync is not available in maintenance mode',
                'code': errors.BASKET_NETWORK_FAILURE,
            }, 400)

        try:
            user_data = get_user_data(email=email, token=token)
        except NewsletterException as e:
            return newsletter_exception_response(e)

        if not user_data:
            if not email:
                # must have email to create a user
                return HttpResponseJSON({
                    'status': 'error',
                    'desc': MSG_EMAIL_OR_TOKEN_REQUIRED,
                    'code': errors.BASKET_USAGE_ERROR,
                }, 400)

        token, created = upsert_contact(api_call_type, data, user_data)
        return HttpResponseJSON({
            'status': 'ok',
            'token': token,
            'created': created,
        })
    else:
        upsert_user.delay(api_call_type, data, start_time=time())
        return HttpResponseJSON({
            'status': 'ok',
        })
Exemplo n.º 11
0
def fxa_callback(request):
    # remove state from session to prevent multiple attempts
    error_url = f'https://{settings.FXA_EMAIL_PREFS_DOMAIN}/newsletter/fxa-error/'
    sess_state = request.session.pop('fxa_state', None)
    if sess_state is None:
        statsd.incr('news.views.fxa_callback.error.no_state')
        return HttpResponseRedirect(error_url)

    code = request.GET.get('code')
    state = request.GET.get('state')
    if not (code and state):
        statsd.incr('news.views.fxa_callback.error.no_code_state')
        return HttpResponseRedirect(error_url)

    if sess_state != state:
        statsd.incr('news.views.fxa_callback.error.no_state_match')
        return HttpResponseRedirect(error_url)

    fxa_oauth, fxa_profile = get_fxa_clients()
    try:
        access_token = fxa_oauth.trade_code(
            code, ttl=settings.FXA_OAUTH_TOKEN_TTL)['access_token']
        user_profile = fxa_profile.get_profile(access_token)
    except Exception:
        statsd.incr('news.views.fxa_callback.error.fxa_comm')
        sentry_client.captureException()
        return HttpResponseRedirect(error_url)

    email = user_profile['email']
    try:
        user_data = get_user_data(email=email)
    except SalesforceError:
        statsd.incr('news.views.fxa_callback.error.get_user_data')
        sentry_client.captureException()
        return HttpResponseRedirect(error_url)

    if user_data:
        token = user_data['token']
    else:
        new_user_data = {
            'email':
            email,
            'optin':
            True,
            'format':
            'H',
            'newsletters': [settings.FXA_REGISTER_NEWSLETTER],
            'source_url':
            f'{settings.FXA_REGISTER_SOURCE_URL}?utm_source=basket-fxa-oauth',
        }
        lang = user_profile.get('locale')
        if lang:
            lang = get_best_language(get_accept_languages(lang))
            new_user_data['lang'] = lang

        try:
            token = upsert_contact(SUBSCRIBE, new_user_data, None)[0]
        except SalesforceError:
            statsd.incr('news.views.fxa_callback.error.upsert_contact')
            sentry_client.captureException()
            return HttpResponseRedirect(error_url)

    redirect_to = f'https://{settings.FXA_EMAIL_PREFS_DOMAIN}/newsletter/existing/{token}/?fxa=1'
    return HttpResponseRedirect(redirect_to)
Exemplo n.º 12
0
 def _test(self, langs_list, expected_lang):
     self.assertEqual(get_best_language(langs_list), expected_lang)
Exemplo n.º 13
0
 def _test(self, langs_list, expected_lang):
     self.assertEqual(get_best_language(langs_list), expected_lang)