Тестирование Facebook авторизации с помощью Python

Facebook

Один из вариантов sign up, sign in - flow для веб сайта есть через свой аккаунт в #Facebook.
Это достаточно простой вариант упростить себе авторизацию на любой сайт.

Проблема

Во время одной регрессии мы нашли баг, который заключался в невозможности использовать этот вариант авторизации. Что сильно нас огорчило.
Решением это проблемы было - сам фикс бага и написание автотестов, которые проверяли этот флоу.

Пример авторизации

Я решил поделиться своим решение, да оно не идеальное и можно было бы его легко отрефакторить, но оно рабочее и времени всегда не хватает на это. Так что пока как есть…
Для того что бы тестировать авторизацию - вам нужно иметь facebook client secret и facebook client_id - найти их вы можете в настройке своего приложения в facebook.
Найти все эти настройки можно через https://developers.facebook.com/

Code

import pytest
from faker import Faker
import requests
from requests import HTTPError

fake = Faker()
FACEBOOK_URL = 'https://graph.facebook.com'
API_VERSION = 'v3.2'
USER_JSON = {'installed': False, 'permissions': 'email'}


class FacebookAPI(object):
    """
    API to create a tests users for a sign up flow testing
    https://developers.facebook.com/docs/graph-api/reference/v3.2/app/accounts/test-users
    Example of usage
    facebook = FacebookAPI()
    new_user = facebook.create_new_facebook_user()
    delete_user = facebook.delete_existing_user(new_user['id'])
    """

    def __init__(self, environment,
                 facebook_staging_client_secret,
                 facebook_staging_client_id,
                 facebook_prod_client_secret,
                 facebook_prod_client_id):
        self.environment = environment
        self.user_json = USER_JSON
        self.get_facebook_staging_client_secret = facebook_staging_client_secret
        self.get_facebook_staging_client_id = facebook_staging_client_id
        self.get_facebook_prod_client_secret = facebook_prod_client_secret
        self.get_facebook_prod_client_id = facebook_prod_client_id


    def facebook_api_environment_config(self):
        return {
            'staging': {
                'client_secret': self.get_facebook_staging_client_secret,
                'client_id': self.get_facebook_staging_client_id
            },
            'prod': {
                'client_secret': self.get_facebook_prod_client_secret,
                'client_id': self.get_facebook_prod_client_id
            }
        }

    def _get_env_config(self):
        config = self.facebook_api_environment_config()[self.environment]
        if config is None:
            raise ValueError('Could not find a Facebook config for environment {}'.format(self.environment))
        return config

    def access_token_url(self):
        return \
            '{api_base}/oauth/access_token?' \
            'client_id={client_id}' \
            '&client_secret={client_secret}' \
            '&grant_type=client_credentials' \
                .format(
                api_base=FACEBOOK_URL,
                client_id=self._get_env_config()['client_id'],
                client_secret=self._get_env_config()['client_secret'])

    def _create_access_token(self):
        """
        Create a access token for facebook api
        :return: access_token
        """
        create_access_token_url = self.access_token_url()
        req = requests.post(create_access_token_url)
        return req.json()['access_token']

    def create_new_facebook_user(self):
        """
        Create new test user
        :return: new user id, email, password, login_url
        """
        req = requests.post(
            FACEBOOK_URL + '/{api_version}/{client_id}/accounts/test-users?access_token={access_token}'.format(
                api_version=API_VERSION,
                client_id=self._get_env_config()['client_id'],
                access_token=self._create_access_token()
            ),
            data=self.user_json
        )
        if req.ok is False:
            raise HTTPError(req.text, response=req.status_code)
        else:
            return req.json()

    def delete_existing_user(self, uid):
        """
        Delete existing test user after tests
        :param uid: user with uid will be deleted
        :return: status code of delete
        """
        req = requests.delete(
            FACEBOOK_URL + '/' + API_VERSION + '/{uid}?access_token={access_token}'.format(
                uid=uid,
                access_token=self._create_access_token()))
        return req.status_code

Выводы

Легко использовать #Facebook #API для контроля качества авторизации через приложение на ваш сайт.

P.S.
Если нужен пример тестов - могу выложить их отдельным постом.

