- •ВВЕДЕНИЕ
- •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
- •ЗАКЛЮЧЕНИЕ
- •СПИСОК ЛИТЕРАТУРЫ
def test_app_is_production(self):
self.assertTrue( app.config['SECRET_KEY'] == \
os.environ.get('SECRET_KEY'))
self.assertFalse(app.config['TESTING'])
self.assertFalse(app.config['DEBUG_TB_ENABLED'])
self.assertTrue(app.config['BCRYPT_LOG_ROUNDS'] == 13)
self.assertTrue(app.config['TOKEN_EXPIRATION_DAYS'] == 30)
self.assertTrue(app.config['TOKEN_EXPIRATION_SECONDS'] == 0)
Затем необходимо обновить метод encode_auth_token() в модели:
def encode_auth_token(self, user_id):
"""Создание токена аутентификации"""
try:
payload = {
'exp': datetime.datetime.utcnow() + datetime.timedelta( days=current_app.config.get('TOKEN_EXPIRATION_DAYS'), se-
conds=current_app.config.get('TOKEN_EXPIRATION_SECONDS') ),
'iat': datetime.datetime.utcnow(), 'sub': user_id
}
return jwt.encode( payload,
current_app.config.get('SECRET_KEY'), algorithm='HS256'
)
except Exception as e: return e
Сейчас самое время написать тест, в котором будет выполняться декоди-
рование токена.
3.5.2. РАСШИФРОВКА ТОКЕНА
Двигаясь дальше, добавим в файл test_user_model.py следующий тест для декодирования токена:
60
def test_decode_auth_token(self):
user = add_user('justatest', 'test@test.com', 'test') auth_token = user.encode_auth_token(user.id) self.assertTrue(isinstance(auth_token, bytes)) self.assertEqual(User.decode_auth_token(auth_token), user.id)
Добавим в класс User() следующий метод:
@staticmethod
def decode_auth_token(auth_token):
"""Расшифровывает токен аутентификации -: param auth_token: -: return: integer | string"""
try:
payload = jwt.decode(
auth_token, current_app.config.get('SECRET_KEY')) return payload['sub']
except jwt.ExpiredSignatureError:
return 'Signature expired. Please log in again.' except jwt.InvalidTokenError:
return 'Invalid token. Please log in again.'
Каждый аутентифицированный запрос должен включать токен аутенти-
фикации для проверки подлинности пользователя. Прежде чем двигаться даль-
ше, необходимо убедиться, что тесты выполняются успешно.
3.6. МАРШРУТЫ ДЛЯ АУТЕНТИФИКАЦИИ
Теперь можно настроить маршруты аутентификации. Прежде чем писать код, необходимо убедиться, что покрытие тестами не уменьшается по мере до-
бавления новых маршрутов. Проверим качество покрытия тестами:
Coverage Summary: |
|
Name |
Stmts Miss Branch BrPart Cover |
---------------------------------------------------------
project/__init__.py |
24 |
12 |
0 |
0 |
50% |
project/api/models.py |
31 |
21 |
2 |
0 |
30% |
project/api/users.py |
50 |
10 |
10 |
0 |
83% |
---------------------------------------------------------
TOTAL |
105 43 |
12 |
0 |
62% |
В этом параграфе мы создадим следующие маршруты:
61
Точка |
HTTP-метод |
Необходимость |
Результат |
доступа |
|
аутентификации |
|
|
|
|
|
/auth/register |
POST |
Нет |
Регистрация пользователя |
/auth/login |
POST |
Нет |
Вход пользователя |
/auth/logout |
GET |
Да |
Выход пользователя |
/auth/status |
GET |
Да |
Получение статуса |
|
|
|
пользователя |
Добавим новый файл в каталог services/users/project/api с именем auth.py:
# services/users/project/api/auth.py
from flask import Blueprint, jsonify, request from sqlalchemy import exc, or_
from project.api.models import User from project import db, bcrypt
auth_blueprint = Blueprint('auth', __name__)
Затем зарегистрируем новый Blueprint в приложении в файле services/users /project /__init__.py:
…
# регистрация blueprints
from project.api.users import users_blueprint app.register_blueprint(users_blueprint)
from project.api.auth import auth_blueprint app.register_blueprint(auth_blueprint)
...
Добавим новый файл с именем test_auth.py в каталог tests для хранения
всех тестов, связанных с Blueprint:
# services/users/project/tests/test_auth.py
import json
from project import db
from project.api.models import User
from project.tests.base import BaseTestCase from project.tests.utils import add_user
62
class TestAuthBlueprint(BaseTestCase):
pass
3.6.1. МАРШРУТ POST /AUTH/REGISTER
Создание маршрута начнём с написания теста:
def test_user_registration(self): with self.client:
response = self.client.post( '/auth/register', data=json.dumps({
'username': 'justatest', 'email': 'test@test.com', 'password': '123456',
}), content_type='application/json'
)
data = json.loads(response.data.decode()) self.assertTrue(data['status'] == 'success') self.assertTrue(data['message'] == 'Successfully registered.') self.assertTrue(data['auth_token']) self.assertTrue(response.content_type == 'application/json') self.assertEqual(response.status_code, 201)
Тест проверят только корректные варианты регистрации. Однако воз-
можны следующие ситуации:
1)адрес электронной почты уже существует;
2)имя пользователя уже существует;
3)полезная нагрузка является недопустимой (пустая, не содержит имени пользователя, не содержит адреса электронной почты, не содержит пароля).
Напишем для трёх перечисленных ситуаций соответствующие тесты:
def test_user_registration_duplicate_email(self): add_user('test', 'test@test.com', 'test')
with self.client:
response = self.client.post( '/auth/register', data=json.dumps({
'username': 'testname1',
63
'email': 'test@test.com', 'password': 'test'
}), content_type='application/json',
)
data = json.loads(response.data.decode()) self.assertEqual(response.status_code, 400) self.assertIn(
'Sorry. That user already exists.', data['message']) self.assertIn('fail', data['status'])
def test_user_registration_duplicate_username(self): add_user('test', 'test@test.com', 'test')
with self.client:
response = self.client.post( '/auth/register', data=json.dumps({
'username': 'test', 'email': 'test@test.com2', 'password': 'test'
}), content_type='application/json',
)
data = json.loads(response.data.decode()) self.assertEqual(response.status_code, 400) self.assertIn(
'Sorry. That user already exists.', data['message']) self.assertIn('fail', data['status'])
def test_user_registration_invalid_json(self): with self.client:
response = self.client.post( '/auth/register', data=json.dumps({}), content_type='application/json'
)
data = json.loads(response.data.decode()) self.assertEqual(response.status_code, 400)
64
self.assertIn('Invalid payload.', data['message']) self.assertIn('fail', data['status'])
def test_user_registration_invalid_json_keys_no_username (self): with self.client:
response = self.client.post( '/auth/register', data=json.dumps({
'email': 'test@test.com', 'password': 'test'
}), 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'])
def test_user_registration_invalid_json_keys_no_email(self): with self.client:
response = self.client.post( '/auth/register', data=json.dumps({
'username': 'justatest', 'password': 'test'
}), 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'])
def test_user_registration_invalid_json_keys_no_password (self): with self.client:
response = self.client.post( '/auth/register', data=json.dumps({
'username': 'justatest',
65
'email': 'test@test.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'])
Убедимся, что после запуска тесты завершаются неудачно, а затем доба-
вим функцию-представление:
@auth_blueprint.route('/auth/register', methods=['POST']) def register_user():
# get post data
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(
or_(User.username == username, User.email == email)).first() if not user:
# добавление нового пользователя в базу данных new_user = User(
username=username,
email=email,
password=password
)
db.session.add(new_user)
db.session.commit()
# генерация токена
auth_token = new_user.encode_auth_token(new_user.id)
66
