diff --git a/.gitignore b/.gitignore index 02a86bf..5e6d4bf 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,7 @@ instance/__pycache__ tests/__pycache__ .vscode/settings.json migrations/ - +.DS_Store +.cache/ +.coverage +cover/ diff --git a/README.md b/README.md index 1db58a6..51cc942 100644 --- a/README.md +++ b/README.md @@ -76,30 +76,31 @@ Interact with the API: -), send http requests using Postman ## User API Endpoints - URL Endpoint | HTTP requests | access | status | - ---------------- | ----------------- | ------------- | ------------------ - / flask_api / v1 / auth / register | POST | Register a new user | publc - / flask_api / v1 / auth / login | POST | Login and retrieve token | public +|URL Endpoint |HTTP requests |access |status| +---------------- | ----------------- | ------------- | ------------------ +|/flask_api/v1/auth/register | POST | Register a new user | public| +|/flask_api/v1/auth/login | POST | Login and retrieve token | public| + ## Categories API Endpoints - URL Endpoint | HTTP requests | access | status | - ---------------- | ----------------- | ------------- | ------------------ - / flask_api / v1 / categories / | POST | Create a new recipe category | private - / flask_api / v1 / categories / | GET | Retrieve all categories for user | private - / flask_api / v1 / categories / search /?q= & limit & page | GET | Retrieve all categories for a given search | private - / flask_api / v1 / categories / / | GET | Retrieve a category by ID | private - / flask_api / v1 / categories / / | PUT | Update a category | private - / flask_api / v1 / categories / / | DELETE | Delete a category | private +URL Endpoint | HTTP requests | access | status | +---------------- | ----------------- | ------------- | ------------------ +/flask_api/v1/categories/ | POST | Create a new recipe category | private +/flask_api/v1/categories/ | GET | Retrieve all categories for user | private +/flask_api/v1/categories/search/?q= & limit & page | GET | Retrieve all categories for a given search | private +/flask_api/v1/categories// | GET | Retrieve a category by ID | private +/flask_api/v1/categories// | PUT | Update a category | private +/flask_api/v1/categories// | DELETE | Delete a category | private ## Recipes API Endpoints - URL Endpoint | HTTP requests | access | status | - ---------------- | ----------------- | ------------- | ------------------ - / flask_api / v1 / categories / /recipes / | GET | Retrive recipes in a given category | private - / flask_api / v1 / categories / /recipes / | POST | Create recipes in a category | private - / flask_api / v1 / categories / /recipes / search /?q= & limit & page | GET | Retrieve all recipes for a given search | private - / flask_api / v1 / categories / /recipes / / | DELETE | Delete a recipe in a category | private - / flask_api / v1 / categories / /recipes / / | PUT | update recipe details | private +URL Endpoint | HTTP requests | access | status | +---------------- | ----------------- | ------------- | ------------------ +/flask_api/v1/categories//recipes/ | GET | Retrive recipes in a given category | private +/flask_api/v1/categories//recipes/ | POST | Create recipes in a category | private +/flask_api/v1/categories//recipes/search/?q=&limit&page | GET | Retrieve all recipes for a given search | private +/flask_api/v1/categories//recipes//| DELETE | Delete a recipe in a category | private +/flask_api/v1/categories//recipes// | PUT | update recipe details | private Run the APIs on postman to ensure they are fully functioning. diff --git a/app/__init__.py b/app/__init__.py index 7464c61..6376f6a 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,6 +1,7 @@ from flask_api import FlaskAPI from flask_sqlalchemy import SQLAlchemy from flasgger import Swagger +from flask import make_response, jsonify, abort # local import @@ -70,4 +71,23 @@ def make_app(config_name): view_func=user_password_reset_view, ) app.add_url_rule(base_url + '/auth/logout', view_func=user_logout_view) + @app.errorhandler(404) + def not_found(error): + """handles error when users enters inappropriate endpoint + """ + response = { + 'message': 'Page not found' + } + return make_response(jsonify(response)), 404 + + @app.errorhandler(405) + def method_not_allowed(error): + """ handles errors if users uses method that is not allowed in an endpoint + """ + response = { + 'message': 'Method not allowed' + } + return make_response(jsonify(response)), 405 + return app + diff --git a/app/auth/authentication.py b/app/auth/authentication.py index 8eea7c4..fc9bf40 100644 --- a/app/auth/authentication.py +++ b/app/auth/authentication.py @@ -1,11 +1,12 @@ """Class to deal with user authenticatication """ -from app.decorators import token_required +from app.helpers.decorators import token_required from app.models import User, Sessions -import re from flask import request, jsonify, make_response from flask.views import MethodView from app import db +from app.helpers.auth_validators import user_registration_validation, \ + user_login_validation, password_reset_validation class RegisterUser(MethodView): @@ -46,62 +47,25 @@ def post(self): 400: description: Bad Requests """ - regex_email = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z-.]+$)" - regex_username = "[a-zA-Z0-9- .]+$" + try: user_details = User.query.filter_by( email=request.data['email'].lower()).first() if not user_details: + email = str(request.data.get('email', '')) + password = str(request.data.get('password', '')) + username = str(request.data.get('username', '')) + secret = str(request.data.get('secret_word', '')) try: - email = str(request.data.get('email', '')) - password = str(request.data.get('password', '')) - username = str(request.data.get('username', '')) - secret = str(request.data.get('secret_word', '')) - - if email: - email = re.sub(r'\s+', ' ', email).strip() - email = None if email == " " else email.lower() - - if username: - username = re.sub(r'\s+', ' ', username).strip() - username = None if username == " " else username.title() - - if secret: - secret = re.sub(r'\s+', ' ', secret).strip() - secret = None if secret == " " else secret - - if not email or not password or not username: - response = { - 'message': "Kindly Provide all required details"} - return make_response(jsonify(response)), 422 - if not secret: - response = { - 'message': "Kindly Provide a secret word"} - return make_response(jsonify(response)), 422 - if not re.search(regex_email, email): - response = {'message': "Email pattern not valid"} - return make_response(jsonify(response)), 400 - - if not re.search(regex_username, username): - response = { - 'message': "No special characters allowed on username"} - return make_response(jsonify(response)), 400 - - if len(password) < 6: - response = {'message': "Password must be at least six characters"} - return make_response(jsonify(response)), 400 - - try: - user = User(email=email, password=password, - username=username, secret_word=secret) - user.save() - response = {'message': "Successfully registered"} - except Exception as e: - response = {'message': "User Exists, Kindly Login"} - return make_response(jsonify(response)), 409 + user_registration_validation(email, username, password, secret) + user = User(email=email, password=password, + username=username, secret_word=secret) + user.save() + response = {'message': "Successfully registered"} return make_response(jsonify(response)), 201 + except Exception as e: response = {'message': str(e)} return make_response(jsonify(response)), 400 @@ -151,10 +115,7 @@ def post(self): email = request.data['email'] password = request.data['password'] user_details = User.query.filter_by(email=email).first() - - if not email or not password: - response = {'message': "Kindly Provide email and password"} - return make_response(jsonify(response)), 422 + user_login_validation(email, password) if user_details and user_details.password_check(password): access_token = user_details.user_token_generator(user_details.id) @@ -162,6 +123,7 @@ def post(self): if access_token: response = { 'message': 'Successful Login', + 'status': 'success', 'access_token': access_token.decode() } return make_response(jsonify(response)), 200 @@ -215,18 +177,13 @@ def put(self): """ try: + email = str(request.data.get('email', '')) user_details = User.query.filter_by( - email=request.data['email']).first() + email=email).first() reset_password = str(request.data.get('reset_password', '')) secret_word = str(request.data.get('secret_word', '')) + password_reset_validation(email, reset_password, secret_word) - if reset_password: - reset_password = re.sub(r'\s+', ' ', reset_password).strip() - reset_password = None if reset_password == " " else reset_password - - if not reset_password: - response = {"message": "No password provided"} - return make_response(jsonify(response)), 400 if user_details and user_details.secret_word_check(secret_word): res_password = User.password_hash(reset_password) user_details.password = res_password diff --git a/app/classes/categories.py b/app/classes/categories.py index b1629d9..378e603 100644 --- a/app/classes/categories.py +++ b/app/classes/categories.py @@ -1,10 +1,10 @@ """Class to handle category creation, viewing and manipulation """ -import re -from app.decorators import token_required +from app.helpers.decorators import token_required from app.models import Categories from flask import request, jsonify, make_response from flask.views import MethodView +from app.helpers.category_validators import category_validation class Category(MethodView): @@ -29,12 +29,13 @@ def post(self, current_user): required: true type: string schema: - id: categories + id: categories create properties: category_name: type: string default: Breakfast + responses: 200: schema: @@ -43,6 +44,18 @@ def post(self, current_user): category_name: type: string default: Breakfast + created_by: + type: integer + default: 2 + date_created: + type: string + default: Wed 20 Dec + date_modified: + type: string + default: Wed 20 Dec + id: + type: integer + default: 1 400: description: category name not valid 400: @@ -54,42 +67,32 @@ def post(self, current_user): 201: description: category created """ - regex_pattern = "[a-zA-Z0-9- .]+$" - category_name = str(request.data.get('category_name')) - - if category_name: - category_name = re.sub(r'\s+', ' ', category_name).strip() - - category_name = None if category_name == " " else category_name.title() - if not category_name: - response = {'message': 'category name not provided'} - return make_response(jsonify(response)), 400 - - if not re.search(regex_pattern, category_name): - response = {'message': 'Category name is not valid'} - return make_response(jsonify(response)), 400 - - category_details = Categories.query.filter_by( - category_name=category_name, created_by=current_user.id).first() - - if category_details: - response = {'message': 'Category name exists'} + try: + category_name = str(request.data.get('category_name', '')) + category_details = Categories.query.filter_by( + category_name=category_name, created_by=current_user.id).first() + category_validation(category_name) + if category_details: + response = {'message': 'Category name exists'} + return make_response(jsonify(response)), 400 + + category = Categories( + category_name=category_name, created_by=current_user.id) + category.save() + response = jsonify({ + 'id': category.id, + 'category_name': category.category_name, + 'created_by': category.created_by, + 'date_created': category.date_created, + 'date_modified': category.date_modified + }) + response.status_code = 201 + return response + except Exception as e: + response = {'message': str(e)} return make_response(jsonify(response)), 400 - category = Categories( - category_name=category_name, created_by=current_user.id) - category.save() - response = jsonify({ - 'id': category.id, - 'category_name': category.category_name, - 'created_by': category.created_by, - 'date_created': category.date_created, - 'date_modified': category.date_modified - }) - response.status_code = 201 - return response - def get(self, current_user): """Method to get all categories of a user in a paginated way --- @@ -282,47 +285,39 @@ def put(self, current_user, id): 200: description: OK """ - regex_pattern = "[a-zA-Z0-9- .]+$" - - category = Categories.query.filter_by( - id=id, created_by=current_user.id).first() - category_name = str(request.data.get('category_name', '')) - if category_name: - category_name = re.sub(r'\s+', ' ', category_name).strip() + try: + category = Categories.query.filter_by( + id=id, created_by=current_user.id).first() + category_name = str(request.data.get('category_name', '')) + category_validation(category_name) - category_name = None if category_name == " " else category_name.title() + if not category: + response = {'message': 'Category does not exist'} + return make_response(jsonify(response)), 404 - if not category_name: - response = {'message': 'category name not provided'} - return make_response(jsonify(response)), 400 - - if not re.search(regex_pattern, category_name): - response = {'message': 'Category name is not valid'} - return make_response(jsonify(response)), 400 - - if not category: - response = {'message': 'Category does not exist'} - return make_response(jsonify(response)), 404 - - category_details = Categories.query.filter_by( + category_details = Categories.query.filter_by( category_name=category_name, created_by=current_user.id).first() - if category_details: - response = {'message': 'Category name exists'} - return make_response(jsonify(response)), 400 + if category_details: + response = {'message': 'Category name exists', + 'status': 'fail'} + return make_response(jsonify(response)), 400 - category.category_name = category_name - category.save() - response = jsonify({ - 'id': category.id, - 'category_name': category.category_name, - 'created_by': category.created_by, - 'date_created': category.date_created, - 'date_modified': category.date_modified - }) - response.status_code = 200 - return response + category.category_name = category_name + category.save() + response = jsonify({ + 'id': category.id, + 'category_name': category.category_name, + 'created_by': category.created_by, + 'date_created': category.date_created, + 'date_modified': category.date_modified + }) + response.status_code = 200 + return response + except Exception as e: + response = {'message': str(e)} + return make_response(jsonify(response)), 400 def delete(self, current_user, id): """Method to delete a single category using its category id @@ -356,13 +351,15 @@ def delete(self, current_user, id): category = Categories.query.filter_by( id=id, created_by=current_user.id).first() if not category: - response = {'message': 'Category does not exist'} + response = {'message': 'Category does not exist', + 'status': 'error'} return make_response(jsonify(response)), 404 else: category.delete_categories() - return { - "message": "successfully deleted category" .format(category.id) - }, 200 + response = {'message': 'successfully deleted category', + 'status': 'success', + 'id': '{}'.format(category.id)} + return make_response(jsonify(response)), 200 class SearchCategory(MethodView): @@ -452,7 +449,8 @@ def get(self, current_user): results.append(category_object) return make_response(jsonify(results)), 200 else: - response = {'message': 'No search item provided'} + response = {'message': 'No search item provided', + 'status': 'error'} return make_response(jsonify(response)), 200 diff --git a/app/classes/recipes.py b/app/classes/recipes.py index 391da52..dddaaf9 100644 --- a/app/classes/recipes.py +++ b/app/classes/recipes.py @@ -2,10 +2,11 @@ """ import re -from app.decorators import token_required +from app.helpers.decorators import token_required from app.models import Recipes from flask import request, jsonify, make_response from flask.views import MethodView +from app.helpers.recipe_validators import recipe_validation class Recipe(MethodView): @@ -56,7 +57,6 @@ def post(self, current_user, id): 404: description: Category does not exist """ - regex_pattern = "[a-zA-Z0-9- .]+$" category_id = id if category_id: try: @@ -64,32 +64,21 @@ def post(self, current_user, id): recipe_ingredients = request.data.get('recipe_ingredients', '') recipe_methods = request.data.get('recipe_methods', '') - if recipe_name: - recipe_name = re.sub(r'\s+', ' ', recipe_name).strip() - recipe_name = None if recipe_name == "" else recipe_name.title() - - if not recipe_name: - response = {'message': 'Recipe name not provided'} - return make_response(jsonify(response)), 400 - if not recipe_ingredients: - response = {'message': 'Recipe ingredients not provided'} - return make_response(jsonify(response)), 400 - if not recipe_methods: - response = { - 'message': 'Recipe preparation methods not provided'} - return make_response(jsonify(response)), 400 - - if not re.search(regex_pattern, recipe_name): - response = {'message': 'Recipe name is not valid'} + try: + recipe_validation(recipe_name, recipe_methods, recipe_ingredients) + except Exception as e: + response = {'message': str(e)} return make_response(jsonify(response)), 400 recipe = Recipes(recipe_name=recipe_name, recipe_ingredients=recipe_ingredients, - recipe_methods=recipe_methods, category_id=category_id) + recipe_methods=recipe_methods, category_id=category_id, created_by=current_user.id) + recipe_details = Recipes.query.filter_by( - category_id=category_id, recipe_name=recipe_name).first() + category_id=category_id, recipe_name=recipe_name, created_by=current_user.id).first() if recipe_details: - response = {'message': 'Recipe name exists'} + response = {'message': 'Recipe name exists', + 'status': 'fail'} return make_response(jsonify(response)), 400 recipe.save() response = {'id': recipe.id, @@ -104,7 +93,8 @@ def post(self, current_user, id): return response except Exception: - response = {'message': 'Category does not exist'} + response = {'message': 'Category does not exist', + 'status': 'error'} return make_response(jsonify(response)), 404 def get(self, current_user, id): @@ -163,7 +153,8 @@ def get(self, current_user, id): results.append(recipe_obj) if len(results) <= 0: - response = {'message': 'No recipe found '} + response = {'message': 'No recipe found ', + 'status': 'error'} response = make_response(jsonify(response)), 404 return response response = jsonify(results) @@ -210,7 +201,8 @@ def get(self, current_user, id, recipe_id): """ recipe = Recipes.query.filter_by(category_id=id, id=recipe_id).first() if not recipe: - response = {'message': 'No recipe found'} + response = {'message': 'No recipe found', + 'status': 'error'} response = make_response(jsonify(response)), 404 return response else: @@ -264,52 +256,39 @@ def put(self, current_user, id, recipe_id): 201: description: Successfully edited a recipe """ - regex_pattern = "[a-zA-Z0-9- .]+$" - category_id = id - recipe = Recipes.query.filter_by( - category_id=category_id, id=recipe_id).first() - recipe_name = str(request.data.get('recipe_name', '')) - recipe_ingredients = str(request.data.get('recipe_ingredients', '')) - recipe_methods = str(request.data.get('recipe_methods', '')) - - if recipe_name: - recipe_name = re.sub(r'\s+', ' ', recipe_name).strip() - recipe_name = None if recipe_name == "" else recipe_name.title() - - if not recipe_name: - response = {'message': 'Recipe name not provided'} - return make_response(jsonify(response)), 400 - if not recipe_ingredients: - response = {'message': 'Recipe ingredients not provided'} - return make_response(jsonify(response)), 400 - if not recipe_methods: - response = {'message': 'Recipe preparation methods not provided'} - return make_response(jsonify(response)), 400 - - if not re.search(regex_pattern, recipe_name): - response = {'message': 'Recipe name is not valid'} + try: + category_id = id + recipe = Recipes.query.filter_by( + category_id=category_id, id=recipe_id, created_by=current_user.id).first() + recipe_name = str(request.data.get('recipe_name', '')) + recipe_ingredients = str(request.data.get('recipe_ingredients', '')) + recipe_methods = str(request.data.get('recipe_methods', '')) + recipe_validation(recipe_name,recipe_methods, recipe_ingredients) + + if not recipe: + response = {'message': 'No recipe found', + 'status': 'error'} + response = make_response(jsonify(response)), 404 + return response + else: + recipe.recipe_name = recipe_name + recipe.recipe_ingredients = recipe_ingredients + recipe.recipe_methods = recipe_methods + recipe.save() + response = {'id': recipe.id, + 'recipe_name': recipe.recipe_name, + 'recipe_ingredients': recipe.recipe_ingredients, + 'recipe_methods': recipe.recipe_methods, + 'category_id': recipe.category_id, + 'date_created': recipe.date_created, + 'date_modified': recipe.date_modified + } + response = make_response(jsonify(response)), 201 + return response + except Exception as e: + response = {'message': str(e)} return make_response(jsonify(response)), 400 - if not recipe: - response = {'message': 'No recipe found'} - response = make_response(jsonify(response)), 404 - return response - else: - recipe.recipe_name = recipe_name - recipe.recipe_ingredients = recipe_ingredients - recipe.recipe_methods = recipe_methods - recipe.save() - response = {'id': recipe.id, - 'recipe_name': recipe.recipe_name, - 'recipe_ingredients': recipe.recipe_ingredients, - 'recipe_methods': recipe.recipe_methods, - 'category_id': recipe.category_id, - 'date_created': recipe.date_created, - 'date_modified': recipe.date_modified - } - response = make_response(jsonify(response)), 201 - return response - def delete(self, current_user, id, recipe_id): """Method to delete a recipe in a category --- @@ -340,13 +319,16 @@ def delete(self, current_user, id, recipe_id): """ recipe = Recipes.query.filter_by(category_id=id, id=recipe_id).first() if not recipe: - response = {'message': 'No recipe found'} + response = {'message': 'No recipe found', + 'status': 'error'} response = make_response(jsonify(response)), 404 return response else: recipe.delete_recipes() response = { - "message": "successfully deleted category".format(recipe.id)} + "message": "successfully deleted recipe", + 'status': 'success', + "id": '{}' .format(recipe.id)} response = make_response(jsonify(response)), 200 return response @@ -422,7 +404,8 @@ def get(self, current_user, id): response.status_code = 200 return response else: - response = {'message': 'No search item provided'} + response = {'message': 'No search item provided', + 'status': 'error'} return make_response(jsonify(response)), 200 diff --git a/app/helpers/__init__.py b/app/helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/helpers/auth_validators.py b/app/helpers/auth_validators.py new file mode 100644 index 0000000..33fb7ec --- /dev/null +++ b/app/helpers/auth_validators.py @@ -0,0 +1,70 @@ +"""Class to validate user input +""" +from marshmallow import ValidationError +from validate_email import validate_email +import re + + +def user_registration_validation(email, username, password, secret): + """ + Method to check if email data is valid + """ + error = None + email = None if email == " " else email.lower() + valid_email = validate_email(email) + + if username: + username = re.sub(r'\s+', '', username).strip() + username = None if username == " " else username.title() + + if not email or not password or not username: + error = ValidationError('Email, password, username missing') + if not len(password) >= 6: + error = ValidationError('Password should be more than six characters') + if not secret: + error = ValidationError('Kindly provide a SECRET word') + if not username: + error = ValidationError('{} Kindly provide a username ') + if not valid_email: + error = ValidationError('{} is not a valid email'.format(email)) + + if error: + raise error + + +def user_login_validation(email, password): + """ + Method to validate login credentials of a user + """ + error = None + if not email or not password: + error = ValidationError('Kindly Provide email and Password') + + if error: + raise error + + +def password_reset_validation(email, reset_password, secret): + """Validators to check password Reset + """ + error = None + if reset_password: + reset_password = re.sub(r'\s+', '', reset_password).strip() + reset_password = None if reset_password == " " else reset_password + + if not reset_password: + error = ValidationError('Kindly provide a reset Password') + + if not email: + error = ValidationError('Invalid user email') + + if not secret: + error = ValidationError('Invalid Secret Word') + + if error: + raise error + + + + + diff --git a/app/helpers/category_validators.py b/app/helpers/category_validators.py new file mode 100644 index 0000000..653d292 --- /dev/null +++ b/app/helpers/category_validators.py @@ -0,0 +1,24 @@ +"""Methods to validate a users category +""" +from marshmallow import ValidationError +import re + + +def category_validation(category_name): + """Validation method for creating a category + """ + error = None + regex_pattern = "[a-zA-Z0-9- .]+$" + + if category_name: + category_name = re.sub(r'\s+', ' ', category_name).strip() + category_name = None if category_name == " " else category_name.title() + + if not category_name: + error = ValidationError('category name not valid') + + if not re.search(regex_pattern, category_name): + error = ValidationError('Category name is not valid') + + if error: + raise error diff --git a/app/decorators.py b/app/helpers/decorators.py similarity index 100% rename from app/decorators.py rename to app/helpers/decorators.py diff --git a/app/helpers/recipe_validators.py b/app/helpers/recipe_validators.py new file mode 100644 index 0000000..36fe571 --- /dev/null +++ b/app/helpers/recipe_validators.py @@ -0,0 +1,26 @@ +from marshmallow import ValidationError +import re + + +def recipe_validation(recipe, *argv): + error = None + regex_pattern = "[a-zA-Z0-9-]+$" + + if recipe: + recipe = re.sub(r'\s+', '', recipe).strip() + recipe = None if recipe == " " else recipe.title() + + if not re.search(regex_pattern, recipe): + error = ValidationError('recipe name cannot be empty or with invalid characters') + + for arg in argv: + if arg: + arg = re.sub(r'\s+', '', arg).strip() + arg = None if arg == " " else arg + + if not arg: + error = ValidationError('Kindly provide ingredients and methods') + + if error: + raise error + diff --git a/app/models.py b/app/models.py index fc7050d..a5c00a0 100644 --- a/app/models.py +++ b/app/models.py @@ -154,9 +154,10 @@ class Recipes(db.Model): date_modified = db.Column( db.DateTime, default=db.func.current_timestamp(), onupdate=db.func.current_timestamp()) + created_by = db.Column(db.Integer, db.ForeignKey(User.id)) category_id = db.Column(db.Integer, db.ForeignKey(Categories.id)) - def __init__(self, recipe_name, recipe_ingredients, recipe_methods, category_id): + def __init__(self, recipe_name, recipe_ingredients, recipe_methods, category_id, created_by): """ Constructor to initialize the class variables, category name and the owner @@ -165,6 +166,7 @@ def __init__(self, recipe_name, recipe_ingredients, recipe_methods, category_id) self.recipe_ingredients = recipe_ingredients self.recipe_methods = recipe_methods self.category_id = category_id + self.created_by = created_by def save(self): """ @@ -204,7 +206,8 @@ def __init__(self, auth_token): """ self.auth_token = auth_token - def check_logout_status(self, auth_token): + @staticmethod + def check_logout_status(auth_token): """Method to check if a user is logged out """ logout_state = Sessions.query.filter_by(auth_token=auth_token).first() diff --git a/requirements.txt b/requirements.txt index aa4d624..23294a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,6 +17,7 @@ flasgger==0.8.0 Flask==0.12.2 Flask-API==1.0 Flask-Bcrypt==0.7.1 +flask-marshmallow==0.8.0 Flask-Migrate==2.1.1 Flask-RESTful==0.3.6 Flask-Script==2.0.6 @@ -30,6 +31,7 @@ jsonschema==2.6.0 lazy-object-proxy==1.3.1 Mako==1.0.7 MarkupSafe==1.0 +marshmallow==2.15.0 mccabe==0.6.1 mistune==0.8.3 nose==1.3.7 @@ -50,6 +52,7 @@ requests==2.18.4 six==1.11.0 SQLAlchemy==1.1.14 urllib3==1.22 +validate-email==1.3 Werkzeug==0.12.2 wrapt==1.10.11 yapf==0.20.0 diff --git a/tests/test_auth.py b/tests/test_auth.py index 9b4478a..39cfcb3 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -41,15 +41,16 @@ def test_register_user(self): self.assertEqual(result['message'], "Successfully registered") self.assertEqual(user_register.status_code, 201) - def test_empty_email_and_password_on_register(self): - """Method to check for empty email or password strings on signup + def test_password_on_register(self): + """Method to check for empty username or password strings on signup """ user_register = self.client().post(base_url + '/register', - data={'email': '', 'password': '', 'username': ''}) - self.assertEqual(user_register.status_code, 422) + data={'email': 'n@n.com', 'password': '4324', 'username': 'new', + 'secret_word': 'secret'}) + self.assertEqual(user_register.status_code, 400) user_details = json.loads(user_register.data.decode()) self.assertEqual(user_details['message'], - "Kindly Provide all required details") + "Password should be more than six characters") def test_empty_secret_word_on_register(self): """Method to test for empty secret word on user register @@ -58,11 +59,10 @@ def test_empty_secret_word_on_register(self): base_url + '/register', data={'email': 'test@test.com', 'password': '32eq5646436rw', 'username': 'New_User', 'secret_word': ''}) - self.assertEqual(user_register.status_code, 422) + self.assertEqual(user_register.status_code, 400) user_details = json.loads(user_register.data.decode()) self.assertEqual(user_details['message'], - "Kindly Provide a secret word") - + "Kindly provide a SECRET word") def test_minimum_required_password_on_register(self): """Methdod to test for the minimum required password length on registration @@ -74,7 +74,7 @@ def test_minimum_required_password_on_register(self): self.assertEqual(user_register.status_code, 400) user_details = json.loads(user_register.data.decode()) self.assertEqual(user_details['message'], - "Password must be at least six characters") + "Password should be more than six characters") def test_to_email_regex_pattern_on_register(self): """Method to check for a valid regex pattern on registration @@ -83,20 +83,6 @@ def test_to_email_regex_pattern_on_register(self): data={'email': 't.com', 'password': '2324dsfscdsf', 'username': 'User', 'secret_word': 'TOP SECRET'}) self.assertEqual(user_register.status_code, 400) - user_details = json.loads(user_register.data.decode()) - self.assertEqual(user_details['message'], "Email pattern not valid") - - def test_username_regex_pattern(self): - """Method to check if username matches provided regex pattern - """ - user_register = self.client().post( - base_url + '/register', - data={'email': 'test@test.com', - 'password': '2324dsfscdsf', 'username': '$%$^', 'secret_word': 'TOP SECRET'}) - self.assertEqual(user_register.status_code, 400) - user_details = json.loads(user_register.data.decode()) - self.assertEqual(user_details['message'], - "No special characters allowed on username") def test_error_exception_on_user_register(self): """Method to check for error handling in registration @@ -113,10 +99,10 @@ def test_empty_email_and_password_on_login(self): user_login = self.client().post(base_url + '/login', data={'email': '', 'password': ''}) - self.assertEqual(user_login.status_code, 422) + self.assertEqual(user_login.status_code, 400) user_details = json.loads(user_login.data.decode()) self.assertEqual(user_details['message'], - "Kindly Provide email and password") + "Error occurred on user login") def test_double_registration(self): """"Method to test a user who is already registered @@ -152,9 +138,9 @@ def test_to_check_empty_email_in_reset_password_in_auth(self): base_url + '/password-reset', data={'email': '', 'reset_password': 'testing_reset_p@ssword', 'secret_word': 'TOP SECRET'}) - self.assertEqual(password_reset.status_code, 404) + self.assertEqual(password_reset.status_code, 400) user_data = json.loads(password_reset.data.decode()) - self.assertIn(user_data['message'], "Kindly provide correct email and secret word") + self.assertIn(user_data['message'], "Invalid user email") def test_empty_reset_password(self): """Method to test for empty password while doing a password reset @@ -166,7 +152,7 @@ def test_empty_reset_password(self): 'reset_password': '', 'secret_word': 'TOP SECRET'}) self.assertEqual(password_reset.status_code, 400) user_data = json.loads(password_reset.data.decode()) - self.assertIn(user_data['message'], "No password provided") + self.assertIn(user_data['message'], "Kindly provide a reset Password") def test_to_check_success_in_reseting_password(self): """Method to check for successfully updated user password @@ -247,3 +233,19 @@ def test_to_check_inexistent_user_email_on_password_reset(self): password_reset_data = json.loads(password_reset.data.decode()) self.assertIn( password_reset_data['message'], '"Kindly provide correct email and secret word"') + + def test_to_check_invalid_route(self): + """test to check message returned after an invalid route is provided on register + """ + user_register = self.client().post(base_url + '/register/', data=self.user_details) + self.assertEqual(user_register.status_code, 404) + register_data = json.loads(user_register.data.decode()) + self.assertIn(register_data['message'], 'Page not found') + + def test_invalid_method_on_route(self): + """Method to check invalid route provided on register + """ + user_register = self.client().get(base_url + '/register', data=self.user_details) + self.assertEqual(user_register.status_code, 405) + register_data = json.loads(user_register.data.decode()) + self.assertIn(register_data['message'], 'Method not allowed') diff --git a/tests/test_categories.py b/tests/test_categories.py index e025fa0..0f2faac 100644 --- a/tests/test_categories.py +++ b/tests/test_categories.py @@ -65,7 +65,7 @@ def test_null_category_name(self): data={'category_name': ''}) categories_data = json.loads(create_categories.data.decode()) self.assertEqual(create_categories.status_code, 400) - self.assertIn(categories_data['message'], 'category name not provided') + self.assertIn(categories_data['message'], 'Category name is not valid') def test_invalid_category_name(self): """Method to test invalid category name @@ -101,26 +101,6 @@ def test_api_can_get_all_recipe_categories(self): Authorization=self.access_token)) self.assertEqual(get_categories.status_code, 200) - def test_to_check_for_invalid_page_number_on_category_fetch(self): - """Method to test invalid arguements of page on category fetch - """ - get_categories = self.client().post(base_url + 'categories/', headers=dict( - Authorization=self.access_token), data=self.categories) - self.assertEqual(get_categories.status_code, 201) - get_categories = self.client().get(base_url + 'categories/?page=fd', headers=dict( - Authorization=self.access_token)) - self.assertEqual(get_categories.status_code, 400) - - def test_to_check_for_invalid_limit_parameter_on_category_fetch(self): - """Method to test invalid arguements of limit on category fetch - """ - get_categories = self.client().post(base_url + 'categories/', headers=dict( - Authorization=self.access_token), data=self.categories) - self.assertEqual(get_categories.status_code, 201) - get_categories = self.client().get(base_url + 'categories/?limit=fd', headers=dict( - Authorization=self.access_token)) - self.assertEqual(get_categories.status_code, 400) - def test_to_check_for_paginated_recipe_categories(self): """Method to test pagination of category results """ @@ -174,7 +154,7 @@ def test_api_can_edit_a_recipe_category(self): self.assertEqual(create_category.status_code, 201) edit_category = self.client().put(base_url + 'categories/1', headers=dict( - Authorization=self.access_token), data={"category_name": "newly_edited_category"}) + Authorization=self.access_token), data={"category_name": "Newly_Edited"}) self.assertEqual(edit_category.status_code, 200) #test to check whether the edited category exists @@ -201,7 +181,7 @@ def test_edit_category_with_null_name(self): headers=dict(Authorization=self.access_token), data={"category_name": ""}) self.assertEqual(edit_category.status_code, 400) category_data = json.loads(edit_category.data.decode()) - self.assertIn(category_data['message'], 'category name not provided') + self.assertIn(category_data['message'], 'Category name is not valid') def test_edit_category_with_invalid_name(self): """test if API can edit a recipe category with an invalid name @@ -275,26 +255,6 @@ def test_to_check_null_search_response(self): Authorization=self.access_token)) self.assertEqual(get_categories.status_code, 200) - def test_to_check_for_invalid_page_number_on_search(self): - """Method to test invalid arguements on category search - """ - get_categories = self.client().post(base_url + 'categories/', headers=dict( - Authorization=self.access_token), data=self.categories) - self.assertEqual(get_categories.status_code, 201) - get_categories = self.client().get(base_url + 'categories/search/?q=New&page=fd', headers=dict( - Authorization=self.access_token)) - self.assertEqual(get_categories.status_code, 400) - - def test_to_check_for_invalid_limit_parameter_on_search(self): - """Method to test pagination of searched category results - """ - get_categories = self.client().post(base_url + 'categories/', headers=dict( - Authorization=self.access_token), data=self.categories) - self.assertEqual(get_categories.status_code, 201) - get_categories = self.client().get(base_url + 'categories/search/?q=New&limit=fd', headers=dict( - Authorization=self.access_token)) - self.assertEqual(get_categories.status_code, 400) - def test_to_search_with_null_category_item(self): """Test to search with no category item provided """ diff --git a/tests/test_recipes.py b/tests/test_recipes.py index 8c53aac..3356b7c 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -79,12 +79,12 @@ def test_null_recipe_name(self): """ create_recipe = self.client().post(base_url + '/categories/1/recipes/', headers=dict(Authorization=self.access_token), - data={"recipe_name": "", + data={"recipe_name": " ", "recipe_ingredients": "milk", "recipe_methods": "heat to boil"}) self.assertEqual(create_recipe.status_code, 400) recipe_data = json.loads(create_recipe.data.decode()) - self.assertIn(recipe_data['message'], 'Recipe name not provided') + self.assertIn(recipe_data['message'], 'recipe name cannot be empty or with invalid characters') def test_null_recipe_methods(self): """test if recipe methods are null @@ -93,12 +93,12 @@ def test_null_recipe_methods(self): headers=dict(Authorization=self.access_token), data={"recipe_name": "New Recipe", "recipe_ingredients": "milk", - "recipe_methods": ""}) + "recipe_methods": " "}) self.assertEqual(create_recipe.status_code, 400) recipe_data = json.loads(create_recipe.data.decode()) self.assertIn(recipe_data['message'], - 'Recipe preparation methods not provided') + 'Kindly provide ingredients and methods') def test_null_recipe_ingredients(self): """test if recipe ingredients are null @@ -112,19 +112,19 @@ def test_null_recipe_ingredients(self): self.assertEqual(create_recipe.status_code, 400) recipe_data = json.loads(create_recipe.data.decode()) self.assertIn(recipe_data['message'], - 'Recipe ingredients not provided') + 'Kindly provide ingredients and methods') def test_invalid_recipe_name(self): """test if recipe name is valid """ create_recipe = self.client().post(base_url + '/categories/1/recipes/', headers=dict(Authorization=self.access_token), - data={"recipe_name": "@@@", + data={"recipe_name": "", "recipe_ingredients": "milk, water", "recipe_methods": "heat to boil"}) - self.assertEqual(create_recipe.status_code, 400) recipe_data = json.loads(create_recipe.data.decode()) - self.assertIn(recipe_data['message'], 'Recipe name is not valid') + self.assertIn(recipe_data['message'], 'recipe name cannot be empty or with invalid characters') + self.assertEqual(create_recipe.status_code, 400) def test_duplicate_recipe(self): """test if recipe is duplicated @@ -201,7 +201,7 @@ def test_to_edit_recipe_with_null_name(self): 'recipe_methods': 'boil to heat'}) category_data = json.loads(edit_recipe.data.decode()) self.assertEqual(edit_recipe.status_code, 400) - self.assertIn(category_data['message'], 'Recipe name not provided') + self.assertIn(category_data['message'], 'recipe name cannot be empty or with invalid characters') def test_to_edit_recipe_with_null_recipe_methods(self): """Test to edit a recipe name with null recipe methods @@ -219,7 +219,7 @@ def test_to_edit_recipe_with_null_recipe_methods(self): category_data = json.loads(edit_recipe.data.decode()) self.assertEqual(edit_recipe.status_code, 400) self.assertIn(category_data['message'], - 'Recipe preparation methods not provided') + 'Kindly provide ingredients and methods') def test_to_edit_recipe_with_null_recipe_ingredients(self): """Test to edit a recipe name with null recipe ingredients @@ -237,7 +237,7 @@ def test_to_edit_recipe_with_null_recipe_ingredients(self): category_data = json.loads(edit_recipe.data.decode()) self.assertEqual(edit_recipe.status_code, 400) self.assertIn(category_data['message'], - 'Recipe ingredients not provided') + 'Kindly provide ingredients and methods') def test_edit_invalid_recipe_name(self): """Test to edit a recipe name with invalid recipe name @@ -254,7 +254,7 @@ def test_edit_invalid_recipe_name(self): 'recipe_methods': 'heat to boil'}) category_data = json.loads(edit_recipe.data.decode()) self.assertEqual(edit_recipe.status_code, 400) - self.assertIn(category_data['message'], 'Recipe name is not valid') + self.assertIn(category_data['message'], 'recipe name cannot be empty or with invalid characters') def test_edit_recipe_with_no_recipe_id(self): """Test to edit a recipe name with invalid recipe name