diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..cbae45f --- /dev/null +++ b/.coveragerc @@ -0,0 +1,5 @@ +[report] +omit = + */migrations/* + *virtualenvs/* + */site-packages/* diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000..3ce2387 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1 @@ +repo_token: nZcYGqlIeVZLAh31Hl9JwvqPB8btPoHp3 diff --git a/.env b/.env deleted file mode 100644 index d41347b..0000000 --- a/.env +++ /dev/null @@ -1,5 +0,0 @@ -workon myvenv -export FLASK_APP="run.py" -export SECRET="doris-waithira-njihia" -export APP_SETTINGS="development" -export DATABASE_URL="postgresql://localhost/flask_api" diff --git a/.gitignore b/.gitignore index 0205d62..f24b01e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,8 @@ *.pyc .DS_Store +.env +.cache/* +.coverage +instance/testdb.db +tests/.coverage +tests/cover/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..15e08b6 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +language: python +python: + - "3.5" + - "3.6" +# command to install dependencies +install: + - pip install -r requirements.txt + - pip install coveralls +# command to run tests +script: + # coverage run -m unittest + nosetests --with-coverage --cover-package=bucketlist +env: + - DB=postgres + +after_success: + coveralls diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..1afef61 --- /dev/null +++ b/Procfile @@ -0,0 +1,3 @@ +web: gunicorn run:app +init: python manage.py db init +upgrade: python manage.py db upgrade diff --git a/README.md b/README.md index 6f81816..33a9d3e 100644 --- a/README.md +++ b/README.md @@ -1 +1,162 @@ -# flask-bucketlist-api +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/93600bb65a2d4e51b0659003115ea6d6)](https://www.codacy.com/app/WNjihia/flask-bucketlist-api?utm_source=github.com&utm_medium=referral&utm_content=WNjihia/flask-bucketlist-api&utm_campaign=badger) +[![Build Status](https://travis-ci.org/WNjihia/flask-bucketlist-api.svg?branch=develop)](https://travis-ci.org/WNjihia/flask-bucketlist-api) +[![Coverage Status](https://coveralls.io/repos/github/WNjihia/flask-bucketlist-api/badge.svg?branch=develop)](https://coveralls.io/github/WNjihia/flask-bucketlist-api?branch=develop) + +API documentation: http://docs.bucketlistwnjihia.apiary.io/ + +### BucketList API + +According to the Oxford Dictionary, a BucketList is a number of experiences or achievements that a person hopes to have or accomplish during their lifetime. + +This is a Flask API for an online BucketList service. + +### Installation and Setup + +Clone the repository from GitHub: +``` +$ git clone https://github.com/WNjihia/flask-bucketlist-api.git +``` + +Fetch from the develop branch: +``` +$ git fetch origin develop +``` + +Navigate to the `flask-bucketlist-api` directory: +``` +$ cd flask-bucketlist-api +``` + +Create a virtual environment: +> Use [this guide](http://docs.python-guide.org/en/latest/dev/virtualenvs/) to create and activate a virtual environment. + +Install the required packages: +``` +$ pip install -r requirements.txt + +``` + +Install postgres: +``` +brew install postgresql +type psql in terminal. +On postgres interactive interface, type CREATE DATABASE flask_api; +``` + +Create a .env file and add the following: +``` +source name-of-virtual-environment/bin/activate +export FLASK_APP="run.py" +export SECRET="a-secret-key" +export DATABASE_URL="postgresql://localhost/flask_api" +``` + +Then run: +``` +source .env +``` + +Run the migrations: +``` +python manage.py db init +python manage.py db migrate +python manage.py db upgrade +``` + +Launch the program: +``` +python run.py +``` + +### API Endpoints + +| Methods | Resource URL | Description | Public Access | +| ---- | ------- | --------------- | ------ | +|POST| `/api/v1/auth/login` | Logs a user in| TRUE | +|POST| `/api/v1/auth/register` | Register a user | TRUE | +|POST| `/api/v1/bucketlists/` | Create a new bucket list | FALSE | +|GET| `/api/v1/bucketlists/` | List all the created bucket lists | FALSE | +|GET| `/api/v1/bucketlists//` | Get single bucket list | FALSE | +|PUT| `/api/v1/bucketlists//` | Update this bucket list | FALSE | +|DELETE| `/api/v1/bucketlists//` | Delete this single bucket list | FALSE | +|POST| `/api/v1/bucketlists//items/` | Create a new item in bucket list | FALSE | +|GET| `/api/v1/bucketlists//items/` | List items in this bucket list | FALSE | +|GET| `/api/v1/bucketlists//items//` | Get single bucket list item | FALSE | +|PUT|`/api/v1/bucketlists//items//` | Update a bucket list item | FALSE | +|DELETE|`/api/v1/bucketlists//items//` | Delete an item in a bucket list | FALSE | +|GET| `/api/v1/bucketlists?limit=2&` | Pagination to get 2 bucket list records per page | FALSE | +|GET| `/api/v1/bucketlists?q=bucket` | Search for bucket lists with name like ```bucket``` | FALSE | +|GET| `/api/v1/bucketlists//items?limit=2&` | Pagination to get 2 bucketlist item records per page | FALSE | +|GET| `/api/v1/bucketlists//items?q=climb` | Search for bucketlist items with name like ```climb``` | FALSE | + +### How to use the API + +**Register a user** + +![Alt text](https://image.ibb.co/gsQT4a/Screen_Shot_2017_07_10_at_14_34_15.png) + +**To Login a user** + +![Alt text](https://image.ibb.co/csyTLF/Screen_Shot_2017_07_10_at_12_31_28.png) + +**Create a new BucketList** + +![Alt text](https://image.ibb.co/dbq9cv/Screen_Shot_2017_07_10_at_14_39_18.png) + +**Get all BucketLists** + +![Alt text](https://image.ibb.co/gEhAZa/Screen_Shot_2017_07_10_at_12_35_12.png) + +**Get a single BucketList** + +![Alt text](https://image.ibb.co/cwy1qF/Screen_Shot_2017_07_10_at_14_43_33.png) + +**Update a BucketList** + +![Alt text](https://image.ibb.co/g0jDnv/Screen_Shot_2017_07_10_at_12_34_35.png) + +**Delete a BucketList** + +![Alt text](https://image.ibb.co/jmhYnv/Screen_Shot_2017_07_10_at_12_34_59.png) + +**Create a new BucketList item** + +![Alt text](https://image.ibb.co/jjFa0F/Screen_Shot_2017_07_10_at_12_36_39.png) + +**Get all BucketList items** + +![Alt text](https://image.ibb.co/j0iRqF/Screen_Shot_2017_07_10_at_12_36_53.png) + +**Get a single BucketList item** + +![Alt text](https://image.ibb.co/mhC5Hv/Screen_Shot_2017_07_10_at_14_42_10.png) + +**Update a BucketList item** + +![Alt text](https://image.ibb.co/ifFvja/Screen_Shot_2017_07_10_at_12_37_50.png) + +**Delete a BucketList item** + +![Alt text](https://image.ibb.co/e6pAHv/Screen_Shot_2017_07_10_at_12_38_13.png) + +**Search for:** + **-a BucketList** + +![Alt text](https://image.ibb.co/fnYvHv/Screen_Shot_2017_07_10_at_14_36_31.png) + + **-an Item** + +![Alt text](https://image.ibb.co/imZfja/Screen_Shot_2017_07_10_at_14_35_56.png) + +**Pagination** + +![Alt text](https://image.ibb.co/kPcYnv/Screen_Shot_2017_07_10_at_12_33_08.png) + +![Alt text](https://image.ibb.co/f3dHEa/Screen_Shot_2017_07_10_at_12_33_24.png) + +### Testing + +To test, run the following command: +``` +nosetests +``` diff --git a/apiary.apib b/apiary.apib new file mode 100644 index 0000000..a3d55cd --- /dev/null +++ b/apiary.apib @@ -0,0 +1,488 @@ +FORMAT: 1A +HOST: https://flask-bucketlist-api.herokuapp.com/ + +# BucketList-API +This is a Flask API for an online BucketList service. + +This API requires token-based authentication to access resources. +It also has pagination and search capabilities on the resources requested. + +The features attached to the service include: +* user registration +* authenticating a user +* creating a new bucket list +* updating and deleting bucket lists +* retrieving a list of all created bucket lists by a registered user. +* creating a new bucket list item +* updating and deleting bucket list items +* retrieving a list of all created bucket list items by a registered user. + +## Group User Authentication + +## Login [/api/v1/auth/login] + +### Authenticate a user and generate a token [POST] + +Use your correct email and password. +Note the token returned on successful login. It is required to access other +resources. + ++ Request (application/json) + + { + "email": "kevin@yahoo.com", + "password": "password" + } + ++ Response 200 (application/json) + + { + "token": "ARandomtok3n" + } + +## Group Resources + +## Registration [/api/v1/auth/register] + +### Register a new user [POST] + +You may use any name or password combination for the registration. + ++ Request (application/json) + + { + "username": "kevin", + "email": "kevin@yahoo.com", + "password": "password" + } + ++ Response 201 (application/json) + + { + "message": "You have been successfully registered." + } + + +## BucketList Resources [/api/v1/bucketlists/] + +### Create a new bucketlist [POST] + +Create a new bucketlist by making a `POST` request to the `/api/v1/bucketlists/` path + ++ Request (application/json) + + + Headers + + Authorization: "Token " + + + Body + + { + "title": "Learn how to swim" + } + ++ Response 201 (application/json) + + { + "message": "Bucketlist Learn how to swim has been added" + } + ++ Response 400 (application/json) + + { + "message": "Invalid bucketlist title!" + } + ++ Response 409 (application/json) + + { + "message": "Bucketlist already exists!" + } + +### Retrieve all bucketlists [GET] + +Retrieve all BucketLists that are owned by the current user + +Optional request parameters: + +- `page` default is 1 +- `limit` default is 20 + ++ Parameters + + + page (optional, int) - the page to receive + + limit (optional, int) - the number of bucketlist results per page + ++ Request (application/json) + + + Headers + + Authorization: "Token " + ++ Response 200 (application/json) + + { + "id": , + "title": "Learn how to swim", + "date_created": "Wed, 05 Jul 2017 10:02:03 GMT" + } + ++ Response 401 (application/json) + + { + "message": "Please provide a valid auth token!" + } + ++ Response 404 (application/json) + + { + "message": "Bucketlist cannot be found" + } + +## Single BucketList Resource [/api/v1/bucketlists//] + +### Get a single bucketlist [GET] + +Retrieve a single bucketlist + ++ Request (application/json) + + + Headers + + Authorization: "Token " + ++ Response 200 (application/json) + + { + "id": , + "title": "Learn how to swim", + "date_created": "Wed, 05 Jul 2017 10:02:03 GMT" + } + ++ Response 401 (application/json) + + { + "message": "Please provide a valid auth token!" + } + ++ Response 404 (application/json) + + { + "message": "Bucketlist cannot be found" + } + +### Update a bucketlist [PUT] + +Update a single bucketlist + ++ Request (application/json) + + + Headers + + Authorization: "Token " + + + Body + + { + "title": "Learn how to play golf" + } + ++ Response 200 (application/json) + + { + "id": , + "title": "Learn how to play golf", + "date_created": "Wed, 05 Jul 2017 10:02:03 GMT" + } + ++ Response 404 (application/json) + + { + "message": "Bucketlist cannot be found" + } + ++ Response 409 (application/json) + + { + "message": "No updates detected" + } + ++ Response 401 (application/json) + + { + "message": "Please provide a valid auth token!" + } + +### Delete a bucketlist [DELETE] + +Delete a single bucketlist + ++ Request (application/json) + + + Headers + + Authorization: "Token " + ++ Response 200 (application/json) + + { + "message": "Bucketlist successfully deleted!" + } + ++ Response 401 (application/json) + + { + "message": "Please provide a valid auth token!" + } + ++ Response 404 (application/json) + + { + "message": "Bucketlist cannot be found" + } + +## BucketList Items Resources [/api/v1/bucketlists//items/] + +### Create a new bucketlist item [POST] + +Create a new item in a bucketlist + ++ Request (application/json) + + + Headers + + Authorization: "Token " + + + Body + + { + "name": "Item on golf" + "description": "Golf - Great sport" + } + ++ Response 201 (application/json) + + { + "message": "Item Item on golf has been added" + } + ++ Response 400 (application/json) + + { + "message": "Invalid name format" + } + ++ Response 409 (application/json) + + { + "message": "Item already exists!" + } + ++ Response 404 (application/json) + + { + "message": "Bucketlist cannot be found" + } + ++ Response 401 (application/json) + + { + "message": "Please provide a valid auth token!" + } + +### List all bucketlist item [GET] + +Retrieve all items in a bucketlist + +Optional request parameters: + +- `page` default is 1 +- `limit` default is 20 + ++ Parameters + + + page (optional, int) - the page to receive + + limit (optional, int) - the number of bucketlist item results per page + ++ Request (application/json) + + + Headers + + Authorization: "Token " + ++ Response 200 (application/json) + + { + "id": , + "name": "Item on golf", + 'description': "Golf - Great sport", + 'status': "Not done", + 'date_created': "Wed, 05 Jul 2017 10:02:03 GMT", + 'bucketlist': "Learn how to play golf" + } + ++ Response 404 (application/json) + + { + "message": "Bucketlist cannot be found" + } + ++ Response 401 (application/json) + + { + "message": "Please provide a valid auth token!" + } + +## Single BucketList Items Resources [/api/v1/bucketlists//items//] + +### Retrieve a single bucketlist item [GET] + +Retrieve a single item in a bucketlist + ++ Request (application/json) + + + Headers + + Authorization: "Token " + ++ Response 200 (application/json) + + { + "id": , + "name": "Item on golf", + 'description': "Golf - Great sport", + 'status': "Not done", + 'date_created': "Wed, 05 Jul 2017 10:02:03 GMT", + 'bucketlist': "Learn how to play golf" + } + ++ Response 404 (application/json) + + { + "message": "Bucketlist cannot be found" + } + ++ Response 401 (application/json) + + { + "message": "Please provide a valid auth token!" + } + +### Update a bucketlist item [PUT] + +Update a single bucketlist item + ++ Request (application/json) + + + Headers + + Authorization: "Token " + + + Body + + { + "name": "Item1", + 'description': "Golf - Great sport" + } + ++ Response 201 (application/json) + + { + "id": , + "name": "Item1", + 'description': "Golf - Great sport", + 'is_completed': "False", + 'date_created': "Wed, 05 Jul 2017 10:02:03 GMT", + 'date_modified': "Wed, 05 Jul 2017 10:02:03 GMT" + } + ++ Response 409 (application/json) + + { + "message": "No updates detected" + } + ++ Response 404 (application/json) + + { + "message": "Item be found" + } + ++ Response 401 (application/json) + + { + "message": "Please provide a valid auth token!" + } + +### Update a bucketlist item completion status [PATCH] + +Update bucketlist item is_completed attribute + ++ Request (application/json) + + + Headers + + Authorization: "Token " + + + Body + + { + "is_completed": "true" + } + ++ Response 200 (application/json) + + { + "id": , + "name": "Item1", + 'description': "Golf - Great sport", + 'is_completed': "True", + 'date_created': "Wed, 05 Jul 2017 10:02:03 GMT", + 'date_modified': "Wed, 05 Jul 2017 10:02:03 GMT" + } + ++ Response 409 (application/json) + + { + "message": "No updates detected" + } + ++ Response 404 (application/json) + + { + "message": "Item be found" + } + ++ Response 401 (application/json) + + { + "message": "Please provide a valid auth token!" + } + +### Delete a single bucketlist item [DELETE] + +Delete a single bucketlist item + ++ Request (application/json) + + + Headers + + Authorization: "Token " + ++ Response 200 (application/json) + + { + "message": "Item succesfully deleted" + } + ++ Response 401 (application/json) + + { + "message": "Please provide a valid auth token!" + } + ++ Response 404 (application/json) + + { + "message": "Item not found" + } diff --git a/bucketlist/__init__.py b/bucketlist/__init__.py index c36021b..696349c 100644 --- a/bucketlist/__init__.py +++ b/bucketlist/__init__.py @@ -1,10 +1,10 @@ from instance.config import app_config # Load the views -from flask import Flask +from flask import Flask, render_template from flask_api import FlaskAPI from flask_sqlalchemy import SQLAlchemy # Initialize the app -app = Flask(__name__, instance_relative_config=True) +app = Flask(__name__) # initialize sql-alchemy db = SQLAlchemy() @@ -18,8 +18,17 @@ def create_app(config_name): app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db.init_app(app) - from auth.views import auth_blueprint + from bucketlist.auth.views import auth_blueprint + from bucketlist.api.views import api_blueprint + from bucketlist.errors import error_blueprint + # register Blueprint app.register_blueprint(auth_blueprint) + app.register_blueprint(api_blueprint) + app.register_blueprint(error_blueprint) + + @app.route('/') + def home(): + return render_template("index.html") return app diff --git a/bucketlist/api/__init__.py b/bucketlist/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bucketlist/api/views.py b/bucketlist/api/views.py new file mode 100644 index 0000000..9ff6d73 --- /dev/null +++ b/bucketlist/api/views.py @@ -0,0 +1,671 @@ +import re + +from flask import Blueprint, make_response, jsonify, request +from flask.views import MethodView + +from bucketlist.models import Bucketlist, User, Item + +api_blueprint = Blueprint('api', __name__) + + +def response_for_updates_with_same_data(): + response = { + 'status': 'fail', + 'message': 'No updates detected' + } + return make_response(jsonify(response)), 409 + + +def validate_token(self): + # get the auth token + auth_header = request.headers.get('Authorization') + if auth_header: + auth_token = auth_header.split(" ")[1] + else: + auth_token = '' + if auth_token: + user_id = User.decode_auth_token(auth_token) + return user_id + + +def validate_input_format(name): + check_name = re.match('^[ a-zA-Z0-9_.-]+$', name) + if check_name is None: + return True + elif len(name.strip(" ")) == 0: + return True + + +class Bucketlist_View(MethodView): + """ + Contains methods for BucketList Resource + """ + def post(self): + """ + Method: `POST` + Create a bucketlist. + `URL` path: `/api/v1/bucketlists/` + """ + + # validate token + user_id_response = validate_token(request) + if user_id_response is None: + response = { + 'status': 'fail', + 'message': 'Please provide a valid auth token!' + } + return make_response(jsonify(response)), 401 + + post_data = request.get_json() + + # check if bucketlist exists + if Bucketlist.query.filter_by(bucketlist_title= + post_data.get('title'), + creator_id=user_id_response).first(): + response = { + 'status': 'fail', + 'message': 'Bucketlist already exists!' + } + return make_response(jsonify(response)), 409 + + # check if title format is valid + if validate_input_format(post_data.get('title')): + response = { + 'status': 'fail', + 'message': 'Invalid bucketlist title!' + } + return make_response(jsonify(response)), 400 + + try: + new_bucketlist = Bucketlist( + bucketlist_title=post_data.get('title'), + creator_id=user_id_response + ) + # insert the bucketlist + new_bucketlist.save() + response = { + 'status': 'success', + 'message': 'Bucketlist {} has been added' + .format(post_data.get('title')) + } + return make_response(jsonify(response)), 201 + + except Exception as e: + response = { + 'status': 'fail' + str(e), + 'message': 'Some error occurred. Please try again' + } + return make_response(jsonify(response)), 500 + + def get(self, id=None): + """ + Method: `GET` + Retrieve all bucketlists or a single bucketlist + `URL` path: `/api/v1/bucketlists/` or + `/api/v1/bucketlists//` + """ + + # validate token + user_id_response = validate_token(request) + if user_id_response is None: + response = { + 'status': 'fail', + 'message': 'Please provide a valid auth token!' + } + return make_response(jsonify(response)), 401 + + if id: + # retrieve a bucketlist + bucketlist = Bucketlist.query.filter_by(id=id, creator_id= + user_id_response).first() + if not bucketlist: + response = { + 'status': 'fail', + 'message': 'Bucketlist cannot be found' + } + return make_response(jsonify(response)), 404 + + if not bucketlist.items: + items = {} + else: + item_data = [] + # make items JSON serializable + for item in bucketlist.items: + items = { + "item_id": item.id, + "item_name": item.item_name, + "item_description": item.description + } + item_data.append(items) + response = { + 'id': bucketlist.id, + 'title': bucketlist.bucketlist_title, + 'date_created': bucketlist.date_created, + 'items': items + } + return make_response(jsonify(response)), 200 + + page = request.args.get("page", default=1, type=int) + limit = request.args.get("limit", default=20, type=int) + search = request.args.get("q", type=str) + response = [] + items = [] + + if search: + # implement search in query + bucketlists = Bucketlist.query \ + .filter_by(creator_id=user_id_response) \ + .filter(Bucketlist.bucketlist_title + .ilike('%' + search + '%')).paginate( + page, limit, False) + else: + bucketlists = Bucketlist.query.filter_by(creator_id= + user_id_response) \ + .paginate(page, limit, False) + + page_count = bucketlists.pages + + # add next and previous url links + if bucketlists.has_next: + next_page = request.url_root + 'api/v1/bucketlists' + \ + '?limit=' + str(limit) + \ + '&page=' + str(page + 1) + else: + next_page = 'None' + if bucketlists.has_prev: + prev_page = request.url_root + 'api/v1/bucketlists' + \ + '?limit=' + str(limit) + \ + '&page=' + str(page - 1) + else: + prev_page = 'None' + + for bucketlist_entry in bucketlists.items: + item_data = [] + if bucketlist_entry.items: + # make items JSON serializable + for item in bucketlist_entry.items: + items = { + "item_id": item.id, + "item_name": item.item_name, + "item_description": item.description + } + item_data.append(items) + + info = { + 'id': bucketlist_entry.id, + 'title': bucketlist_entry.bucketlist_title, + 'date_created': bucketlist_entry.date_created, + 'items': item_data + } + response.append(info) + + meta_data = {'meta_data': {'next_page': next_page, + 'previous_page': prev_page, + 'total_pages': page_count + }} + response.append(meta_data) + return make_response(jsonify(response)), 200 + + def put(self, id): + """ + Method: `PUT` + Update a bucketlist + `URL` path: `/api/v1/bucketlists//` + """ + # validate token + user_id_response = validate_token(request) + if user_id_response is None: + response = { + 'status': 'fail', + 'message': 'Please provide a valid auth token!' + } + return make_response(jsonify(response)), 401 + + # get the bucketlist + bucketlist = Bucketlist.query.filter_by(id=id, + creator_id= + user_id_response).first() + if not bucketlist: + response = { + 'status': 'fail', + 'message': 'Bucketlist does not exist!' + } + return make_response(jsonify(response)), 404 + + post_data = request.get_json() + + # check if title format is valid + if validate_input_format(post_data.get('title')): + response = { + 'status': 'fail', + 'message': 'Invalid bucketlist title!' + } + return make_response(jsonify(response)), 400 + + # check for updates + if post_data.get('title') == bucketlist.bucketlist_title: + return response_for_updates_with_same_data() + + bucketlist.bucketlist_title = post_data.get('title') + bucketlist.save() + + info = { + 'id': bucketlist.id, + 'title': bucketlist.bucketlist_title, + 'date_created': bucketlist.date_created, + 'date_modified': bucketlist.date_modified + } + response = { + 'status': 'success', + 'message': info + } + return make_response(jsonify(response)), 200 + + def delete(self, id): + """ + Delete a bucketlist + `URL` path: `/api/v1/bucketlists//` + """ + # validate token + user_id_response = validate_token(request) + if user_id_response is None: + response = { + 'status': 'fail', + 'message': 'Please provide a valid auth token!' + } + return make_response(jsonify(response)), 401 + + # retrieve bucketlist + bucketlist = Bucketlist.query.filter_by(id=id, + creator_id= + user_id_response).first() + # check if bucketlist exists + if not bucketlist: + response = { + 'status': 'success', + 'message': 'Bucketlist cannot be found' + } + return make_response(jsonify(response)), 404 + + bucketlist.delete() + + response = { + 'status': 'success', + 'message': 'Bucketlist successfully deleted!' + } + return make_response(jsonify(response)), 200 + + +class Items_View(MethodView): + def post(self, bucketlist_id): + """ + Method: `POST` + Create a bucketlist item. + `URL` path: `/api/v1/bucketlists//items/` + """ + # validate token + user_id_response = validate_token(request) + if user_id_response is None: + response = { + 'status': 'fail', + 'message': 'Please provide a valid auth token!' + } + return make_response(jsonify(response)), 401 + + post_data = request.get_json() + + # check if bucketlist exists + bucketlist = Bucketlist.query.filter_by(id=bucketlist_id, + creator_id= + user_id_response).first() + if not bucketlist: + response = { + 'status': 'fail', + 'message': 'Bucketlist not found!' + } + return make_response(jsonify(response)), 404 + + # check if item already exists + duplicate_item = Item.query.filter_by(item_name= + post_data.get('name'), + bucketlist_id= + bucketlist_id).first() + if duplicate_item: + response = { + 'status': 'fail', + 'message': 'Item already exists!' + } + return make_response(jsonify(response)), 409 + + # check if title format is valid + if validate_input_format(post_data.get('name')): + response = { + 'status': 'fail', + 'message': 'Invalid name format' + } + return make_response(jsonify(response)), 400 + + # insert item + new_item = Item( + item_name=post_data.get('name'), + description=post_data.get('description'), + bucketlist_id=bucketlist_id + ) + new_item.save() + response = { + 'status': 'success', + 'message': 'Item {} has been added' + .format(post_data.get('name')) + } + return make_response(jsonify(response)), 201 + + def get(self, bucketlist_id, item_id=None): + """ + Method: `GET` + Retrieve all bucketlist items or a single bucketlist item. + `URL` path: `/api/v1/bucketlists//items/` or + `/api/v1/bucketlists//items/` + """ + # validate token + user_id_response = validate_token(request) + if user_id_response is None: + response = { + 'status': 'fail', + 'message': 'Please provide a valid auth token!' + } + return make_response(jsonify(response)), 401 + + # check if bucketlist exists + bucketlist = Bucketlist.query.filter_by(id=bucketlist_id, + creator_id= + user_id_response).first() + # check if bucketlist exists + if not bucketlist: + response = { + 'status': 'fail', + 'message': 'Bucketlist not found!' + } + return make_response(jsonify(response)), 404 + + if item_id: + item = Item.query.filter_by(bucketlist_id=bucketlist_id, + id=item_id).first() + # check if item exists + if not item: + response = { + 'status': 'fail', + 'message': 'Item not found!' + } + return make_response(jsonify(response)), 404 + # if not item.is_completed: + # status = "Not done" + # else: + # status = "Done" + response = { + 'id': item.id, + 'name': item.item_name, + 'description': item.description, + 'is_completed': item.is_completed, + 'date_created': item.created_date, + 'bucketlist': bucketlist.bucketlist_title + } + return make_response(jsonify(response)), 200 + + page = request.args.get("page", default=1, type=int) + limit = request.args.get("limit", default=20, type=int) + search = request.args.get("q", type=str) + response = [] + if search: + # implement search in query + items = Item.query.filter_by(bucketlist_id=bucketlist_id) \ + .filter(Item.item_name + .ilike('%' + search + '%')) \ + .paginate(page, limit, False) + else: + items = Item.query.filter_by(bucketlist_id=bucketlist_id) \ + .paginate(page, limit, False) + + if not items: + response = { + 'status': 'success', + 'message': 'This bucketlist has no items' + } + return make_response(jsonify(response)), 200 + + page_count = items.pages + + # add next and previous url links + if items.has_next: + next_page = request.url_root + '/api/v1/bucketlists' + \ + bucketlist_id + '/items' + '?limit=' + str(limit) + \ + '&page=' + str(page + 1) + else: + next_page = 'None' + if items.has_prev: + prev_page = request.url_root + '/api/v1/bucketlists/' + \ + bucketlist_id + '/items' + '?limit=' + str(limit) + \ + '&page=' + str(page - 1) + else: + prev_page = 'None' + + for item in items.items: + # if not item.is_completed: + # status = "Not done" + # else: + # status = "Done" + info = { + 'id': item.id, + 'name': item.item_name, + 'description': item.description, + 'is_completed': item.is_completed, + 'date_created': item.created_date, + 'bucketlist': bucketlist.bucketlist_title + } + response.append(info) + meta_data = {'meta_data': {'next_page': next_page, + 'previous_page': prev_page, + 'total_pages': page_count + }} + response.append(meta_data) + return make_response(jsonify(response)), 200 + + def patch(self, bucketlist_id, item_id): + """ + Method: `PATCH` + Update a bucketlist item completion status. + `URL` path: `/api/v1/bucketlists//items//` + """ + # validate token + user_id_response = validate_token(request) + if user_id_response is None: + response = { + 'status': 'fail', + 'message': 'Please provide a valid auth token!' + } + return make_response(jsonify(response)), 401 + + # check if bucketlist exists + bucketlist = Bucketlist.query.filter_by(id=bucketlist_id, + creator_id= + user_id_response).first() + if not bucketlist: + response = { + 'status': 'fail', + 'message': 'Bucketlist not found!' + } + return make_response(jsonify(response)), 404 + + # check if item exists + item = Item.query.filter_by(id=item_id, bucketlist_id= + bucketlist_id).first() + if not item: + response = { + 'status': 'fail', + 'message': 'Item not found!' + } + return make_response(jsonify(response)), 404 + + post_data = request.get_json() + + if item.is_completed is True: + if post_data.get('is_completed') == "true": + return response_for_updates_with_same_data() + if item.is_completed is False: + if post_data.get('is_completed') == "false": + return response_for_updates_with_same_data() + + item.is_completed = post_data.get('is_completed') + # if (post_data.get('is_completed')): + # if (post_data.get('is_completed') != item.is_completed): + # item.is_completed = post_data.get('is_completed') + # else: + # return response_for_updates_with_same_data() + + item.save() + + info = { + 'id': item.id, + 'name': item.item_name, + 'description': item.description, + 'completion_status': item.is_completed, + 'date_created': item.created_date, + 'date_modified': item.modified_date + } + response = { + 'status': 'success', + 'message': info + } + return make_response(jsonify(response)), 200 + + def put(self, bucketlist_id, item_id): + """ + Method: `PUT` + Update a bucketlist item. + `URL` path: `/api/v1/bucketlists//items//` + """ + # validate token + user_id_response = validate_token(request) + if user_id_response is None: + response = { + 'status': 'fail', + 'message': 'Please provide a valid auth token!' + } + return make_response(jsonify(response)), 401 + + # check if bucketlist exists + bucketlist = Bucketlist.query.filter_by(id=bucketlist_id, + creator_id= + user_id_response).first() + if not bucketlist: + response = { + 'status': 'fail', + 'message': 'Bucketlist not found!' + } + return make_response(jsonify(response)), 404 + + # check if item exists + item = Item.query.filter_by(id=item_id, bucketlist_id= + bucketlist_id).first() + if not item: + response = { + 'status': 'fail', + 'message': 'Item not found!' + } + return make_response(jsonify(response)), 404 + + post_data = request.get_json() + if item.item_name == post_data.get('name') and \ + item.description == post_data.get('description'): + return response_for_updates_with_same_data() + + item.item_name = post_data.get('name') + item.description = post_data.get('description') + + item.save() + + info = { + 'id': item.id, + 'name': item.item_name, + 'description': item.description, + 'completion_status': item.is_completed, + 'date_created': item.created_date, + 'date_modified': item.modified_date + } + response = { + 'status': 'success', + 'message': info + } + return make_response(jsonify(response)), 201 + + def delete(self, bucketlist_id, item_id): + """ + Delete a bucketlist item. + `URL` path: `/api/v1/bucketlists//items/` + """ + # validate token + user_id_response = validate_token(request) + if user_id_response is None: + response = { + 'status': 'fail', + 'message': 'Please provide a valid auth token!' + } + return make_response(jsonify(response)), 401 + + # check if bucketlist exists + bucketlist = Bucketlist.query.filter_by(id=bucketlist_id, + creator_id= + user_id_response).first() + if not bucketlist: + response = { + 'status': 'fail', + 'message': 'Bucketlist not found!' + } + return make_response(jsonify(response)), 404 + + # check if item exists + item = Item.query.filter_by(id=item_id, bucketlist_id= + bucketlist_id).first() + + if not item: + response = { + 'status': 'fail', + 'message': 'Item not found!' + } + return make_response(jsonify(response)), 404 + + item.delete() + + response = { + 'status': 'success', + 'message': 'Item succesfully deleted' + } + return make_response(jsonify(response)), 200 + + +add_bucket_view = Bucketlist_View.as_view('add_bucket_api') +add_item_view = Items_View.as_view('add_item_view') + +# add rules for API endpoints +api_blueprint.add_url_rule( + '/api/v1/bucketlists/', + view_func=add_bucket_view, + methods=['POST', 'GET'] +) + +api_blueprint.add_url_rule( + '/api/v1/bucketlists//', + view_func=add_bucket_view, + methods=['GET', 'PUT', 'DELETE'] +) + +api_blueprint.add_url_rule( + '/api/v1/bucketlists//items/', + view_func=add_item_view, + methods=['POST', 'GET'] +) + +api_blueprint.add_url_rule( + '/api/v1/bucketlists//items//', + view_func=add_item_view, + methods=['GET', 'PUT', 'PATCH', 'DELETE'] +) diff --git a/bucketlist/auth/views.py b/bucketlist/auth/views.py index 5ae19d9..c3942f6 100644 --- a/bucketlist/auth/views.py +++ b/bucketlist/auth/views.py @@ -1,3 +1,8 @@ +import json +import jwt +import os +import re + from flask import Blueprint, make_response, jsonify, request from flask.views import MethodView @@ -12,6 +17,39 @@ class UserRegistration(MethodView): def post(self): # get the post data data_posted = request.get_json() + if (data_posted.get('email') == ''): + response = { + 'status': 'fail', + 'message': 'Please provide an email!' + } + return make_response(jsonify(response)), 400 + if (data_posted.get('username') == ''): + response = { + 'status': 'fail', + 'message': 'Please provide a username!' + } + return make_response(jsonify(response)), 400 + if (data_posted.get('password') == ''): + response = { + 'status': 'fail', + 'message': 'Please a valid password' + } + return make_response(jsonify(response)), 400 + + if (len(data_posted.get('password')) < 8): + response = { + 'status': 'fail', + 'message': 'Password too short!' + } + return make_response(jsonify(response)), 400 + + if not re.match(r'^[A-Za-z0-9\.\+_-]+@[A-Za-z0-9\._-]+\.[a-zA-Z]*$', + data_posted.get('email')): + response = { + 'status': 'fail', + 'message': 'Invalid email!' + } + return make_response(jsonify(response)), 400 # check if the user already exists user = User.query.filter_by(email=data_posted.get('email')).first() if not user: @@ -26,7 +64,7 @@ def post(self): db.session.commit() response = { 'status': 'success', - 'message': 'You have been successfully registered' + 'message': 'You have been successfully registered.' } return make_response(jsonify(response)), 201 except Exception as e: @@ -49,7 +87,12 @@ def post(self): data_posted = request.get_json() try: user = User.query.filter_by(email=data_posted.get('email')).first() - auth_token = user.generate_auth_token(user.id) + if not user: + response = {'status': 'fail', + 'message': 'Invalid username/password!' + } + return make_response(jsonify(response)), 401 + auth_token = user.encode_auth_token(user.id) if not auth_token: response = {'status': 'fail', @@ -58,11 +101,13 @@ def post(self): return make_response(jsonify(response)), 401 response = {'status': 'success', - 'message': 'You have successfully logged in.' + 'message': 'You have successfully logged in.', + 'auth_token': auth_token.decode() } + return make_response(jsonify(response)), 200 except Exception as e: - response = {'status': 'fail' + str(e), + response = {'status': str(e), 'message': 'Login failed! Please try again' } return make_response(jsonify(response)), 500 diff --git a/bucketlist/errors.py b/bucketlist/errors.py new file mode 100644 index 0000000..a8c9016 --- /dev/null +++ b/bucketlist/errors.py @@ -0,0 +1,27 @@ +from flask import Blueprint, make_response, jsonify + +error_blueprint = Blueprint('bucketlist', __name__) + + +@error_blueprint.app_errorhandler(404) +def route_not_found(e): + response = { + 'message': 'Not found' + } + return make_response(jsonify(response)), 404 + + +@error_blueprint.app_errorhandler(405) +def method_not_found(e): + response = { + 'message': 'Method not allowed' + } + return make_response(jsonify(response)), 405 + + +@error_blueprint.app_errorhandler(500) +def internal_server_error(e): + response = { + 'message': 'Internal server error' + } + return make_response(jsonify(response)), 500 diff --git a/bucketlist/models.py b/bucketlist/models.py index 6db505c..4ff5ef7 100644 --- a/bucketlist/models.py +++ b/bucketlist/models.py @@ -2,8 +2,8 @@ import jwt import os -from bucketlist import db -from datetime import datetime +from bucketlist import db, app +from datetime import datetime, timedelta from werkzeug.security import generate_password_hash @@ -27,29 +27,29 @@ def set_password(self, password): pw_hash = generate_password_hash(password) return pw_hash - def generate_auth_token(self, id): + def encode_auth_token(self, id): """Generate the Auth Token.""" try: + expire_date = datetime.utcnow() + timedelta(days=0, minutes=10) payload = { - 'expiration_date': datetime.datetime.utcnow() + - datetime.timedelta(days=0, minutes=10), - 'time_token_is_generated': datetime.datetime.utcnow(), - 'user': id + 'exp': expire_date, + 'iat': datetime.utcnow(), + 'sub': id } return jwt.encode( payload, - app.config.get(os.getenv('SECRET')), + os.getenv('SECRET'), algorithm='HS256' ) except Exception as e: return e @staticmethod - def verify_signature(auth_token): + def decode_auth_token(auth_token): """Decode the auth token and verify the signature.""" try: payload = jwt.decode(auth_token, os.getenv('SECRET')) - return payload['user'] + return payload['sub'] except jwt.ExpiredSignatureError: return 'Signature Expired. Try log in again' except jwt.InvalidTokenError: @@ -67,15 +67,13 @@ class Bucketlist(db.Model): date_modified = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now) creator_id = db.Column(db.Integer, db.ForeignKey('user.id')) - created_by = db.relationship('User', - backref=db.backref('user', lazy='dynamic')) items = db.relationship('Item', backref=db.backref('bucketlists'), cascade="all, delete-orphan") def __repr__(self): """Return printable representation of the object.""" - return "Bucketlist: %d" % self.bucketlist_title + return "Bucketlist: {}}".format(self.bucketlist_title) def save(self): """Save a bucketlist.""" @@ -104,8 +102,14 @@ class Item(db.Model): def __repr__(self): """Return printable representation of the object.""" - return "Item: %d" % self.item_name + return "Item: {}".format(self.item_name) - # def __init__(self, item_name): - # """Initialize with item name.""" - # self.item_name = item_name + def save(self): + """Save an item.""" + db.session.add(self) + db.session.commit() + + def delete(self): + """Delete an item.""" + db.session.delete(self) + db.session.commit() diff --git a/bucketlist/templates/index.html b/bucketlist/templates/index.html new file mode 100644 index 0000000..d65036d --- /dev/null +++ b/bucketlist/templates/index.html @@ -0,0 +1,44 @@ + + + + + apiary + + + + + + diff --git a/dredd.yml b/dredd.yml new file mode 100644 index 0000000..327206e --- /dev/null +++ b/dredd.yml @@ -0,0 +1,36 @@ +reporter: apiary +custom: + apiaryApiKey: adf9b2e7b27564c58d233cd0f55b4906 + apiaryApiName: bucketlistwnjihia +dry-run: null +hookfiles: null +language: python +sandbox: false +server: python run.py +server-wait: 3 +init: false +names: false +only: [] +output: [] +header: [] +sorted: false +user: null +inline-errors: false +details: false +method: [] +color: true +level: info +timestamp: false +silent: false +path: [] +hooks-worker-timeout: 5000 +hooks-worker-connect-timeout: 1500 +hooks-worker-connect-retry: 500 +hooks-worker-after-connect-wait: 100 +hooks-worker-term-timeout: 5000 +hooks-worker-term-retry: 500 +hooks-worker-handler-host: 127.0.0.1 +hooks-worker-handler-port: 61321 +config: ./dredd.yml +blueprint: /Users/waithiranjihia/flask-bucketlist-api/apiary.apib +endpoint: 'http://127.0.0.1:5000' diff --git a/instance/config.py b/instance/config.py index ce20a82..1683a53 100644 --- a/instance/config.py +++ b/instance/config.py @@ -1,5 +1,6 @@ """config.py.""" import os +basedir = os.path.abspath(os.path.dirname(__file__)) class Config(object): @@ -21,7 +22,8 @@ class TestingConfig(Config): """Configurations for Testing, with a separate test database.""" TESTING = True - SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/testdb' + SQLALCHEMY_DATABASE_URI = 'sqlite:////' + os.path.join(basedir, + 'testdb.db') DEBUG = True diff --git a/requirements.txt b/requirements.txt index 4703c6b..b4f3836 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,18 @@ alembic==0.9.2 +aniso8601==1.2.1 click==6.7 +coverage==4.4.1 +dredd-hooks==0.1.3 Flask==0.12.2 Flask-API==0.7.1 Flask-Migrate==2.0.4 +flask-restplus==0.10.1 Flask-Script==2.0.5 Flask-SQLAlchemy==2.2 +gunicorn==19.7.1 itsdangerous==0.24 Jinja2==2.9.6 +jsonschema==2.6.0 Mako==1.0.6 MarkupSafe==1.0 nose==1.3.7 @@ -16,6 +22,7 @@ PyJWT==1.4.2 pytest==3.1.2 python-dateutil==2.6.0 python-editor==1.0.3 +pytz==2017.2 six==1.10.0 SQLAlchemy==1.1.10 Werkzeug==0.12.2 diff --git a/tests/test_bucketlist.py b/tests/test_bucketlist.py index efc6a6d..a0022b9 100644 --- a/tests/test_bucketlist.py +++ b/tests/test_bucketlist.py @@ -8,109 +8,163 @@ class BucketListTestCase(BaseTestCase): URL = "/api/v1/bucketlists/" - def test_create_new_bucketList(self): + def test_create_new_bucketlist(self): """Test for successful creation of a bucketlist.""" - payload = {'bucketlist_title': 'Visit Kenya'} + payload = {'title': 'Visit Kenya'} response = self.client.post(self.URL, data=json.dumps(payload), - headers=self.auth_header, + headers=self.set_header(), content_type="application/json") self.assertEqual(response.status_code, 201) - self.assertEqual("Bucketlist created successfully", str(response.data)) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Bucketlist Visit Kenya has been added", + res_message['message']) - def test_create_new_bucketList_with_invalid_name_format(self): + def test_create_new_bucketlist_with_invalid_token(self): + """Test for creation of a bucketlist with an invalid token.""" + payload = {'title': 'Visit Kenya'} + response = self.client.post(self.URL, data=json.dumps(payload), + content_type="application/json") + self.assertEqual(response.status_code, 401) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Please provide a valid auth token!", + res_message['message']) + + def test_create_new_bucketlist_with_invalid_name_format(self): """Test for creation of a bucketlist with invalid name format.""" - payload = {'bucketlist_title': ''} + payload = {'title': ''} response = self.client.post(self.URL, data=json.dumps(payload), - headers=self.auth_header, + headers=self.set_header(), content_type="application/json") self.assertEqual(response.status_code, 400) - self.assertEqual("Invalid name", str(response.data)) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Invalid bucketlist title!", res_message['message']) - payload = {'bucketlist_title': '@#$%^**^%$'} + payload = {'title': '@#$%^**^%'} response = self.client.post(self.URL, data=json.dumps(payload), - headers=self.auth_header, + headers=self.set_header(), content_type="application/json") self.assertEqual(response.status_code, 400) - self.assertEqual("Invalid name", str(response.data)) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Invalid bucketlist title!", res_message['message']) - def test_create_bucketList_that_exists(self): + def test_create_bucketlist_that_exists(self): """Test for creation of a bucketlist that already exists.""" - payload = {'bucketlist_title': 'Visit Paris'} + payload = {'title': 'Visit Paris'} response = self.client.post(self.URL, data=json.dumps(payload), - headers=self.auth_header, + headers=self.set_header(), content_type="application/json") self.assertEqual(response.status_code, 409) - self.assertIn("This bucketlist already exists", str(response.data)) + res_message = json.loads(response.data.decode('utf8')) + self.assertIn("Bucketlist already exists!", res_message['message']) - def test_get_all_bucketLists(self): + def test_get_all_bucketlists(self): """Test for retrieval of all bucketlists.""" - response = self.client.get(self.URL, headers=self.auth_header) + response = self.client.get(self.URL, headers=self.set_header()) self.assertEqual(response.status_code, 200) - self.assertIn("Visit Paris", str(response.data)) + res_message = json.loads(response.data.decode('utf8')) + self.assertIn("Visit Paris", res_message[0]['title']) - def test_get_bucketList_by_id(self): - """Test for retrieval of a bucketlists by id.""" + def test_get_bucketlist_by_id(self): + """Test for retrieval of a bucketlist by id.""" # Get bucketlist with ID 1 - response = self.client.get("/api/v1/bucketlists/1", - headers=self.auth_header) + response = self.client.get("/api/v1/bucketlists/1/", + headers=self.set_header()) self.assertEqual(response.status_code, 200) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual('Visit Paris', res_message['title']) - # Get bucketlist with ID 2 - payload = {'bucketlist_title': 'Visit Rome'} - response = self.client.post(self.URL, data=json.dumps(payload), - headers=self.auth_header, - content_type="application/json") - response = self.client.get("/api/v1/bucketlists/2") - self.assertEqual(response.status_code, 200) + def test_get_bucketlist_with_invalid_token(self): + """Test for retrieval of a bucketlist with an invalid token.""" + # Get bucketlist with ID 1 + response = self.client.get("/api/v1/bucketlists/1/") + self.assertEqual(response.status_code, 401) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual('Please provide a valid auth token!', + res_message['message']) - def test_get_bucketList_that_does_not_exist(self): + def test_get_bucketlist_that_does_not_exist(self): """Test for retrieval of a bucketlists that does not exist.""" - response = self.client.get("/api/v1/bucketlists/15", - headers=self.auth_header) + response = self.client.get("/api/v1/bucketlists/15/", + headers=self.set_header()) self.assertEqual(response.status_code, 404) - self.assertEqual("Bucketlist cannot be found", str(response.data)) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Bucketlist cannot be found", res_message['message']) - def test_update_bucketList_successfully(self): + def test_update_bucketlist_successfully(self): """Test for updating a bucketlists by id.""" - payload = {'bucketlist_title': 'Visit Israel'} - response = self.client.put("/api/v1/bucketlists/1", + payload = {'title': 'Visit Israel'} + response = self.client.put("/api/v1/bucketlists/1/", data=json.dumps(payload), - headers=self.auth_header, + headers=self.set_header(), content_type="application/json") self.assertEqual(response.status_code, 200) - self.assertEqual("Bucketlist succesfully updated", str(response.data)) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Visit Israel", res_message['message']['title']) - def test_update_bucketList_that_does_not_exist(self): + def test_update_bucketlist_with_invalid_token(self): + """Test for updating a bucketlist with an invalid token.""" + payload = {'title': 'Visit Israel'} + response = self.client.put("/api/v1/bucketlists/1/", + data=json.dumps(payload), + content_type="application/json") + self.assertEqual(response.status_code, 401) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Please provide a valid auth token!", + res_message['message']) + + def test_update_bucketlist_that_does_not_exist(self): """Test for updating a bucketlist that does not exist.""" - payload = {'bucketlist_title': 'Visit Israel'} - response = self.client.put("/api/v1/bucketlists/15", + payload = {'title': 'Visit Israel'} + response = self.client.put("/api/v1/bucketlists/15/", data=json.dumps(payload), - headers=self.auth_header, + headers=self.set_header(), content_type="application/json") self.assertEqual(response.status_code, 404) - self.assertEqual("Bucketlist cannot be found", str(response.data)) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Bucketlist does not exist!", res_message['message']) - def test_update_bucketList_with_the_same_data(self): + def test_update_bucketlist_with_the_same_data(self): """Test for updating a bucketlist with the same data.""" - payload = {'bucketlist_title': 'Visit Paris'} - response = self.client.put("/api/v1/bucketlists/1", + payload = {'title': 'Visit Paris'} + response = self.client.put("/api/v1/bucketlists/1/", data=json.dumps(payload), - headers=self.auth_header, + headers=self.set_header(), content_type="application/json") self.assertEqual(response.status_code, 409) + res_message = json.loads(response.data.decode('utf8')) self.assertEqual("No updates detected", - str(response.data)) + res_message['message']) - def test_delete_bucketList_successfully(self): + def test_delete_bucketlist_successfully(self): """Test for deleting a bucketlist succesfully.""" - response = self.client.delete("/api/v1/bucketlists/1", - headers=self.auth_header) + response = self.client.delete("/api/v1/bucketlists/1/", + headers=self.set_header()) + self.assertEqual(response.status_code, 200) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Bucketlist successfully deleted!", + res_message['message']) + + def test_delete_bucketlist_with_invalid_token(self): + """Test for deleting a bucketlist with an invalid token.""" + response = self.client.delete("/api/v1/bucketlists/1/") + self.assertEqual(response.status_code, 401) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Please provide a valid auth token!", + res_message['message']) + + def test_delete_bucketlist_with_invalid_bucketlist(self): + """Test for deleting a bucketlist with an invalid bucketlist.""" + response = self.client.delete("/api/v1/bucketlists/1/", + headers=self.set_header()) self.assertEqual(response.status_code, 200) - self.assertEqual("Bucketlist succesfully deleted", str(response.data)) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Bucketlist successfully deleted!", + res_message['message']) - def test_delete_bucketList_that_does_not_exist(self): + def test_delete_bucketlist_that_does_not_exist(self): """Test for deleting a bucketlist that does not exist.""" - response = self.client.delete("/api/v1/bucketlists/15", - headers=self.auth_header) + response = self.client.delete("/api/v1/bucketlists/15/", + headers=self.set_header()) self.assertEqual(response.status_code, 404) - self.assertEqual("Bucketlist cannot be found", str(response.data)) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Bucketlist cannot be found", res_message['message']) diff --git a/tests/test_items.py b/tests/test_items.py index 745c0a2..930f32f 100644 --- a/tests/test_items.py +++ b/tests/test_items.py @@ -1,158 +1,248 @@ """test_bucketlistitems.py.""" import json from tests.test_setup import BaseTestCase -from bucketlist.models import Item class ItemsTestCase(BaseTestCase): """This class contains tests for items.""" - def test_create_new_Item(self): + def test_create_new_item(self): """Test for successful creation of an item.""" - payload = {'item_name': 'The Louvre', + payload = {'name': 'The Louvre', 'description': 'Largest museum in Paris'} - response = self.client.post("/api/v1/bucketlists/1/items", + response = self.client.post("/api/v1/bucketlists/1/items/", data=json.dumps(payload), - headers=self.auth_header, + headers=self.set_header(), content_type="application/json") self.assertEqual(response.status_code, 201) - self.assertEqual("Item created successfully", str(response.data)) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Item The Louvre has been added", + res_message['message']) + + def test_create_new_item_with_invalid_token(self): + """Test for creation of an item with an invalid token.""" + payload = {'name': 'The Louvre', + 'description': 'Largest museum in Paris'} + response = self.client.post("/api/v1/bucketlists/1/items/", + data=json.dumps(payload), + content_type="application/json") + self.assertEqual(response.status_code, 401) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Please provide a valid auth token!", + res_message['message']) def test_create_Item_with_invalid_name_format(self): """Test for creation of an item with an invalid name format.""" - payload = {'item_name': '1234%$#@!^&', + payload = {'name': '[]**%', 'description': 'Largest museum in Paris'} - response = self.client.post("/api/v1/bucketlists/1/items", + response = self.client.post("/api/v1/bucketlists/1/items/", data=json.dumps(payload), - headers=self.auth_header, + headers=self.set_header(), content_type="application/json") self.assertEqual(response.status_code, 400) - self.assertEqual("Invalid format", str(response.data)) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Invalid name format", res_message['message']) - def test_create_Item_that_exists(self): + def test_create_item_that_exists(self): """Test for creation of an item that already exists.""" - payload = {'item_name': 'The Eiffel Tower', + payload = {'name': 'The Eiffel Tower', 'description': 'Wrought iron lattice tower in France'} - response = self.client.post("/api/v1/bucketlists/1/items", + response = self.client.post("/api/v1/bucketlists/1/items/", data=json.dumps(payload), - headers=self.auth_header, + headers=self.set_header(), content_type="application/json") self.assertEqual(response.status_code, 409) - self.assertIn("This item already exists", str(response.data)) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Item already exists!", res_message['message']) - def test_create_Item_with_non_existent_bucketlist(self): + def test_create_item_with_non_existent_bucketlist(self): """Test creation of an item with non existent bucketlist.""" - payload = {'item_name': 'The Louvre', + payload = {'name': 'The Louvre', 'description': 'Largest museum in Paris'} - response = self.client.post("/api/v1/bucketlists/15/items", + response = self.client.post("/api/v1/bucketlists/15/items/", data=json.dumps(payload), - headers=self.auth_header, + headers=self.set_header(), content_type="application/json") self.assertEqual(response.status_code, 404) - self.assertEqual("Bucketlist cannot be found", str(response.data)) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Bucketlist not found!", res_message['message']) - def test_get_all_BucketListItems(self): + def test_get_all_bucketlistitems(self): """Test retrieval of items successfully.""" - response = self.client.get("/api/v1/bucketlists/1/items") + response = self.client.get("/api/v1/bucketlists/1/items/", + headers=self.set_header()) self.assertEqual(response.status_code, 200) - self.assertIn("The Eiffel Tower", str(response.data)) - - def test_get_Items_with_invalid_BucketList_Id(self): + res_message = json.loads(response.data.decode('utf8')) + self.assertIn("The Eiffel Tower", res_message[0]['name']) + + def test_get_all_bucketlistitems_with_invalid_token(self): + """Test retrieval of items with an invalid token.""" + response = self.client.get("/api/v1/bucketlists/1/items/") + self.assertEqual(response.status_code, 401) + res_message = json.loads(response.data.decode('utf8')) + self.assertIn("Please provide a valid auth token!", + res_message['message']) + + def test_get_items_with_invalid_bucketList_id(self): """Test retrieval of items with invalid bucketlist ID.""" - response = self.client.get("/api/v1/bucketlists/15/items", - headers=self.auth_header) + response = self.client.get("/api/v1/bucketlists/15/items/", + headers=self.set_header()) self.assertEqual(response.status_code, 404) - self.assertEqual("Bucketlist cannot be found", str(response.data)) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Bucketlist not found!", res_message['message']) - def test_get_Items_by_id(self): + def test_get_items_by_id(self): """Test retrieval of an item by ID.""" - response = self.client.get("/api/v1/bucketlists/1/items/1", - headers=self.auth_header) + response = self.client.get("/api/v1/bucketlists/1/items/1/", + headers=self.set_header()) self.assertEqual(response.status_code, 200) - def test_update_Item_by_id(self): + def test_update_item_by_id(self): """Test updating an item by ID.""" - payload = {'item_name': 'The Eiffel Tower', + payload = {'name': 'Just a tower', 'description': 'Tallest building in France'} - response = self.client.put("/api/v1/bucketlists/1/items/1", + response = self.client.put("/api/v1/bucketlists/1/items/1/", data=json.dumps(payload), - headers=self.auth_header, + headers=self.set_header(), content_type="application/json") - self.assertEqual(response.status_code, 200) - self.assertEqual("Item succesfully updated", str(response.data)) + self.assertEqual(response.status_code, 201) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Just a tower", res_message['message']['name']) - def test_update_Items_with_invalid_BucketList_Id(self): + def test_update_item_with_invalid_token(self): + """Test updating an item with an invlid token.""" + payload = {'name': 'Just a tower', + 'description': 'Tallest building in France'} + response = self.client.put("/api/v1/bucketlists/1/items/1/", + data=json.dumps(payload), + content_type="application/json") + self.assertEqual(response.status_code, 401) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Please provide a valid auth token!", + res_message['message']) + + def test_update_items_with_invalid_bucketList_id(self): """Test updating an item with invalid Bucketlist ID.""" - payload = {'item_name': 'The Eiffel Tower', + payload = {'name': 'The Eiffel Tower', 'description': 'Tallest building in France'} - response = self.client.put("/api/v1/bucketlists/15/items/1", + response = self.client.put("/api/v1/bucketlists/15/items/1/", data=json.dumps(payload), - headers=self.auth_header, + headers=self.set_header(), content_type="application/json") self.assertEqual(response.status_code, 404) - self.assertEqual("Bucketlist cannot be found", str(response.data)) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Bucketlist not found!", res_message['message']) - def test_update_Item_that_does_not_exist(self): + def test_update_item_that_does_not_exist(self): """Test updating an item that does not exist.""" - payload = {'item_name': 'The Eiffel Tower', + payload = {'name': 'The Eiffel Tower', 'description': 'Tallest building in France'} - response = self.client.put("/api/v1/bucketlists/1/items/15", + response = self.client.put("/api/v1/bucketlists/1/items/15/", data=json.dumps(payload), - headers=self.auth_header, + headers=self.set_header(), content_type="application/json") self.assertEqual(response.status_code, 404) - self.assertEqual("Item cannot be found", str(response.data)) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Item not found!", res_message['message']) - def test_update_Item_with_same_data(self): + def test_update_item_with_the_same_data(self): """Test updating an item with the same data.""" - payload = {'item_name': 'The Eiffel Tower', + payload = {'name': 'The Eiffel Tower', 'description': 'Wrought iron lattice tower in France'} - response = self.client.put("/api/v1/bucketlists/1/items/1", + response = self.client.put("/api/v1/bucketlists/1/items/1/", data=json.dumps(payload), - headers=self.auth_header, + headers=self.set_header(), content_type="application/json") self.assertEqual(response.status_code, 409) + res_message = json.loads(response.data.decode('utf8')) self.assertEqual("No updates detected", - str(response.data)) + res_message['message']) - def test_delete_Item_by_id(self): + def test_delete_item_successfully(self): """Test deleting an item by ID.""" - response = self.client.delete("/api/v1/bucketlists/1/items/1", - headers=self.auth_header) + payload = {'name': 'The Louvre', + 'description': 'Largest museum in Paris'} + self.client.post("/api/v1/bucketlists/1/items/", + data=json.dumps(payload), + headers=self.set_header(), + content_type="application/json") + response = self.client.delete("/api/v1/bucketlists/1/items/2/", + headers=self.set_header()) self.assertEqual(response.status_code, 200) - self.assertEqual("Item succesfully deleted", str(response.data)) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Item succesfully deleted", res_message['message']) + + def test_delete_item_with_invalid_token(self): + """Test deleting an item with an invalid token.""" + payload = {'name': 'The Louvre', + 'description': 'Largest museum in Paris'} + self.client.post("/api/v1/bucketlists/1/items/", + data=json.dumps(payload), + content_type="application/json") + response = self.client.delete("/api/v1/bucketlists/1/items/2/") + self.assertEqual(response.status_code, 401) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Please provide a valid auth token!", + res_message['message']) - def test_delete_Item_that_does_not_exist(self): + def test_delete_item_that_does_not_exist(self): """Test deleting an item that does not exist.""" - response = self.client.delete("/api/v1/bucketlists/1/items/15", - headers=self.auth_header,) + response = self.client.delete("/api/v1/bucketlists/1/items/15/", + headers=self.set_header(),) self.assertEqual(response.status_code, 404) - self.assertEqual("Item cannot be found", str(response.data)) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Item not found!", res_message['message']) - def test_delete_Items_with_invalid_BucketList_Id(self): + def test_delete_items_with_invalid_bucketList_id(self): """Test deleting an item with an invalid bucketlist ID.""" - response = self.client.delete("/api/v1/bucketlists/5/items/1", - headers=self.auth_header,) + response = self.client.delete("/api/v1/bucketlists/5/items/1/", + headers=self.set_header(),) self.assertEqual(response.status_code, 404) - self.assertEqual("Bucketlist cannot be found", str(response.data)) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Bucketlist not found!", res_message['message']) - def test_change_Item_status(self): + def test_change_item_status(self): """Test change of item status""" - payload = {'item_name': 'The Louvre', + payload = {'name': 'The Louvre', 'description': 'Largest museum in Paris'} - self.client.post("/api/v1/bucketlists/1/items", + self.client.post("/api/v1/bucketlists/1/items/", data=json.dumps(payload), - headers=self.auth_header, + headers=self.set_header(), content_type="application/json") - new_item = Item.query.filter_by(item_name="The Louvre") - self.assertTrue(new_item.is_completed, False) - - payload = {'is_completed': True} - response = self.client.put("/api/v1/bucketlists/1/items/1", + response = self.client.get("/api/v1/bucketlists/1/items/2/", data=json.dumps(payload), - headers=self.auth_header, - content_type="application/json") + headers=self.set_header()) + res_message = json.loads(response.data.decode('utf8')) + self.assertFalse(res_message['is_completed'], False) + + payload = {'is_completed': 'true'} + response = self.client.patch("/api/v1/bucketlists/1/items/2/", + data=json.dumps(payload), + headers=self.set_header(), + content_type="application/json") self.assertEqual(response.status_code, 200) - self.assertEqual("Item succesfully updated", str(response.data)) - new_item = Item.query.filter_by(item_name="The Louvre") - self.assertTrue(new_item.is_completed, True) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual(res_message['message']['completion_status'], True) + + def test_change_item_status_with_invalid_token(self): + """Test change of item status with an invalid token""" + payload = {'name': 'The Louvre', + 'description': 'Largest museum in Paris'} + self.client.post("/api/v1/bucketlists/1/items/", + data=json.dumps(payload), + headers=self.set_header(), + content_type="application/json") + response = self.client.get("/api/v1/bucketlists/1/items/2/", + data=json.dumps(payload), + headers=self.set_header()) + res_message = json.loads(response.data.decode('utf8')) + self.assertFalse(res_message['is_completed'], False) + + payload = {'is_completed': 'true'} + response = self.client.patch("/api/v1/bucketlists/1/items/2/", + data=json.dumps(payload), + content_type="application/json") + self.assertEqual(response.status_code, 401) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual(res_message['message'], + "Please provide a valid auth token!") diff --git a/tests/test_setup.py b/tests/test_setup.py index 8adcebc..f4d26ba 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -1,5 +1,6 @@ """base test file.""" import unittest +import json from bucketlist import create_app, db from bucketlist.models import User, Bucketlist, Item @@ -16,8 +17,8 @@ def setUp(self): self.app_context.push() db.create_all() - user = User(username="johndoe", - email="johndoe@gmail.com", + user = User(username="lynn", + email="lynn@gmail.com", password="password") bucketlist = Bucketlist(bucketlist_title="Visit Paris", creator_id=1) item = Item(item_name="The Eiffel Tower", @@ -27,11 +28,23 @@ def setUp(self): db.session.add(user) db.session.add(bucketlist) db.session.add(item) - db.session.commit() - - # set header - self.auth_header = {'Authorization': user.generate_auth_token(user.id)} - self.token = user.generate_auth_token(user.id) + try: + db.session.commit() + except: + db.session.rollback() + + def set_header(self): + payload = dict(email='lynn@gmail.com', + password='password' + ) + response = self.client.post('/api/v1/auth/login', + data=json.dumps(payload), + content_type="application/json") + res_message = json.loads(response.data.decode()) + print(res_message) + self.token = res_message['auth_token'] + return {'Authorization': 'Token ' + self.token + } def tearDown(self): """Tear down the test database.""" diff --git a/tests/test_user_auth.py b/tests/test_user_auth.py index 373e92e..1406000 100644 --- a/tests/test_user_auth.py +++ b/tests/test_user_auth.py @@ -13,7 +13,7 @@ def test_successful_user_registration(self): """Test for successful user registration.""" self.payload = dict(username='test_username', email='user@test.com', - password='1234' + password='123456789' ) response = self.client.post(self.REGISTER_URL, data=json.dumps(self.payload), @@ -22,11 +22,24 @@ def test_successful_user_registration(self): self.assertIn("You have been successfully registered.", str(response.data)) + def test_user_registration_with_short_password(self): + """Test for successful user registration.""" + self.payload = dict(username='test_username', + email='user@test.com', + password='1234' + ) + response = self.client.post(self.REGISTER_URL, + data=json.dumps(self.payload), + content_type="application/json") + self.assertEqual(response.status_code, 400) + self.assertIn("Password too short!", + str(response.data)) + def test_user_registration_when_user_already_exists(self): """Test registration of an already existing user.""" self.payload = dict(username='test_username', email='user@test.com', - password='1234' + password='123456789' ) self.client.post(self.REGISTER_URL, data=json.dumps(self.payload), content_type="application/json") @@ -39,48 +52,65 @@ def test_user_registration_when_user_already_exists(self): def test_user_registration_with_no_email(self): """Test for user registration with no email.""" self.payload = dict(username='test_username', - password='1234' + password='123456789', + email='' + ) + response = self.client.post(self.REGISTER_URL, + data=json.dumps(self.payload), + content_type="application/json") + self.assertEqual(response.status_code, 400) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Please provide an email!", res_message['message']) + + def test_user_registration_with_invalid_email_format(self): + """Test for user registration with invalid email format.""" + self.payload = dict(username='test_username', + email='memi.gmail', + password='123456789' ) response = self.client.post(self.REGISTER_URL, data=json.dumps(self.payload), content_type="application/json") self.assertEqual(response.status_code, 400) - self.assertEqual("Please provide an email!", str(response.data)) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Invalid email!", res_message['message']) def test_user_registration_with_empty_username(self): """Test for user registration with empty username.""" self.payload = dict(username='', email='user@test.com', - password='1234' + password='123456789' ) response = self.client.post(self.REGISTER_URL, data=json.dumps(self.payload), content_type="application/json") self.assertEqual(response.status_code, 400) - self.assertEqual("Please provide an email!", str(response.data)) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Please provide a username!", res_message['message']) def test_user_registration_username_already_exists(self): """Test for registration with an already existing username.""" self.payload = dict(username='test_username', email='user@test.com', - password='1234' + password='123456789' ) self.client.post(self.REGISTER_URL, data=json.dumps(self.payload), content_type="application/json") self.data = dict(username='test_username', email='me@user.com', - password='56789' + password='453256789' ) response = self.client.post(self.REGISTER_URL, data=json.dumps(self.payload), content_type="application/json") - self.assertEqual(response.status_code, 403) - self.assertEqual("Username already exists! Please provide another", - str(response.data)) + self.assertEqual(response.status_code, 409) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("User already exists!", + res_message['message']) def test_user_login(self): """Test for successful user login.""" - self.payload = dict(email="ciranjihia@gmail.com", + self.payload = dict(email="lynn@gmail.com", password="password" ) response = self.client.post(self.LOGIN_URL, @@ -88,34 +118,40 @@ def test_user_login(self): content_type="application/json") self.assertEqual(response.status_code, 200) res_message = json.loads(response.data.decode('utf8')) - self.assertEqual("You have successfully logged in.", res_message['message']) + self.assertEqual("You have successfully logged in.", + res_message['message']) def test_user_login_with_invalid_credentials(self): """Test for user login with invalid user credentials.""" self.payload = dict(email="johndoe@gmail.com", - password="johnny" + password="johnny12" ) - response = self.client.post(self.LOGIN_URL, data=json.dumps(self.payload), + response = self.client.post(self.LOGIN_URL, + data=json.dumps(self.payload), content_type="application/json") self.assertEqual(response.status_code, 401) - self.assertEqual("Invalid username/password!", str(response.data)) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Invalid username/password!", res_message['message']) self.payload = dict(email="me@gmail.com", - password="ohndoe" + password="ohndoe12" ) response = self.client.post(self.LOGIN_URL, data=json.dumps(self.payload), content_type="application/json") self.assertEqual(response.status_code, 401) - self.assertEqual("Invalid username/password!", str(response.data)) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Invalid username/password!", res_message['message']) + # repetitive test def test_user_login_with_unregistered_user(self): """Test for login with an unregistered user.""" self.payload = dict(email="jane@gmail.com", - password="jane" + password="jane1234" ) response = self.client.post(self.LOGIN_URL, data=json.dumps(self.payload), content_type="application/json") - self.assertEqual(response.status_code, 404) - self.assertEqual("Invalid username/password!", str(response.data)) + self.assertEqual(response.status_code, 401) + res_message = json.loads(response.data.decode('utf8')) + self.assertEqual("Invalid username/password!", res_message['message'])