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 bbb1eba..fc9bf40 100644 --- a/app/auth/authentication.py +++ b/app/auth/authentication.py @@ -123,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 diff --git a/app/classes/categories.py b/app/classes/categories.py index ad427b3..378e603 100644 --- a/app/classes/categories.py +++ b/app/classes/categories.py @@ -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: @@ -287,7 +300,8 @@ def put(self, current_user, id): category_name=category_name, created_by=current_user.id).first() if category_details: - response = {'message': 'Category name exists'} + response = {'message': 'Category name exists', + 'status': 'fail'} return make_response(jsonify(response)), 400 category.category_name = category_name @@ -337,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): @@ -433,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 9d9e4ae..dddaaf9 100644 --- a/app/classes/recipes.py +++ b/app/classes/recipes.py @@ -71,13 +71,14 @@ def post(self, current_user, id): 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, @@ -92,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): @@ -151,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) @@ -198,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: @@ -255,14 +259,15 @@ def put(self, current_user, id, recipe_id): try: category_id = id recipe = Recipes.query.filter_by( - category_id=category_id, id=recipe_id).first() + 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'} + response = {'message': 'No recipe found', + 'status': 'error'} response = make_response(jsonify(response)), 404 return response else: @@ -314,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 @@ -396,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/auth_validators.py b/app/helpers/auth_validators.py index 61fec73..33fb7ec 100644 --- a/app/helpers/auth_validators.py +++ b/app/helpers/auth_validators.py @@ -1,6 +1,6 @@ """Class to validate user input """ -from marshmallow import Schema, fields, ValidationError +from marshmallow import ValidationError from validate_email import validate_email import re @@ -14,17 +14,17 @@ def user_registration_validation(email, username, password, secret): valid_email = validate_email(email) if username: - username = re.sub(r'\s+', ' ', username).strip() + 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('Kindly provide all details') + 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('{} is not a valid username'.format(username)) + error = ValidationError('{} Kindly provide a username ') if not valid_email: error = ValidationError('{} is not a valid email'.format(email)) @@ -49,7 +49,7 @@ def password_reset_validation(email, reset_password, secret): """ error = None if reset_password: - reset_password = re.sub(r'\s+', ' ', reset_password).strip() + reset_password = re.sub(r'\s+', '', reset_password).strip() reset_password = None if reset_password == " " else reset_password if not reset_password: diff --git a/app/helpers/category_validators.py b/app/helpers/category_validators.py index a348858..653d292 100644 --- a/app/helpers/category_validators.py +++ b/app/helpers/category_validators.py @@ -22,4 +22,3 @@ def category_validation(category_name): if error: raise error - diff --git a/app/helpers/recipe_validators.py b/app/helpers/recipe_validators.py index 912510b..36fe571 100644 --- a/app/helpers/recipe_validators.py +++ b/app/helpers/recipe_validators.py @@ -1,34 +1,26 @@ -from marshmallow import ValidationError +from marshmallow import ValidationError import re -def recipe_validation(recipe_name, recipe_methods, recipe_ingredients): +def recipe_validation(recipe, *argv): error = None regex_pattern = "[a-zA-Z0-9-]+$" - if recipe_name: - recipe_name = re.sub(r'\s+', ' ', recipe_name).strip() - recipe_name = None if recipe_name == " " else recipe_name.title() + if recipe: + recipe = re.sub(r'\s+', '', recipe).strip() + recipe = None if recipe == " " else recipe.title() - if recipe_ingredients: - recipe_ingredients = re.sub(r'\s+', ' ', recipe_ingredients).strip() - recipe_ingredients = None if recipe_ingredients == " " else recipe_ingredients + if not re.search(regex_pattern, recipe): + error = ValidationError('recipe name cannot be empty or with invalid characters') - if recipe_methods: - recipe_methods = re.sub(r'\s+', ' ', recipe_methods).strip() - recipe_methods = None if recipe_methods == " " else recipe_methods + for arg in argv: + if arg: + arg = re.sub(r'\s+', '', arg).strip() + arg = None if arg == " " else arg - if not recipe_name: - error = ValidationError('Recipe name not provided') + if not arg: + error = ValidationError('Kindly provide ingredients and methods') - if not recipe_ingredients: - error = ValidationError('Recipe ingredients not provided') + if error: + raise error - if not recipe_methods: - error = ValidationError('Recipe preparation methods not provided') - - if not re.search(regex_pattern, recipe_name): - error = ValidationError('Recipe name is not valid') - - if error: - raise error diff --git a/app/models.py b/app/models.py index fa1e316..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): """ diff --git a/tests/test_auth.py b/tests/test_auth.py index a03e1cc..39cfcb3 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -233,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_recipes.py b/tests/test_recipes.py index d95aa7f..3356b7c 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -84,7 +84,7 @@ def test_null_recipe_name(self): "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') def test_null_recipe_methods(self): """test if recipe methods are null @@ -98,7 +98,7 @@ def test_null_recipe_methods(self): 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,7 +112,7 @@ 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 @@ -123,7 +123,7 @@ def test_invalid_recipe_name(self): "recipe_ingredients": "milk, water", "recipe_methods": "heat to boil"}) 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): @@ -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 is not valid') + 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