diff --git a/app/__init__.py b/app/__init__.py index 0587aae..9004a4d 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -6,13 +6,13 @@ from app import extensions from app.blueprints import blueprints -from app.middleware import middleware +from app.middleware import Middleware def create_app(env_config: str) -> Flask: app = flask.Flask(__name__) app.config.from_object(env_config) - app.wsgi_app = middleware(app) + app.wsgi_app = Middleware(app) init_logging(app) init_app(app) @@ -22,7 +22,8 @@ def create_app(env_config: str) -> Flask: def init_app(app: Flask) -> None: - """Call the method 'init_app' to register the extensions in the flask.Flask object passed as parameter.""" + """Call the method 'init_app' to register the extensions in the flask.Flask + object passed as parameter.""" extensions.init_app(app) diff --git a/app/blueprints/__init__.py b/app/blueprints/__init__.py index d8c7e88..300e2fc 100644 --- a/app/blueprints/__init__.py +++ b/app/blueprints/__init__.py @@ -12,4 +12,4 @@ blueprint_auth, blueprint_documents, blueprint_tasks, -] \ No newline at end of file +] diff --git a/app/blueprints/auth.py b/app/blueprints/auth.py index a895318..4b68232 100644 --- a/app/blueprints/auth.py +++ b/app/blueprints/auth.py @@ -5,40 +5,36 @@ from flask_restx import Resource from flask_security import verify_password from flask_security.passwordless import generate_login_token -from werkzeug.exceptions import Forbidden, Unauthorized, UnprocessableEntity, NotFound +from werkzeug.exceptions import (Forbidden, Unauthorized, UnprocessableEntity, + NotFound) from app.extensions import api as root_api from app.models.user import User as UserModel, user_datastore from app.celery.tasks import reset_password_email -from app.utils.cerberus_schema import MyValidator, user_login_schema, confirm_reset_password_schema +from app.utils.cerberus_schema import (MyValidator, user_login_schema, + confirm_reset_password_schema) from app.utils.decorators import token_required -from config import Config +from app.utils.swagger_models.auth import (AUTH_LOGIN_SW_MODEL, + AUTH_TOKEN_SW_MODEL, + AUTH_REQUEST_RESET_PASSWORD_SW_MODEL, + AUTH_RESET_PASSWORD_SW_MODEL) blueprint = Blueprint('auth', __name__) api = root_api.namespace('auth', description='Authentication endpoints') - logger = logging.getLogger(__name__) @api.route('/login') class AuthUserLoginResource(Resource): - _parser = api.parser() - _parser.add_argument('email', type=str, location='json') - _parser.add_argument('password', type=str, location='json') - - @api.doc(responses={ - 200: 'Success', - 401: 'Unauthorized', - 403: 'Forbidden', - 422: 'Unprocessable Entity', - }) - @api.expect(_parser) + @api.doc(responses={401: 'Unauthorized', 403: 'Forbidden', + 422: 'Unprocessable Entity'}) + @api.expect(AUTH_LOGIN_SW_MODEL) + @api.marshal_with(AUTH_TOKEN_SW_MODEL) def post(self) -> tuple: data = request.get_json() v = MyValidator(schema=user_login_schema()) v.allow_unknown = False - if not v.validate(data): raise UnprocessableEntity(v.errors) @@ -53,41 +49,23 @@ def post(self) -> tuple: token = generate_login_token(user) # TODO: Pending to testing whats happen id add a new field in user model when a user is logged flask_security.login_user(user) - return { - 'token': token, - }, 200 + return {'token': f'Bearer {token}'}, 200 @api.route('/logout') class AuthUserLogoutResource(Resource): - _parser = api.parser() - _parser.add_argument(Config.SECURITY_TOKEN_AUTHENTICATION_HEADER, location='headers', required=True, - default='Bearer token') - - @api.doc(responses={ - 200: 'Success', - 401: 'Unauthorized', - }) - @api.expect(_parser) + @api.doc(responses={200: 'Success', 401: 'Unauthorized'}, + security='auth_token') @token_required def post(self) -> tuple: flask_security.logout_user() - return {}, 200 -# TODO: update endpoint name @api.route('/reset_password') class RequestResetPasswordResource(Resource): - _parser = api.parser() - _parser.add_argument('email', type=str, location='json') - - @api.doc(responses={ - 200: 'Success', # TODO: change status code to 202 - 403: 'Forbidden', - 404: 'Not Found', - }) - @api.expect(_parser) + @api.doc(responses={202: 'Success', 403: 'Forbidden', 404: 'Not Found'}) + @api.expect(AUTH_REQUEST_RESET_PASSWORD_SW_MODEL) def post(self) -> tuple: data = request.get_json() email = data.get('email') @@ -103,27 +81,22 @@ def post(self) -> tuple: raise Forbidden('User is not active') token = user.get_reset_token() - - reset_password_url = url_for('auth_reset_password_resource', token=token, _external=True) + reset_password_url = url_for('auth_reset_password_resource', + token=token, + _external=True) email_data = { 'email': user.email, 'reset_password_url': reset_password_url, } - reset_password_email.delay(email_data) - - return {}, 200 + return {}, 202 -# TODO: update endpoint name @api.route('/reset_password/') @api.doc(params={'token': 'A password reset token created previously'}) class ResetPasswordResource(Resource): - @api.doc(responses={ - 200: 'Success', - 403: 'Forbidden', - }) + @api.doc(responses={200: 'Success', 403: 'Forbidden'}) def get(self, token: str) -> tuple: user = UserModel.verify_reset_token(token) @@ -138,15 +111,10 @@ def get(self, token: str) -> tuple: return {}, 200 - _parser = api.parser() - _parser.add_argument('password', type=str, location='json') - - @api.doc(responses={ - 200: 'Success', - 403: 'Forbidden', - 422: 'Unprocessable Entity', - }) - @api.expect(_parser) + @api.doc(responses={200: 'Success', 403: 'Forbidden', + 422: 'Unprocessable Entity'}) + @api.expect(AUTH_RESET_PASSWORD_SW_MODEL) + @api.marshal_with(AUTH_TOKEN_SW_MODEL) def post(self, token: str) -> tuple: data = request.get_json() @@ -164,6 +132,4 @@ def post(self, token: str) -> tuple: token = generate_login_token(user) - return { - 'token': token, - }, 200 + return {'token': f'Bearer {token}'}, 200 diff --git a/app/blueprints/base.py b/app/blueprints/base.py index 169527a..f5e9f4f 100644 --- a/app/blueprints/base.py +++ b/app/blueprints/base.py @@ -1,6 +1,6 @@ import logging -from flask_restx import Resource, fields +from flask_restx import Resource from flask import Blueprint from peewee import ModelSelect from werkzeug.exceptions import UnprocessableEntity @@ -11,11 +11,11 @@ blueprint = Blueprint('base', __name__) api = root_api.namespace('', description='Base endpoints') - logger = logging.getLogger(__name__) + class BaseResource(Resource): - db_model: db.Model + db_model = db.Model request_validation_schema = {} def request_validation(self, request_data: dict) -> None: @@ -27,7 +27,8 @@ def request_validation(self, request_data: dict) -> None: def get_request_query_fields(self, request_data: dict) -> tuple: return get_request_query_fields(self.db_model, request_data) - def create_search_query(self, query: ModelSelect, request_data: dict) -> ModelSelect: + def create_search_query(self, query: ModelSelect, request_data: dict) \ + -> ModelSelect: return create_search_query(self.db_model, query, request_data) diff --git a/app/blueprints/documents.py b/app/blueprints/documents.py index e61361d..53eaa6c 100644 --- a/app/blueprints/documents.py +++ b/app/blueprints/documents.py @@ -6,41 +6,31 @@ from flask import Blueprint, request, current_app, send_file from flask_login import current_user -from flask_restx import fields from flask_security import roles_accepted from marshmallow import EXCLUDE, ValidationError from werkzeug.datastructures import FileStorage as WerkzeugFileStorage -from werkzeug.exceptions import NotFound, UnprocessableEntity, InternalServerError, BadRequest +from werkzeug.exceptions import (NotFound, UnprocessableEntity, + InternalServerError, BadRequest) from app.blueprints.base import BaseResource -from app.blueprints.users import creator_sw_model from app.extensions import api as root_api from app.models.document import Document as DocumentModel from app.utils.cerberus_schema import document_model_schema, search_model_schema from app.utils.decorators import token_required from app.utils.file_storage import FileStorage -from app.utils.marshmallow_schema import DocumentSchema as DocumentSerializer, \ - GetDocumentDataInputSchema as GetDocumentDataInputSerializer -from config import Config +from app.utils.marshmallow_schema import (DocumentSchema as DocumentSerializer, + GetDocumentDataInputSchema as GetDocumentDataInputSerializer) +from app.utils.swagger_models import SEARCH_INPUT_SW_MODEL +from app.utils.swagger_models.document import (DOCUMENT_OUTPUT_SW_MODEL, + DOCUMENT_SEARCH_OUTPUT_SW_MODEL) + +_API_DESCRIPTION = ('Users with role admin, team_leader or worker can ' + 'manage these endpoints.') blueprint = Blueprint('documents', __name__) -api = root_api.namespace('documents', - description='Documents endpoints. Users with role admin, team_leader or worker can manage these endpoints.') +api = root_api.namespace('documents', description=_API_DESCRIPTION) logger = logging.getLogger(__name__) -document_sw_model = api.model('Document', { - 'id': fields.Integer, - 'name': fields.String, - 'internal_name': fields.String, - 'mime_type': fields.String, - 'size': fields.Integer, - 'url': fields.String, - 'created_at': fields.String, - 'updated_at': fields.String, - 'deleted_at': fields.String, - 'created_by': fields.Nested(creator_sw_model), -}) - class DocumentBaseResource(BaseResource): db_model = DocumentModel @@ -71,11 +61,10 @@ def get_document_data(self, document_id: int) -> tuple: document_data = self.document_serializer.dump(document) - return { - 'data': document_data, - }, 200 + return {'data': document_data}, 200 - def get_document_content(self, document_id: int): + @staticmethod + def get_document_content(document_id: int): document = DocumentModel.get_or_none(DocumentModel.id == document_id, DocumentModel.deleted_at.is_null()) if document is None: @@ -110,21 +99,19 @@ def get_document_content(self, document_id: int): @api.route('') class NewDocumentResource(DocumentBaseResource): - _parser = api.parser() - _parser.add_argument(Config.SECURITY_TOKEN_AUTHENTICATION_HEADER, location='headers', required=True, - default='Bearer token') - _parser.add_argument('Content-Type', type=str, location='headers', required=True, - choices=('multipart/form-data',)) - _parser.add_argument(DocumentBaseResource.request_field_name, type=WerkzeugFileStorage, location='files', - required=True, help='You only can upload Excel and PDF files.') - - @api.doc(responses={ - 201: ('Success', document_sw_model), - 401: 'Unauthorized', - 403: 'Forbidden', - 422: 'Unprocessable Entity', - }) - @api.expect(_parser) + parser = api.parser() + parser.add_argument('Content-Type', type=str, location='headers', + required=True, choices=('multipart/form-data',)) + parser.add_argument(DocumentBaseResource.request_field_name, + type=WerkzeugFileStorage, location='files', + required=True, help='You only can upload Excel and ' + 'PDF files.') + + @api.doc(responses={401: 'Unauthorized', 403: 'Forbidden', + 422: 'Unprocessable Entity'}, + security='auth_token') + @api.expect(parser) + @api.marshal_with(DOCUMENT_OUTPUT_SW_MODEL, code=201) @token_required @roles_accepted('admin', 'team_leader', 'worker') def post(self): @@ -133,10 +120,14 @@ def post(self): self.request_validation(request_data) request_file = request_data.get(self.request_field_name) - file_extension = mimetypes.guess_extension(request_file.get('mime_type')) + file_extension = mimetypes.guess_extension( + request_file.get('mime_type') + ) internal_filename = '%s%s' % (uuid.uuid1().hex, file_extension) - filepath = '%s/%s' % (current_app.config.get('STORAGE_DIRECTORY'), internal_filename) + filepath = '%s/%s' % ( + current_app.config.get('STORAGE_DIRECTORY'), internal_filename + ) try: fs = FileStorage() @@ -160,26 +151,20 @@ def post(self): document_data = self.document_serializer.dump(document) - return { - 'data': document_data, - }, 200 + return {'data': document_data}, 201 @api.route('/') class DocumentResource(DocumentBaseResource): _parser = api.parser() - _parser.add_argument(Config.SECURITY_TOKEN_AUTHENTICATION_HEADER, location='headers', required=True, - default='Bearer token') - _parser.add_argument('Content-Type', type=str, location='headers', required=True, - choices=('application/json', 'application/octet-stream',)) - - @api.doc(responses={ - 200: ('Success', document_sw_model), - 401: 'Unauthorized', - 403: 'Forbidden', - 404: 'Not Found', - 422: 'Unprocessable Entity', - }) + _parser.add_argument('Content-Type', type=str, location='headers', + required=True, choices=('application/json', + 'application/octet-stream',)) + + @api.doc(responses={200: ('Success', DOCUMENT_OUTPUT_SW_MODEL), + 401: 'Unauthorized', 403: 'Forbidden', 404: 'Not Found', + 422: 'Unprocessable Entity'}, + security='auth_token') @api.expect(_parser) @token_required @roles_accepted('admin', 'team_leader', 'worker') @@ -193,22 +178,11 @@ def get(self, document_id: int) -> tuple: return response - _parser = api.parser() - _parser.add_argument(Config.SECURITY_TOKEN_AUTHENTICATION_HEADER, location='headers', required=True, - default='Bearer token') - _parser.add_argument('Content-Type', type=str, location='headers', required=True, - choices=('multipart/form-data',)) - _parser.add_argument(DocumentBaseResource.request_field_name, type=WerkzeugFileStorage, location='files', - required=True, help='You only can upload Excel and PDF files.') - - @api.doc(responses={ - 200: ('Success', document_sw_model), - 401: 'Unauthorized', - 403: 'Forbidden', - 404: 'Not Found', - 422: 'Unprocessable Entity', - }) - @api.expect(_parser) + @api.doc(responses={401: 'Unauthorized', 403: 'Forbidden', 404: 'Not Found', + 422: 'Unprocessable Entity'}, + security='auth_token') + @api.expect(NewDocumentResource.parser) + @api.marshal_with(DOCUMENT_OUTPUT_SW_MODEL) @token_required @roles_accepted('admin', 'team_leader', 'worker') def put(self, document_id: int) -> tuple: @@ -248,22 +222,12 @@ def put(self, document_id: int) -> tuple: DocumentModel.deleted_at.is_null())) document_data = self.document_serializer.dump(document) - return { - 'data': document_data, - }, 200 + return {'data': document_data}, 200 - _parser = api.parser() - _parser.add_argument(Config.SECURITY_TOKEN_AUTHENTICATION_HEADER, location='headers', required=True, - default='Bearer token') - - @api.doc(responses={ - 200: ('Success', document_sw_model), - 400: 'Bad Request', - 401: 'Unauthorized', - 403: 'Forbidden', - 404: 'Not Found', - }) - @api.expect(_parser) + @api.doc(responses={400: 'Bad Request', 401: 'Unauthorized', + 403: 'Forbidden', 404: 'Not Found'}, + security='auth_token') + @api.marshal_with(DOCUMENT_OUTPUT_SW_MODEL) @token_required @roles_accepted('admin', 'team_leader', 'worker') def delete(self, document_id: int): @@ -279,9 +243,7 @@ def delete(self, document_id: int): document_data = self.document_serializer.dump(document) - return { - 'data': document_data, - }, 200 + return {'data': document_data}, 200 @api.route('/search') @@ -289,21 +251,11 @@ class SearchDocumentResource(DocumentBaseResource): document_fields = DocumentModel.get_fields() request_validation_schema = search_model_schema(document_fields) - _parser = api.parser() - _parser.add_argument(Config.SECURITY_TOKEN_AUTHENTICATION_HEADER, location='headers', required=True, - default='Bearer token') - _parser.add_argument('search', type=list, location='json') - _parser.add_argument('order', type=list, location='json') - _parser.add_argument('items_per_page', type=int, location='json') - _parser.add_argument('page_number', type=int, location='json') - - @api.doc(responses={ - 200: 'Success', - 401: 'Unauthorized', - 403: 'Forbidden', - 422: 'Unprocessable Entity', - }) - @api.expect(_parser) + @api.doc(responses={200: 'Success', 401: 'Unauthorized', 403: 'Forbidden', + 422: 'Unprocessable Entity'}, + security='auth_token') + @api.expect(SEARCH_INPUT_SW_MODEL) + @api.marshal_with(DOCUMENT_SEARCH_OUTPUT_SW_MODEL) @token_required @roles_accepted('admin', 'team_leader', 'worker') def post(self): diff --git a/app/blueprints/roles.py b/app/blueprints/roles.py index c704d95..d8d1023 100644 --- a/app/blueprints/roles.py +++ b/app/blueprints/roles.py @@ -2,33 +2,27 @@ from datetime import datetime from flask import Blueprint, request -from flask_restx import fields from flask_security import roles_required from marshmallow import INCLUDE, ValidationError from werkzeug.exceptions import UnprocessableEntity, NotFound, BadRequest -from config import Config from .base import BaseResource from app.extensions import api as root_api from app.models.role import Role as RoleModel from app.utils.cerberus_schema import role_model_schema, search_model_schema from app.utils.marshmallow_schema import RoleSchema as RoleSerializer from ..utils.decorators import token_required +from ..utils.swagger_models import SEARCH_INPUT_SW_MODEL +from ..utils.swagger_models.role import (ROLE_INPUT_SW_MODEL, + ROLE_SEARCH_OUTPUT_SW_MODEL, + ROLE_OUTPUT_SW_MODEL) + +_API_DESCRIPTION = 'Users with role admin can manage these endpoints.' blueprint = Blueprint('roles', __name__) -api = root_api.namespace('roles', description='Roles endpoints. Users with role admin can manage these endpoints.') +api = root_api.namespace('roles', description=_API_DESCRIPTION) logger = logging.getLogger(__name__) -role_sw_model = api.model('Role', { - 'id': fields.Integer, - 'name': fields.String, - 'description': fields.String, - 'label': fields.String, - 'created_at': fields.String, - 'updated_at': fields.String, - 'deleted_at': fields.String, -}) - class RoleBaseResource(BaseResource): db_model = RoleModel @@ -44,20 +38,11 @@ def deserialize_request_data(self, **kwargs: dict) -> dict: @api.route('') class NewRoleResource(RoleBaseResource): - _parser = api.parser() - _parser.add_argument(Config.SECURITY_TOKEN_AUTHENTICATION_HEADER, location='headers', required=True, - default='Bearer token') - _parser.add_argument('name', type=str, location='json', required=True) - _parser.add_argument('label', type=str, location='json', required=True) - _parser.add_argument('description', type=str, location='json', required=True) - - @api.doc(responses={ - 201: ('Success', role_sw_model), - 401: 'Unauthorized', - 403: 'Forbidden', - 422: 'Unprocessable Entity', - }) - @api.expect(_parser) + @api.doc(responses={401: 'Unauthorized', 403: 'Forbidden', + 422: 'Unprocessable Entity'}, + security='auth_token') + @api.expect(ROLE_INPUT_SW_MODEL) + @api.marshal_with(ROLE_OUTPUT_SW_MODEL, code=201) @token_required @roles_required('admin') def post(self) -> tuple: @@ -68,24 +53,14 @@ def post(self) -> tuple: role = RoleModel.create(**data) role_data = self.role_serializer.dump(role) - return { - 'data': role_data - }, 201 + return {'data': role_data}, 201 @api.route('/') class RoleResource(RoleBaseResource): - _parser = api.parser() - _parser.add_argument(Config.SECURITY_TOKEN_AUTHENTICATION_HEADER, location='headers', required=True, - default='Bearer token') - - @api.doc(responses={ - 200: ('Success', role_sw_model), - 401: 'Unauthorized', - 403: 'Forbidden', - 422: 'Unprocessable Entity', - }) - @api.expect(_parser) + @api.doc(responses={401: 'Unauthorized', 403: 'Forbidden', 404: 'Not found'}, + security='auth_token') + @api.marshal_with(ROLE_OUTPUT_SW_MODEL) @token_required @roles_required('admin') def get(self, role_id: int) -> tuple: @@ -94,26 +69,13 @@ def get(self, role_id: int) -> tuple: raise NotFound('Role doesn\'t exist') role_data = self.role_serializer.dump(role) + return {'data': role_data}, 200 - return { - 'data': role_data, - }, 200 - - _parser = api.parser() - _parser.add_argument(Config.SECURITY_TOKEN_AUTHENTICATION_HEADER, location='headers', required=True, - default='Bearer token') - _parser.add_argument('name', type=str, location='json') - _parser.add_argument('label', type=str, location='json') - _parser.add_argument('description', type=str, location='json') - - @api.doc(responses={ - 200: ('Success', role_sw_model), - 400: 'Bad Request', - 401: 'Unauthorized', - 403: 'Forbidden', - 422: 'Unprocessable Entity', - }) - @api.expect(_parser) + @api.doc(responses={400: 'Bad Request', 401: 'Unauthorized', + 403: 'Forbidden', 422: 'Unprocessable Entity'}, + security='auth_token') + @api.expect(ROLE_INPUT_SW_MODEL) + @api.marshal_with(ROLE_OUTPUT_SW_MODEL) @token_required @roles_required('admin') def put(self, role_id: int) -> tuple: @@ -137,22 +99,12 @@ def put(self, role_id: int) -> tuple: RoleModel.deleted_at.is_null())) role_data = self.role_serializer.dump(role) - return { - 'data': role_data, - }, 200 + return {'data': role_data}, 200 - _parser = api.parser() - _parser.add_argument(Config.SECURITY_TOKEN_AUTHENTICATION_HEADER, location='headers', required=True, - default='Bearer token') - - @api.doc(responses={ - 200: ('Success', role_sw_model), - 400: 'Bad Request', - 401: 'Unauthorized', - 403: 'Forbidden', - 422: 'Unprocessable Entity', - }) - @api.expect(_parser) + @api.doc(responses={400: 'Bad Request', 401: 'Unauthorized', + 403: 'Forbidden'}, + security='auth_token') + @api.marshal_with(ROLE_OUTPUT_SW_MODEL) @token_required @roles_required('admin') def delete(self, role_id: int) -> tuple: @@ -168,9 +120,7 @@ def delete(self, role_id: int) -> tuple: role_data = self.role_serializer.dump(role) - return { - 'data': role_data, - }, 200 + return {'data': role_data}, 200 @api.route('/search') @@ -178,21 +128,11 @@ class RolesSearchResource(RoleBaseResource): role_fields = RoleModel.get_fields(['id']) request_validation_schema = search_model_schema(role_fields) - _parser = api.parser() - _parser.add_argument(Config.SECURITY_TOKEN_AUTHENTICATION_HEADER, location='headers', required=True, - default='Bearer token') - _parser.add_argument('search', type=list, location='json') - _parser.add_argument('order', type=list, location='json') - _parser.add_argument('items_per_page', type=int, location='json') - _parser.add_argument('page_number', type=int, location='json') - - @api.doc(responses={ - 200: 'Success', - 401: 'Unauthorized', - 403: 'Forbidden', - 422: 'Unprocessable Entity', - }) - @api.expect(_parser) + @api.doc(responses={200: 'Success', 401: 'Unauthorized', 403: 'Forbidden', + 422: 'Unprocessable Entity'}, + security='auth_token') + @api.expect(SEARCH_INPUT_SW_MODEL) + @api.marshal_with(ROLE_SEARCH_OUTPUT_SW_MODEL) @token_required @roles_required('admin') def post(self) -> tuple: diff --git a/app/blueprints/tasks.py b/app/blueprints/tasks.py index 6284703..ce5ef9d 100644 --- a/app/blueprints/tasks.py +++ b/app/blueprints/tasks.py @@ -10,16 +10,15 @@ from app.utils import class_for_name from app.utils.decorators import token_required -from config import Config blueprint = Blueprint('tasks', __name__, url_prefix='/api/tasks') api = root_api.namespace('tasks', description='Tasks endpoints') - logger = logging.getLogger(__name__) class TaskResource(Resource): - def get_task(self, task_id: str) -> PromiseProxy: + @staticmethod + def get_task(task_id: str) -> PromiseProxy: def build_task_import(row: str) -> tuple: row_list = row.split('.') class_name = row_list.pop(len(row_list) - 1) @@ -43,18 +42,9 @@ def build_task_import(row: str) -> tuple: @api.route('/status/') class TaskStatusResource(TaskResource): - _parser = api.parser() - _parser.add_argument(Config.SECURITY_TOKEN_AUTHENTICATION_HEADER, location='headers', required=True, - default='Bearer token') - - @api.doc(responses={ - 200: 'Success', - 401: 'Unauthorized', - 403: 'Forbidden', - 404: 'Not found', - 422: 'Unprocessable Entity', - }) - @api.expect(_parser) + @api.doc(responses={200: 'Success', 401: 'Unauthorized', 403: 'Forbidden', + 404: 'Not found', 422: 'Unprocessable Entity'}, + security='auth_token') @token_required @roles_accepted('admin', 'team_leader', 'worker') def get(self, task_id: str): diff --git a/app/blueprints/users.py b/app/blueprints/users.py index 74d0626..b9a630e 100644 --- a/app/blueprints/users.py +++ b/app/blueprints/users.py @@ -3,7 +3,6 @@ from flask_login import current_user from flask import Blueprint, request, url_for -from flask_restx import fields from flask_security import roles_accepted from marshmallow import ValidationError, INCLUDE, EXCLUDE from werkzeug.exceptions import UnprocessableEntity, NotFound, BadRequest @@ -11,40 +10,27 @@ from app.celery.word.tasks import export_user_data_in_word from app.celery.excel.tasks import export_user_data_in_excel from app.celery.tasks import create_user_email -from config import Config from .base import BaseResource -from .roles import role_sw_model from ..extensions import db_wrapper, api as root_api from ..models.user import User as UserModel, user_datastore from ..models.role import Role as RoleModel from ..utils.cerberus_schema import user_model_schema, search_model_schema from ..utils.decorators import token_required -from ..utils.marshmallow_schema import UserSchema as UserSerializer, ExportWordInputSchema as ExportWordInputSerializer +from ..utils.marshmallow_schema import (UserSchema as UserSerializer, + ExportWordInputSchema as + ExportWordInputSerializer) +from ..utils.swagger_models import SEARCH_INPUT_SW_MODEL +from ..utils.swagger_models.user import (USER_INPUT_SW_MODEL, + USER_OUTPUT_SW_MODEL, + USER_SEARCH_OUTPUT_SW_MODEL) -blueprint = Blueprint('users', __name__, ) -api = root_api.namespace('users', description='Users endpoints. Users with role admin or team_leader can manage these endpoints.') +_API_DESCRIPTION = ('Users with role admin or team_leader can manage ' + 'these endpoints.') +blueprint = Blueprint('users', __name__, ) +api = root_api.namespace('users', description=_API_DESCRIPTION) logger = logging.getLogger(__name__) -creator_sw_model = api.model('Creator', { - 'id': fields.Integer, -}) - -user_sw_model = api.model('User', { - 'id': fields.Integer, - 'name': fields.String, - 'last_name': fields.String, - 'email': fields.String, - 'genre': fields.String(enum=('m', 'f')), - 'birth_date': fields.String, - 'active': fields.Boolean, - 'created_at': fields.String, - 'updated_at': fields.String, - 'deleted_at': fields.String, - 'created_by': fields.Nested(creator_sw_model), - 'roles': fields.List(fields.Nested(role_sw_model)) -}) - class UserBaseResource(BaseResource): db_model = UserModel @@ -60,24 +46,11 @@ def deserialize_request_data(self, **kwargs: dict) -> dict: @api.route('') class NewUserResource(UserBaseResource): - _parser = api.parser() - _parser.add_argument(Config.SECURITY_TOKEN_AUTHENTICATION_HEADER, location='headers', required=True, - default='Bearer token') - _parser.add_argument('name', type=str, location='json', required=True) - _parser.add_argument('last_name', type=str, location='json', required=True) - _parser.add_argument('email', type=str, location='json', required=True) - _parser.add_argument('genre', type=str, location='json', required=True) - _parser.add_argument('password', type=str, location='json', required=True) - _parser.add_argument('birth_date', type=str, location='json', required=True) - _parser.add_argument('role_id', type=int, location='json', required=True) - - @api.doc(responses={ - 201: ('Success', user_sw_model), - 401: 'Unauthorized', - 403: 'Forbidden', - 422: 'Unprocessable Entity', - }) - @api.expect(_parser) + @api.doc(responses={401: 'Unauthorized', 403: 'Forbidden', + 422: 'Unprocessable Entity'}, + security='auth_token') + @api.expect(USER_INPUT_SW_MODEL) + @api.marshal_with(USER_OUTPUT_SW_MODEL, code=201) @token_required @roles_accepted('admin', 'team_leader') def post(self) -> tuple: @@ -94,25 +67,17 @@ def post(self) -> tuple: user_data = self.user_serializer.dump(user) create_user_email.delay(user_data) + logger.debug(user_data) - return { - 'data': user_data, - }, 201 + return {'data': user_data}, 201 @api.route('/') class UserResource(UserBaseResource): - _parser = api.parser() - _parser.add_argument(Config.SECURITY_TOKEN_AUTHENTICATION_HEADER, location='headers', required=True, - default='Bearer token') - - @api.doc(responses={ - 200: ('Success', user_sw_model), - 401: 'Unauthorized', - 403: 'Forbidden', - 422: 'Unprocessable Entity', - }) - @api.expect(_parser) + @api.doc(responses={401: 'Unauthorized', 403: 'Forbidden', + 422: 'Unprocessable Entity'}, + security='auth_token') + @api.marshal_with(USER_OUTPUT_SW_MODEL) @token_required @roles_accepted('admin', 'team_leader') def get(self, user_id: int) -> tuple: @@ -122,29 +87,13 @@ def get(self, user_id: int) -> tuple: user_data = self.user_serializer.dump(user) - return { - 'data': user_data, - }, 200 + return {'data': user_data}, 200 - _parser = api.parser() - _parser.add_argument(Config.SECURITY_TOKEN_AUTHENTICATION_HEADER, location='headers', required=True, - default='Bearer token') - _parser.add_argument('name', type=str, location='json') - _parser.add_argument('last_name', type=str, location='json') - _parser.add_argument('email', type=str, location='json') - _parser.add_argument('genre', type=str, location='json') - _parser.add_argument('password', type=str, location='json') - _parser.add_argument('birth_date', type=str, location='json') - _parser.add_argument('role_id', type=int, location='json') - - @api.doc(responses={ - 200: ('Success', user_sw_model), - 400: 'Bad Request', - 401: 'Unauthorized', - 403: 'Forbidden', - 422: 'Unprocessable Entity', - }) - @api.expect(_parser) + @api.doc(responses={400: 'Bad Request', 401: 'Unauthorized', + 403: 'Forbidden', 422: 'Unprocessable Entity'}, + security='auth_token') + @api.expect(USER_INPUT_SW_MODEL) + @api.marshal_with(USER_OUTPUT_SW_MODEL) @token_required @roles_accepted('admin', 'team_leader') def put(self, user_id: int) -> tuple: @@ -174,22 +123,12 @@ def put(self, user_id: int) -> tuple: UserModel.deleted_at.is_null())) user_data = self.user_serializer.dump(user) - return { - 'data': user_data, - }, 200 + return {'data': user_data}, 200 - _parser = api.parser() - _parser.add_argument(Config.SECURITY_TOKEN_AUTHENTICATION_HEADER, location='headers', required=True, - default='Bearer token') - - @api.doc(responses={ - 200: ('Success', role_sw_model), - 400: 'Bad Request', - 401: 'Unauthorized', - 403: 'Forbidden', - 422: 'Unprocessable Entity', - }) - @api.expect(_parser) + @api.doc(responses={400: 'Bad Request', 401: 'Unauthorized', + 403: 'Forbidden', 422: 'Unprocessable Entity'}, + security='auth_token') + @api.marshal_with(USER_OUTPUT_SW_MODEL) @token_required @roles_accepted('admin', 'team_leader') def delete(self, user_id: int) -> tuple: @@ -204,9 +143,7 @@ def delete(self, user_id: int) -> tuple: user.save() user_data = self.user_serializer.dump(user) - return { - 'data': user_data, - }, 200 + return {'data': user_data}, 200 @api.route('/search') @@ -214,21 +151,11 @@ class UsersSearchResource(UserBaseResource): user_fields = UserModel.get_fields(exclude=['id', 'password']) request_validation_schema = search_model_schema(user_fields) - _parser = api.parser() - _parser.add_argument(Config.SECURITY_TOKEN_AUTHENTICATION_HEADER, location='headers', required=True, - default='Bearer token') - _parser.add_argument('search', type=list, location='json') - _parser.add_argument('order', type=list, location='json') - _parser.add_argument('items_per_page', type=int, location='json') - _parser.add_argument('page_number', type=int, location='json') - - @api.doc(responses={ - 200: 'Success', - 401: 'Unauthorized', - 403: 'Forbidden', - 422: 'Unprocessable Entity', - }) - @api.expect(_parser) + @api.doc(responses={200: 'Success', 401: 'Unauthorized', 403: 'Forbidden', + 422: 'Unprocessable Entity'}, + security='auth_token') + @api.expect(SEARCH_INPUT_SW_MODEL) + @api.marshal_with(USER_SEARCH_OUTPUT_SW_MODEL) @token_required @roles_accepted('admin', 'team_leader') def post(self) -> tuple: @@ -260,32 +187,25 @@ class ExportUsersExcelResource(UserBaseResource): user_fields = UserModel.get_fields(exclude=['id', 'password']) request_validation_schema = search_model_schema(user_fields) - _parser = api.parser() - _parser.add_argument(Config.SECURITY_TOKEN_AUTHENTICATION_HEADER, location='headers', required=True, - default='Bearer token') - _parser.add_argument('search', type=list, location='json') - _parser.add_argument('order', type=list, location='json') - _parser.add_argument('items_per_page', type=int, location='json') - _parser.add_argument('page_number', type=int, location='json') - - @api.doc(responses={ - 202: 'Accepted', - 401: 'Unauthorized', - 403: 'Forbidden', - 422: 'Unprocessable Entity', - }) - @api.expect(_parser) + @api.doc(responses={202: 'Accepted', 401: 'Unauthorized', 403: 'Forbidden', + 422: 'Unprocessable Entity'}, + security='auth_token') + @api.expect(SEARCH_INPUT_SW_MODEL) @token_required @roles_accepted('admin', 'team_leader', 'worker') def post(self) -> tuple: request_data = request.get_json() self.request_validation(request_data) - task = export_user_data_in_excel.apply_async((current_user.id, request_data), countdown=5) + task = export_user_data_in_excel.apply_async( + (current_user.id, request_data), + countdown=5 + ) return { 'task': task.id, - 'url': url_for('tasks_task_status_resource', task_id=task.id, _external=True), + 'url': url_for('tasks_task_status_resource', task_id=task.id, + _external=True), }, 202 @@ -294,21 +214,10 @@ class ExportUsersWordResource(UserBaseResource): user_fields = UserModel.get_fields(exclude=['id', 'password']) request_validation_schema = search_model_schema(user_fields) - _parser = api.parser() - _parser.add_argument(Config.SECURITY_TOKEN_AUTHENTICATION_HEADER, location='headers', required=True, - default='Bearer token') - _parser.add_argument('search', type=list, location='json') - _parser.add_argument('order', type=list, location='json') - _parser.add_argument('items_per_page', type=int, location='json') - _parser.add_argument('page_number', type=int, location='json') - - @api.doc(responses={ - 202: 'Accepted', - 401: 'Unauthorized', - 403: 'Forbidden', - 422: 'Unprocessable Entity', - }) - @api.expect(_parser) + @api.doc(responses={202: 'Accepted', 401: 'Unauthorized', 403: 'Forbidden', + 422: 'Unprocessable Entity'}, + security='auth_token') + @api.expect(SEARCH_INPUT_SW_MODEL) @token_required @roles_accepted('admin', 'team_leader', 'worker') def post(self) -> tuple: @@ -317,14 +226,18 @@ def post(self) -> tuple: try: serializer = ExportWordInputSerializer() - request_args = serializer.load(request.args.to_dict(), unknown=EXCLUDE) + request_args = serializer.load(request.args.to_dict(), + unknown=EXCLUDE) to_pdf = request_args.get('to_pdf', 0) except ValidationError as e: raise UnprocessableEntity(e.messages) - task = export_user_data_in_word.apply_async(args=[current_user.id, request_data, to_pdf]) + task = export_user_data_in_word.apply_async( + args=[current_user.id, request_data, to_pdf] + ) return { 'task': task.id, - 'url': url_for('tasks_task_status_resource', task_id=task.id, _external=True), + 'url': url_for('tasks_task_status_resource', task_id=task.id, + _external=True), }, 202 diff --git a/app/extensions.py b/app/extensions.py index a5b6716..26d5cfb 100644 --- a/app/extensions.py +++ b/app/extensions.py @@ -6,13 +6,23 @@ from playhouse.flask_utils import FlaskDB from app.celery import MyCelery +from config import Config db_wrapper = FlaskDB() security = Security() mail = Mail() celery = MyCelery() ma = Marshmallow() -api = Api(prefix='/api') + +authorizations = { + 'auth_token': { + 'type': 'apiKey', + 'in': 'header', + 'name': Config.SECURITY_TOKEN_AUTHENTICATION_HEADER, + }, +} +api = Api(prefix='/api', title='Flask Api', description='A simple TodoMVC API', + authorizations=authorizations) def init_app(app: Flask) -> None: @@ -23,24 +33,7 @@ def init_app(app: Flask) -> None: mail.init_app(app) ma.init_app(app) - authorizations = { - 'apikey': { - 'type': 'apiKey', - 'in': 'header', - 'name': app.config.get('SECURITY_TOKEN_AUTHENTICATION_HEADER') - }, - 'oauth2': { - 'type': 'oauth2', - 'flow': 'accessCode', - 'tokenUrl': 'https://somewhere.com/token', - 'authorizationUrl': 'https://somewhere.com/auth', - 'scopes': { - 'read': 'Grant read-only access', - 'write': 'Grant read-write access', - } - } - } - api.init_app(app, title='Flask Api', description='A simple TodoMVC API') + api.init_app(app) # This hook ensures that a connection is opened to handle any queries # generated by the request. diff --git a/app/middleware.py b/app/middleware.py index d3caac1..5a48e86 100644 --- a/app/middleware.py +++ b/app/middleware.py @@ -1,17 +1,16 @@ from flask import Request, Response, Flask -class middleware(): - """ - Simple WSGI middleware for checking if the request to the API has a valid content type. - """ +class Middleware: + """Simple WSGI middleware for checking if the request has a valid content type.""" def __init__(self, app: Flask): self.app = app.wsgi_app self.content_types = app.config.get('ALLOWED_CONTENT_TYPES') self.accept = ('application/json') - def _parse_content_type(self, request_content_type: any) -> str: + @staticmethod + def _parse_content_type(request_content_type: any) -> str: """ Content-Type := type "/" subtype *[";" parameter] https://tools.ietf.org/html/rfc1341 @@ -19,8 +18,8 @@ def _parse_content_type(self, request_content_type: any) -> str: parsed_content_type = '' if isinstance(request_content_type, str): - parsed_content_type = request_content_type.split(';')[0] if request_content_type.find( - ';') else request_content_type + parsed_content_type = request_content_type.split(';')[0] \ + if request_content_type.find(';') else request_content_type return parsed_content_type @@ -35,7 +34,8 @@ def __call__(self, environ, start_response): if content_type in self.content_types or accept_mimetypes: return self.app(environ, start_response) - response = Response('{"message": "Content type no valid"}', mimetype='aplication/json', + response = Response('{"message": "Content type no valid"}', + mimetype='aplication/json', status=400) return response(environ, start_response) return self.app(environ, start_response) diff --git a/app/utils/__init__.py b/app/utils/__init__.py index 4d36be2..25acea3 100644 --- a/app/utils/__init__.py +++ b/app/utils/__init__.py @@ -38,6 +38,7 @@ """ REQUEST_QUERY_DELIMITER = ';' + class FileEmptyError(OSError): pass @@ -103,7 +104,7 @@ def get_request_query_fields(db_model: Type[Model], request_data=None) -> tuple: items_per_page = int(request_data.get('items_per_page', 10)) order_by = _build_order_by(db_model, request_data) - return (page_number, items_per_page, order_by,) + return page_number, items_per_page, order_by def _build_string_clause(field: Field, field_operator: str, field_value) -> tuple: diff --git a/app/utils/cerberus_schema.py b/app/utils/cerberus_schema.py index 2336c3a..4de5354 100644 --- a/app/utils/cerberus_schema.py +++ b/app/utils/cerberus_schema.py @@ -258,13 +258,6 @@ def role_model_schema(is_creation: bool = True) -> dict: 'only_deleted': False, }, }, - 'label': { - 'type': 'string', - 'required': is_creation, - 'empty': False, - 'nullable': False, - 'maxlength': 255, - }, 'description': { 'type': 'string', 'required': False, @@ -272,6 +265,13 @@ def role_model_schema(is_creation: bool = True) -> dict: 'nullable': True, 'maxlength': 255, }, + 'label': { + 'type': 'string', + 'required': is_creation, + 'empty': False, + 'nullable': False, + 'maxlength': 255, + }, } diff --git a/app/utils/decorators.py b/app/utils/decorators.py index 3d9c9bd..3ed1b3a 100644 --- a/app/utils/decorators.py +++ b/app/utils/decorators.py @@ -3,7 +3,7 @@ from flask import current_app, request from flask_security.passwordless import login_token_status -from werkzeug.exceptions import NotFound, Forbidden, BadRequest, Unauthorized +from werkzeug.exceptions import Forbidden, Unauthorized from app.utils import TOKEN_REGEX @@ -26,9 +26,9 @@ def decorator(*args, **kwargs): return fnc(*args, **kwargs) else: raise Forbidden('User is not active') - elif invalid: - raise BadRequest('Token is invalid') + elif expired: + raise Unauthorized('Token has expired') else: - raise BadRequest('Token has expired') + raise Unauthorized('Unauthorized') return decorator diff --git a/app/utils/swagger_models/__init__.py b/app/utils/swagger_models/__init__.py new file mode 100644 index 0000000..3c051f2 --- /dev/null +++ b/app/utils/swagger_models/__init__.py @@ -0,0 +1,29 @@ +from flask_restx import fields + +from app.extensions import api + + +CREATOR_SW_MODEL = api.model('Creator', { + 'id': fields.Integer(), +}) + +_SEARCH_SEARCH_INPUT_SW_MODEL = api.model('SearchSearch', { + 'field_name': fields.String(required=True), + 'field_operator': fields.String(required=True), + 'field_value': fields.String(required=True, + description='Could be string or integer.'), +}) + +_ORDER_DESCRIPTION = ('First value is the field name, second value is the ' + 'sort ( asc or desc ).') + +SEARCH_INPUT_SW_MODEL = api.model('SearchInput', { + 'search': fields.List(fields.Nested(_SEARCH_SEARCH_INPUT_SW_MODEL, + required=True)), + 'order': fields.List(fields.List(fields.String, + description=_ORDER_DESCRIPTION, + required=True), + example=[['name', 'asc'], ['size', 'desc']]), + 'items_per_page': fields.Integer(required=True), + 'page_number': fields.Integer(required=True), +}) diff --git a/app/utils/swagger_models/auth.py b/app/utils/swagger_models/auth.py new file mode 100644 index 0000000..317de7d --- /dev/null +++ b/app/utils/swagger_models/auth.py @@ -0,0 +1,21 @@ +from flask_restx import fields + +from app.extensions import api + + +AUTH_LOGIN_SW_MODEL = api.model('AuthUserLogin', { + 'email': fields.String(required=True), + 'password': fields.String(required=True), +}) + +AUTH_TOKEN_SW_MODEL = api.model('AuthUserToken', { + 'token': fields.String(), +}) + +AUTH_REQUEST_RESET_PASSWORD_SW_MODEL = api.model('AuthUserResetPassword', { + 'email': fields.String(required=True), +}) + +AUTH_RESET_PASSWORD_SW_MODEL = api.model('AuthUserResetPasswordToken', { + 'password': fields.String(required=True), +}) diff --git a/app/utils/swagger_models/document.py b/app/utils/swagger_models/document.py new file mode 100644 index 0000000..ac51097 --- /dev/null +++ b/app/utils/swagger_models/document.py @@ -0,0 +1,28 @@ +from flask_restx import fields + +from app.extensions import api +from app.utils.swagger_models import CREATOR_SW_MODEL + + +DOCUMENT_SW_MODEL = api.model('Document', { + 'id': fields.Integer(), + 'name': fields.String, + 'internal_name': fields.String, + 'mime_type': fields.String, + 'size': fields.Integer, + 'url': fields.String, + 'created_at': fields.String(), + 'updated_at': fields.String(), + 'deleted_at': fields.String(), + 'created_by': fields.Nested(CREATOR_SW_MODEL), +}) + +DOCUMENT_OUTPUT_SW_MODEL = api.model('DocumentOutput', { + 'data': fields.Nested(DOCUMENT_SW_MODEL), +}) + +DOCUMENT_SEARCH_OUTPUT_SW_MODEL = api.model('DocumentSearch', { + 'data': fields.List(fields.Nested(DOCUMENT_SW_MODEL)), + 'records_total': fields.Integer, + 'records_filtered': fields.Integer, +}) diff --git a/app/utils/swagger_models/role.py b/app/utils/swagger_models/role.py new file mode 100644 index 0000000..a8715b3 --- /dev/null +++ b/app/utils/swagger_models/role.py @@ -0,0 +1,29 @@ +from flask_restx import fields + +from app.extensions import api + +ROLE_INPUT_SW_MODEL = api.model('RoleInput', { + 'name': fields.String(required=True), + 'description': fields.String, + 'label': fields.String(required=True), +}) + +ROLE_SW_MODEL = api.model('Role', { + 'id': fields.Integer(), + 'name': fields.String, + 'description': fields.String, + 'label': fields.String, + 'created_at': fields.String(), + 'updated_at': fields.String(), + 'deleted_at': fields.String(), +}) + +ROLE_OUTPUT_SW_MODEL = api.model('RoleOutput', { + 'data': fields.Nested(ROLE_SW_MODEL), +}) + +ROLE_SEARCH_OUTPUT_SW_MODEL = api.model('RoleSearchOutput', { + 'data': fields.List(fields.Nested(ROLE_SW_MODEL)), + 'records_total': fields.Integer, + 'records_filtered': fields.Integer, +}) diff --git a/app/utils/swagger_models/user.py b/app/utils/swagger_models/user.py new file mode 100644 index 0000000..80ff169 --- /dev/null +++ b/app/utils/swagger_models/user.py @@ -0,0 +1,44 @@ +from flask_restx import fields + +from app.extensions import api +from app.utils.swagger_models import CREATOR_SW_MODEL + +USER_INPUT_SW_MODEL = api.model('UserInput', { + 'name': fields.String(required=True), + 'last_name': fields.String(required=True), + 'email': fields.String(required=True), + 'genre': fields.String(required=True), + 'password': fields.String(required=True), + 'birth_date': fields.String(required=True), + 'role_id': fields.Integer(required=True), +}) + +USER_ROLE_OUTPUT_SW_MODEL = api.model('UserRoleOutput', { + 'name': fields.String(readonly=True), + 'label': fields.String(readonly=True), +}) + +USER_SW_MODEL = api.model('User', { + 'id': fields.Integer(), + 'name': fields.String, + 'last_name': fields.String, + 'email': fields.String, + 'genre': fields.String(enum=('m', 'f')), + 'birth_date': fields.String, + 'active': fields.Boolean, + 'created_at': fields.String(), + 'updated_at': fields.String(), + 'deleted_at': fields.String(), + 'created_by': fields.Nested(CREATOR_SW_MODEL), + 'roles': fields.List(fields.Nested(USER_ROLE_OUTPUT_SW_MODEL)) +}) + +USER_OUTPUT_SW_MODEL = api.model('UserOutput', { + 'data': fields.Nested(USER_SW_MODEL), +}) + +USER_SEARCH_OUTPUT_SW_MODEL = api.model('UserSearchOutput', { + 'data': fields.List(fields.Nested(USER_SW_MODEL)), + 'records_total': fields.Integer, + 'records_filtered': fields.Integer, +}) diff --git a/config.py b/config.py index 5748aa2..a475741 100644 --- a/config.py +++ b/config.py @@ -86,18 +86,24 @@ class Config(metaclass=Meta): CELERY_INCLUDE = ['app.celery.tasks'] CELERY_TASK_TRACK_STARTED = True CELERY_RESULT_EXPIRES = 3600 - CELERY_WORKER_LOG_FORMAT = '%(asctime)s - %(levelname)s - %(processName)s - %(message)s' - CELERY_WORKER_TASK_LOG_FORMAT = '%(asctime)s - %(levelname)s - %(processName)s - %(' \ - 'task_name)s - %(task_id)s - %(message)s' + CELERY_WORKER_LOG_FORMAT = ('%(asctime)s - %(levelname)s - ' + '%(processName)s - %(message)s') + CELERY_WORKER_TASK_LOG_FORMAT = ('%(asctime)s - %(levelname)s - ' + '%(processName)s - %(task_name)s - ' + '%(task_id)s - %(message)s') CELERY_RESULT_EXTENDED = True CELERY_TASK_DEFAULT_RATE_LIMIT = 3 # Flask Swagger UI SWAGGER_URL = os.getenv('SWAGGER_URL', '/docs') - SWAGGER_API_URL = os.getenv('SWAGGER_API_URL', f'http://{SERVER_NAME}/static/swagger.yaml') + SWAGGER_API_URL = os.getenv( + 'SWAGGER_API_URL', f'http://{SERVER_NAME}/static/swagger.yaml' + ) # Flask Restful + ERROR_404_HELP = False FLASK_RESTFUL_PREFIX = '/api' + RESTX_MASK_SWAGGER = False # Mr Developer HOME = os.getenv('HOME') diff --git a/tests/blueprints/test_auth.py b/tests/blueprints/test_auth.py index 0f9d8b6..a711cdf 100644 --- a/tests/blueprints/test_auth.py +++ b/tests/blueprints/test_auth.py @@ -71,7 +71,6 @@ def _test_login(): _test_login() - def test_user_logout(client: FlaskClient, auth_header: any): with client: response = client.post('/api/auth/logout', json={}, headers=auth_header()) @@ -87,7 +86,7 @@ def test_request_reset_password(client: FlaskClient): response = client.post('/api/auth/reset_password', json=data) - assert 200 == response.status_code + assert 202 == response.status_code def test_validate_reset_password(client: FlaskClient, app: Flask): diff --git a/tests/blueprints/test_documents.py b/tests/blueprints/test_documents.py index b0e81a9..db5e454 100644 --- a/tests/blueprints/test_documents.py +++ b/tests/blueprints/test_documents.py @@ -24,7 +24,7 @@ def test_save_document(client: FlaskClient, auth_header: any): parse_url = urlparse(json_data.get('url')) - assert 200 == response.status_code + assert 201 == response.status_code assert 1 == json_data.get('created_by').get('id') assert pdf_file == json_data.get('name') assert 'application/pdf' == json_data.get('mime_type') diff --git a/tests/conftest.py b/tests/conftest.py index c2741df..288ec53 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -34,15 +34,13 @@ def app(): init_seed() yield app - """ - TODO: add these code before/after all tests are executed storage_path = app.config.get('STORAGE_DIRECTORY') _remove_test_files(storage_path) logger.info(' Deleting test database...') os.remove(app.config.get('DATABASE').get('name')) logger.info(' Deleted test database!') - """ + @pytest.fixture def client(app: Flask): @@ -133,7 +131,7 @@ def _create_auth_header(user_email: str = None) -> dict: token = json_response.get('token') return { - app.config.get('SECURITY_TOKEN_AUTHENTICATION_HEADER'): 'Bearer %s' % token, + app.config.get('SECURITY_TOKEN_AUTHENTICATION_HEADER'): token, } return _create_auth_header