- •ВВЕДЕНИЕ
- •1. ПОДГОТОВКА К РАЗРАБОТКЕ
- •1.1. НАЧАЛО РАБОТЫ
- •1.2. ИСПОЛЬЗОВАНИЕ КОНТЕЙНЕРОВ DOCKER
- •1.3. НАСТРОЙКА СУБД POSTGRESQL
- •1.4. НАСТРОЙКА ТЕСТОВ
- •1.6. СОЗДАНИЕ RESTFUL-МАРШРУТОВ
- •1.6.1. МАРШРУТ POST /USERS
- •1.6.3. МАРШРУТ GET /USERS
- •1.6.2. МАРШРУТ GET /USERS/:ID
- •1.7. ИСПОЛЬЗОВАНИЕ ШАБЛОНОВ JINJA
- •1.8. СТРУКТУРА ПРОЕКТА
- •2. ОТЛАДКА ПРИЛОЖЕНИЯ
- •2.1. ПОКРЫТИЕ ТЕСТАМИ И КАЧЕСТВО КОДА
- •2.1.1. ПОКРЫТИЕ КОДА ТЕСТАМИ
- •2.1.2. КАЧЕСТВО КОДА
- •2.2. НЕПРЕРЫВНАЯ ИНТЕГРАЦИЯ
- •2.3. ПАНЕЛЬ ИНСТРУМЕНТОВ ОТЛАДКИ FLASK
- •2.4. СТРУКТУРА ПРИЛОЖЕНИЯ
- •3. РАЗВИТИЕ ПРИЛОЖЕНИЯ
- •3.1. ПАКЕТ FLASK MIGRATE
- •3.1.1. ИЗМЕНЕНИЕ МОДЕЛИ
- •3.1.2. НАСТРОЙКА FLASK MIGRATE
- •3.2. РЕФАКТОРИНГ КОДА
- •3.3. ПАКЕТ FLASK BCRYPT
- •3.4. НАСТРОЙКА LOG ROUNDS
- •3.5. ИСПОЛЬЗОВАНИЕ ТОКЕНОВ JWT
- •3.5.1. ПАКЕТ PYJWT
- •3.5.2. РАСШИФРОВКА ТОКЕНА
- •3.6. МАРШРУТЫ ДЛЯ АУТЕНТИФИКАЦИИ
- •3.6.1. МАРШРУТ POST /AUTH/REGISTER
- •3.6.2. МАРШРУТ POST /AUTH/LOGIN
- •3.6.3. МАРШРУТ GET /AUTH/LOGOUT
- •3.6.4. МАРШРУТ GET /AUTH/STATUS
- •ЗАКЛЮЧЕНИЕ
- •СПИСОК ЛИТЕРАТУРЫ
Имейте в виду, что если в базе данных уже есть повторяющиеся имена пользователей и/или адреса электронной почты, то мы получим сообщение об ошибке при попытке применить миграцию к базе данных. Мы можем обновить данные или очистить базу данных и начать заново.
Перейдите в клиент psql, чтобы убедиться, что таблица обновлена:
$ docker exec -ti users-db psql -U postgres -W
Азатем выполните команды:
#\c users_dev
#\d+ users
Вы должны увидеть ограничения на уникальность:
Indexes:
"users_pkey" PRIMARY KEY, btree (id)
"users_email_key" UNIQUE CONSTRAINT, btree (email)
"users_username_key" UNIQUE CONSTRAINT, btree (username)
Снова запустим тесты. Теперь должна быть только одна ошибка:
NameError: name 'add_user' is not defined
3.2. РЕФАКТОРИНГ КОДА
Вы заметили, что мы несколько раз добавляли нового пользователя в тес-
тах test_user_model.py? Давайте абстрагируем вспомогательную функцию add_user() из файла test_users.py до отдельного служебного файла, чтобы мы могли использовать её в обоих тестах.
Добавим новый файл с именем utils.py в tests:
# services/users/project/tests/utils.py
from project import db
from project.api.models import User
def add_user(username, email):
user = User(username=username, email=email) db.session.add(user)
db.session.commit() return user
Затем удалим хелпер из файла test_users.py и добавим импорт в тот же
файл:
from project.tests.utils import add_user
45
Выполним рефакторинг содержимого файла test_user_model.py следую-
щим образом:
# services/users/project/tests/test_user_model.py
import unittest
from sqlalchemy.exc import IntegrityError from project import db
from project.api.models import User
from project.tests.base import BaseTestCase from project.tests.utils import add_user
class TestUserModel(BaseTestCase): def test_add_user(self):
user = add_user('justatest', 'test@test.com') self.assertTrue(user.id) self.assertEqual(user.username, 'justatest') self.assertEqual(user.email, 'test@test.com') self.assertTrue(user.active)
def test_add_user_duplicate_username(self): add_user('justatest', 'test@test.com') duplicate_user = User(
username='justatest',
email='test@test2.com',
)
db.session.add(duplicate_user) self.assertRaises(IntegrityError, db.session.commit)
def test_add_user_duplicate_email(self): add_user('justatest', 'test@test.com') duplicate_user = User(
username='justatest2',
email='test@test.com',
)
db.session.add(duplicate_user) self.assertRaises(IntegrityError, db.session.commit)
def test_to_json(self):
user = add_user('justatest', 'test@test.com') self.assertTrue(isinstance(user.to_json(), dict))
46
if __name__ == '__main__':
unittest.main()
Снова запустим тесты, чтобы убедиться, что в результате рефакторинга
ничего «не сломалось»:
------------------------------------------------------------------
Ran 19 tests in 0.512s
Проверим, что покажет линтер flake8:
$ docker-compose -f docker-compose-dev.yml \ run users flake8 project
В случае обнаружения проблем исправьте их, а затем сделайте коммит и
отправьте код на GitHub. Убедитесь, что сборка с помощью Travic успешно вы-
полняется.
3.3.ПАКЕТ FLASK BCRYPT
Вэтом параграфе добавим поддержку хеширования паролей пользовате-
лей. Для управления хешированием паролей будем использовать расширение
Flask-Bcrypt. Добавим его в файл requirements.txt:
flask-bcrypt==0.7.1
Можно заменить Flask-Bcrypt на хелпер для хеширования паролей из
Werkzeug.
Подключим Flask-Bcrypt к приложению в services/users/project/ __init__.py.
# services/users/project/__init__.py
import os
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_debugtoolbar import DebugToolbarExtension from flask_cors import CORS
from flask_migrate import Migrate from flask_bcrypt import Bcrypt
# инстанционирование расширений db = SQLAlchemy()
toolbar = DebugToolbarExtension() migrate = Migrate()
bcrypt = Bcrypt()
47
def create_app(script_info=None):
# инстанционирование приложения app = Flask(__name__)
#включение CORS CORS(app)
#подключение конфигурации
app_settings = os.getenv('APP_SETTINGS') app.config.from_object(app_settings)
# запуск расширений db.init_app(app) toolbar.init_app(app) migrate.init_app(app, db) bcrypt.init_app(app)
# регистрация blueprints
from project.api.users import users_blueprint app.register_blueprint(users_blueprint)
# контекст оболочки для flask cli app.shell_context_processor({'app': app, 'db': db} return app
Перед обновлением модели добавим в test_user_model.py следующий
тест:
def test_passwords_are_random(self):
user_one = add_user('justatest', 'test@test.com',\
'testpassword')
user_two = add_user('justatest2', 'test@test2.com',\
'testpassword')
self.assertNotEqual(user_one.password, user_two.password)
Обновим хелпер, чтобы он в качестве аргумента принимал пароль:
def add_user(username, email, password):
user = User(username=username, email=email, password=password)
db.session.add(user)
db.session.commit()
return user
Теперь необходимо передавать аргумент для всех экземпляров add_user()
и User(), а также в полезной нагрузке для запросов POST к маршрутам /users и /
как в test_user_model.py, так и в test_users.py.
48
Примеры:
response = self.client.post( '/users',
data=json.dumps({ 'username': 'testname1',
'email': 'testname1@flaskapp.org', 'password': 'testpassword'
}), content_type='application/json', )
...
user = add_user('justatest', 'test@test.com', 'testpassword')
...
duplicate_user = User( username='justatest', email='test@test2.com', password='testpassword'
)
Обновим контейнеры после изменений:
$ docker-compose -f docker-compose-dev.yml up -d --build
И запустим тесты:
$ docker-compose -f docker-compose-dev.yml \ run users python manage.py test
После запуска должно появиться сообщение об ошибке:
TypeError: __init__() got an unexpected keyword argument
'password'
Чтобы тесты заработали, сначала добавим поле password в модель в файл
services/users/project/api/models.py:
class User(db.Model): __tablename__ = "users"
id = db.Column(db.Integer, primary_key=True, autoincrement=True) username = db.Column(db.String(128), unique=True, nullable=False) email = db.Column(db.String(128), unique=True, nullable=False) password = db.Column(db.String(255), nullable=False)
active = db.Column(db.Boolean, default=True, nullable=False)
49
def __init__(self, username, email, password):
self.username = username
self.email = email
self.password = bcrypt.generate_password_hash(password).decode()
Затем добавим импорт bcrypt:
from project import db, bcrypt
Снова запустим тесты. Сообщение об ошибке поменяется:
TypeError: __init__() missing 1 required positional argument:
'password'
Применим миграции:
$ docker-compose -f docker-compose-dev.yml \ run users python man-
age.py db migrate
$ docker-compose -f docker-compose-dev.yml \ run users python man-
age.py db upgrade
Сообщение об ошибке снова поменяется:
sqlalchemy.exc.IntegrityError: (psycopg2.IntegrityError) column "password" contains null values
[SQL: 'ALTER TABLE users ADD COLUMN password VARCHAR(255) NOT NULL'] (Background on this error at: http://sqlalche.me/e/gkpj)
Для устранения ошибки можно сделать следующее
1. Удалить всех пользователей в psql с помощью команды delete from
users.
2. Обновить существующий файл миграции для создания нового поля без добавления ограничения, не допускающего значения NULL, изменить данные, а
затем добавить ограничение:
def upgrade():
#### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('password', sa.String(length=255)))
op.execute('UPDATE users SET password=email') op.alter_column('users', 'password', nullable=False)
#### end Alembic commands ###
Поскольку на данный момент мы работаем в режиме разработки, дейст-
вительно не имеет значения, какой из вариантов вы выберете.
Далее обновим функцию add_user() в файле services/users/project/api/users.py:
50
@users_blueprint.route('/users', methods=['POST']) def add_user():
post_data = request.get_json() response_object = {
'status': 'fail',
'message': 'Invalid payload.'
}
if not post_data:
return jsonify(response_object), 400 username = post_data.get('username') email = post_data.get('email') password = post_data.get('password') try:
user = User.query.filter_by(email=email).first() if not user:
db.session.add(User(
username=username, email=email, password=password)) db.session.commit()
response_object['status'] = 'success' response_object['message'] = f'{email} was added!' return jsonify(response_object), 201
else:
response_object['message'] = 'Sorry. That email already exists.'
return jsonify(response_object), 400 except exc.IntegrityError as e:
db.session.rollback()
return jsonify(response_object), 400
Также обновим функцию index():
@users_blueprint.route('/', methods=['GET', 'POST']) def index():
if request.method == 'POST':
username = request.form['username'] email = request.form['email'] password = request.form['password']
db.session.add(User(username=username, email=email,\ password=password))
db.session.commit()
51
users = User.query.all()
return render_template('index.html', users=users)
Теперь тесты должны выполниться успешно:
------------------------------------------------------------------
Ran 20 tests in 6.121s
Предположим, что в полезной нагрузке не оказалось пароля. Напишем
для этой ситуации тест test_users.py:
def test_add_user_invalid_json_keys_no_password(self):
"""Проверка, что выдаётся ошибка, если у объекта JSON нет ключа password. """
with self.client:
response = self.client.post( '/users', data=json.dumps(dict(
username='testname1', email='testname1@reallyflaskapp.com')), content_type='application/json',
)
data = json.loads(response.data.decode()) self.assertEqual(response.status_code, 400) self.assertIn('Invalid payload.', data['message']) self.assertIn('fail', data['status'])
При запуске тестов должно появиться сообщение о следующей ошибке:
raise ValueError('Password must be non-empty.') ValueError: Pass-
word must be non-empty.
Чтобы исправить проблему, необходимо добавить ещё один обработчик исключения в блок try/except в обработчике представления add_user:
except (exc.IntegrityError, ValueError) as e:
db.session.rollback()
return jsonify(response_object), 400
Проверим тесты ещё раз, а затем обновим следующий тест в файле test_user_model.py с целью проверки, что объект User имеет поле с паролем:
def test_add_user(self):
user = add_user('justatest', 'test@test.com', 'test') self.assertTrue(user.id) self.assertEqual(user.username, 'justatest') self.assertEqual(user.email, 'test@test.com') self.assertTrue(user.active) self.assertTrue(user.password)
52