Любопытно выглядит :+1:, тем не менее не желая быть занудой, хотел бы указать на некоторые неточности кода:

        config = self.facebook_api_environment_config()[self.environment]
        if config is None:

Это некорректно: судя по facebook_api_environment_config не имеет ключа со значением None. А если self.environment содержит несуществующий ключ, то питон выкинет ошибку KeyError. Если действительно нужно получить None в config при несуществующем ключе, то правильно сделать так:

        config = self.facebook_api_environment_config().get(self.environment)
        if config is None:

И н-р if req.ok is False: – более по-питонистски будет if not req.ok:, ну это так, мелочь безусловно :slight_smile:

Кстати говоря, насчет конкатенации и форматирования строк, питон с v3.6 поддерживает f-strings, так что если есть возможность, то с ними красивее выглядит.

1 лайк

спасибо за критику)
давно хочу отрефакторить, но руки не доходили.

не за что) всегда интересно посмотреть код

1 лайк

Ну тогда и про джойны урлов можно было бы упомянуть)
Давно уже использую:

from urllib.parse import urljoin

И вам советую)
И сравнения типа

if config is None
if req.ok is False

замечательно смотрятся вот так:

if not config
if not req.ok

можно было, только вот urljoin не так прост, как кажется, и в целом конкатенация проще к пониманию, особенно если нет уверенности что твои коллеги понимают правильно urljoin и спецификацию формирования URL / URI :slight_smile:

In [6]: urljoin('http://my.com', '/my/path')                                                                           
Out[6]: 'http://my.com/my/path' # correct

In [7]: urljoin('http://my.com/init/part', '/my/path')                                                                 
Out[7]: 'http://my.com/my/path' # `init/part` is missed

In [11]: urljoin('http://my.com/init/part', 'my/path')                                                                 
Out[11]: 'http://my.com/init/my/path' # `part` is missed

Чуть-чуть невнимательности с urljoin и можно получить большие неприятности) особенно если объединяются не строки, а переменные их содержащие :slight_smile: Даже в код ревью такое будет сложно отследить

Кстати говоря is None довольно часто используется когда нужно подчеркнуть именно явное сравнение с None, так что это ок, да if req.ok is False тоже норм, это была мелкая придирка, как я написал выше

Ну пора бы уже понимать, я и сам когда-то не понимал и в конфигах держал URI типа:

profile: /profile

А потом перешел на urljoin и меня “приятно” удивила данная особенность, когда не смог направить тесты на бейс урл типа https://api.com/v2/ без правки конфига и удаления всех первых слешей.
И все понимание сводится к тому чтобы запомнить где должен быть слеш, а где его быть не должно.

Честно говоря из краткого описания несильно ясно какая была проблема и как urljoin помог ее решить, но безусловно это здорово, когда инструмент избавляет от мороки помнить лишнее.

Не только это, но и то, что первым аргументом должен быть только домен, без части URL, если у второго аргумента есть слэш в начале:

In [25]: urljoin('http://my.com/first', '/second')                                                                     
Out[25]: 'http://my.com/second'  # `first` removed

и то, что стоит избегать двойных слешей в начале второго аргумента

In [27]: urljoin('http://my.com/first', '//second')                                                                   
Out[27]: 'http://second'  # `my.com/first` removed

И также нужно держать в голове то, что наличие слэша на конце первого аргумента также играет роль, если у второго нет слэша в начале:

In [34]: urljoin('http://my.com/first/', 'second')                                                                     
Out[34]: 'http://my.com/first/second'

In [35]: urljoin('http://my.com/first', 'second')                                                                      
Out[35]: 'http://my.com/second'

И я думаю, это не все варианты, которые возможны по спецификации RFC 3986

/profile убирал v2 потому что был написан с идентификатором корня

Я считаю - стоит один раз это изучить и всегда знать, чем париться с конкатенацией вручную)

Я уже хотел было еще привести аргументы почему urljoin мощная но опасная штука, особенно если используются сторонние функции отдающие части URL для склейки, но мне правда лень пополнять этот бесконечный тред :slight_smile: Но и оставить без ответа было бы неуважительно, поэтому предлагаю здесь остановиться)

Все пишут как им удобно) А это всего лишь малая часть кода, представляю какие разногласия будут во всех компонентах если произвести ревью 2-3 проектов)) При этом каждый будет работать и не вызывать особых проблем.