diff --git a/.coverage b/.coverage new file mode 100644 index 0000000..29290f2 --- /dev/null +++ b/.coverage @@ -0,0 +1 @@ +!coverage.py: This is a private format, don't read it directly!{"arcs":{"/home/ipaullly/dev/parcels/sendIT/app/__init__.py":[[-1,1],[1,2],[2,3],[3,6],[6,-1],[-6,7],[7,9],[9,10],[10,11],[11,13],[13,-6]],"/home/ipaullly/dev/parcels/sendIT/app/api/__init__.py":[[-1,1],[1,-1]],"/home/ipaullly/dev/parcels/sendIT/app/api/v1/__init__.py":[[-1,1],[1,2],[2,3],[3,5],[5,7],[7,9],[9,10],[10,11],[11,12],[12,-1]],"/home/ipaullly/dev/parcels/sendIT/app/api/v1/views.py":[[-1,1],[1,2],[2,3],[3,5],[5,7],[-7,7],[7,10],[10,11],[11,38],[38,-7],[7,48],[-48,48],[48,51],[51,52],[52,-48],[48,69],[-69,69],[69,72],[72,73],[73,-69],[69,80],[-80,80],[80,83],[83,84],[84,-80],[80,-1],[-11,15],[15,16],[16,17],[17,18],[18,19],[19,20],[20,22],[22,23],[23,24],[24,33],[33,34],[34,35],[35,36],[36,-11],[23,25],[25,28],[28,29],[29,30],[30,31],[31,32],[32,-11],[-52,57],[57,58],[58,65],[65,67],[67,-52],[-38,42],[42,43],[43,44],[44,45],[45,46],[46,-38],[-73,74],[74,75],[75,76],[76,77],[77,78],[78,-73],[58,59],[59,60],[60,61],[61,62],[62,-52],[-84,88],[88,89],[89,90],[90,91],[91,92],[92,-84]],"/home/ipaullly/dev/parcels/sendIT/app/api/v1/models.py":[[-2,2],[2,4],[-4,4],[4,7],[7,8],[8,12],[12,28],[28,34],[34,45],[45,53],[53,-4],[4,-2],[-8,9],[9,10],[10,-8],[-12,17],[17,18],[18,19],[19,20],[20,21],[21,22],[22,23],[23,25],[25,26],[26,-12],[-34,38],[38,39],[39,42],[42,-34],[-28,32],[32,-28],[-53,57],[-57,57],[57,57],[57,-57],[57,58],[58,-53],[39,40],[40,-34],[-45,49],[-49,49],[49,49],[49,-49],[49,50],[50,51],[51,-45]],"/home/ipaullly/dev/parcels/sendIT/app/auth/__init__.py":[[-1,1],[1,-1]],"/home/ipaullly/dev/parcels/sendIT/app/auth/v1/__init__.py":[[-1,1],[1,2],[2,3],[3,5],[5,7],[7,9],[9,10],[10,-1]],"/home/ipaullly/dev/parcels/sendIT/app/auth/v1/views.py":[[-1,1],[1,2],[2,3],[3,4],[4,6],[-6,6],[6,9],[9,10],[10,-6],[6,42],[-42,42],[42,45],[45,46],[46,-42],[42,-1],[-10,11],[11,12],[12,13],[13,15],[15,37],[37,39],[39,-10],[15,16],[16,32],[32,34],[34,-10],[-46,47],[47,48],[48,49],[49,51],[51,52],[52,53],[53,66],[66,68],[68,70],[70,-46],[16,17],[17,18],[18,19],[19,21],[21,22],[22,23],[23,-10],[53,54],[54,55],[55,56],[56,58],[58,60],[60,-46],[17,26],[26,29],[29,-10]],"/home/ipaullly/dev/parcels/sendIT/app/auth/v1/models.py":[[-1,1],[1,2],[2,3],[3,4],[4,6],[-6,6],[6,9],[9,10],[10,15],[15,-6],[6,18],[-10,11],[11,12],[12,13],[13,14],[14,-10],[18,20],[-20,20],[20,23],[23,24],[24,-20],[20,31],[-31,31],[31,34],[34,35],[35,42],[42,52],[52,60],[60,67],[67,86],[86,-31],[31,-1],[-86,91],[91,95],[95,-86],[-35,36],[36,37],[37,38],[38,39],[39,40],[40,-35],[-42,46],[46,47],[47,48],[48,49],[49,50],[-60,64],[64,65],[-65,65],[65,65],[65,-65],[65,-60],[50,-42],[91,92],[92,93],[93,94],[94,-86],[-52,56],[56,57],[57,-52],[-67,71],[71,73],[73,74],[74,75],[75,77],[77,78],[78,79],[79,80],[80,83],[83,84],[84,-67],[93,91]],"/home/ipaullly/dev/parcels/sendIT/app/utilities/__init__.py":[[-1,1],[1,-1]],"/home/ipaullly/dev/parcels/sendIT/app/utilities/validation_functions.py":[[-1,1],[1,3],[3,10],[10,-1],[-3,4],[4,5],[5,8],[8,-3],[5,6],[6,-3],[-10,11],[11,12],[12,15],[15,-10],[12,13],[13,-10]],"/home/ipaullly/dev/parcels/sendIT/app/tests/__init__.py":[[-1,1],[1,-1]],"/home/ipaullly/dev/parcels/sendIT/app/tests/v1/__init__.py":[[-1,1],[1,-1]],"/home/ipaullly/dev/parcels/sendIT/app/tests/v1/test_edgecases.py":[[-1,1],[1,2],[2,3],[3,5],[-5,5],[5,8],[8,9],[9,38],[38,47],[47,53],[53,59],[59,-5],[5,66],[66,-1],[-9,13],[13,14],[14,16],[16,17],[17,18],[18,19],[19,20],[20,23],[23,24],[24,27],[27,28],[28,31],[31,32],[32,33],[33,34],[34,35],[35,-9],[-38,42],[42,43],[43,44],[44,45],[45,-38],[-59,60],[60,61],[61,62],[62,63],[63,64],[64,-59],[-47,48],[48,49],[49,50],[50,51],[51,-47],[-53,54],[54,55],[55,56],[56,57],[57,-53]],"/home/ipaullly/dev/parcels/sendIT/app/tests/v1/test_login.py":[[-1,1],[1,2],[2,3],[3,5],[-5,5],[5,8],[8,9],[9,21],[21,31],[31,-5],[5,39],[39,-1],[-9,10],[10,11],[11,13],[13,14],[14,17],[17,18],[18,-9],[-31,33],[33,34],[34,35],[35,36],[36,-31],[-21,23],[23,24],[24,25],[25,26],[26,27],[27,28],[28,29],[29,-21]],"/home/ipaullly/dev/parcels/sendIT/app/tests/v1/test_parcels.py":[[-1,1],[1,2],[2,3],[3,5],[-5,5],[5,8],[8,9],[9,23],[23,32],[32,43],[43,53],[53,63],[63,-5],[5,75],[75,-1],[-9,13],[13,14],[14,16],[16,17],[17,18],[18,19],[19,20],[20,-9],[-32,36],[36,37],[37,38],[38,39],[39,40],[40,41],[41,-32],[-53,57],[57,58],[58,59],[59,60],[60,61],[61,-53],[-43,47],[47,48],[48,49],[49,50],[50,51],[51,-43],[-23,27],[27,28],[28,29],[29,30],[30,-23],[-63,67],[67,68],[68,69],[69,70],[70,71],[71,-63]],"/home/ipaullly/dev/parcels/sendIT/app/tests/v1/test_register.py":[[-1,1],[1,2],[2,3],[3,5],[-5,5],[5,8],[8,9],[9,22],[22,28],[28,-5],[5,36],[36,-1],[-9,10],[10,11],[11,13],[13,14],[14,17],[17,18],[18,-9],[-28,29],[29,30],[30,31],[31,32],[32,33],[33,34],[34,-28],[-22,23],[23,24],[24,25],[25,-22]],"/home/ipaullly/dev/parcels/sendIT/app/utilities/JWT_token.py":[]}} \ No newline at end of file diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..a3caf3b --- /dev/null +++ b/.coveragerc @@ -0,0 +1,13 @@ +[run] +branch = True +source = app + +[report] +exclude_lines = + if self.debug: + pragma: no cover + raise NotImplementedError + if __name__ == .__main__.: +ignore_errors = True +omit = + tests/* \ No newline at end of file diff --git a/.gitignore b/.gitignore index e2d16ff..7281740 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ *__pycache__ venv .vscode +*.pytest_cache +db_config.py diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ca2f18f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,23 @@ +env: + global: + - CC_TEST_REPORTER_ID=e6fc31601f7b6906c6ff9c0bfc5afb08d679e93ea89c315aaf6758c20bcdc274 +language: python +python: + - "3.6.5" +install: + - pip install --upgrade pip && pip install -r requirements.txt + - pip install coveralls + - pip install codecov + - pip install pytest +before_script: + - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter + - chmod +x ./cc-test-reporter + - ./cc-test-reporter before-build +script: + - pytest +after_script: + - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT + - coveralls +after_success: + - codecov -t b1bc8af2-07e9-45e9-8824-c733282c6cea + diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..62e430a --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: gunicorn 'app:create_app()' \ No newline at end of file diff --git a/README.md b/README.md index 5970e9f..903a8cc 100644 --- a/README.md +++ b/README.md @@ -1 +1,100 @@ # sendIT +[![Maintainability](https://api.codeclimate.com/v1/badges/db4df351dbe833d147b0/maintainability)](https://codeclimate.com/github/ipaullly/sendIT/maintainability) + [![Build Status](https://travis-ci.com/ipaullly/sendIT.svg?branch=ft-GET-user-orders-161858618)](https://travis-ci.com/ipaullly/sendIT) [![codecov](https://codecov.io/gh/ipaullly/sendIT/branch/ch-code-climates-161921842/graph/badge.svg)](https://codecov.io/gh/ipaullly/sendIT) + +The sendIT app is built using flask to make RESTful APIs to achieve basic functionalities for the app + +## RESTful API Endpoints for sendIT + + +| Method | Endpoint | Description | +| ------------- | ------------- | ------------- | +| `POST` | `/api/v1/parcels` | Create a new parcel order | +| `GET` | `/api/v1/parcels` | Get a all parcel delivery orders | +| `GET` | `/api/v1/parcels/` | Get a single delivery order by id | +| `POST` | `/auth/v1/register` | Register a new user | +| `POST` | `/auth/v1/login` | log a user into account | +| `PUT` | `/api/v1/parcels//cancel` | Cancel a specific parcel delivery order | + + +# Development Configuration + +Ensure that you have python 3.6.5, pip and virtualenv running + +# Initial Setup + +Create a project directory in your local machine + +``` +mkdir sendIT +``` + +Move into your directory: + +``` +cd sendIT +``` + +## Initialize a virtual python Environment to House all your Dependencies + +create the virtual environment + +``` +python3 -p virtualenv venv +``` +activate the environment before cloning the project from github + +``` +source venv/bin/activate +``` + +## Clone and Configure a the sendIT flask Project + +Provided you have a github account, login before entering the command to create a local copy of the repo + +``` +git clone https://github.com/ipaullly/sendIT.git +``` + +Next, install the requirements by typing: + +``` +pip install -r requirements.txt +``` +## Running the sendIT flask app locally + +Once you are in a virtual environment with all the dependencies installed, set the environmental variables: +``` +export FLASK_APP=run.py +export FLASK_ENV=development +``` +initialize the server with the command: +``` +flask run +``` + +## Testing +To test the endpointsensure that the following tools are available the follow steps below + +### Tool: + Postman + + with the flask server running power up POSTMAN to test your endpoints. set the localhost: + ``` + http://127.0.0.1:5000/ + ``` + append the localhost with urls for the various endpoints, for example: + ``` + http://127.0.0.1:5000/api/v1/parcels + ``` + ensure to set the correct HTTP method before sending the request. + + Alternatively you can access the API documentation via this [link](https://documenter.getpostman.com/view/4014888/RzZCCwun#22450978-87c5-be21-e538-51cd4100035a). + +### Commands + The application was tested using `pytest` and code cov. + run the command + ``` + pytest --cov app + ``` + this generates a detailed log of the tests in your app directory diff --git a/app/__init__.py b/app/__init__.py index 80afb5b..fe9c30a 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,8 +1,13 @@ from flask import Flask, Blueprint from .api.v1 import version1 +from .auth.v1 import auth +#from db_config import create_tables, destroy_tables def create_app(): - app = Flask(__name__) + app = Flask(__name__, instance_relative_config=True) + #create_tables() + app.config.from_pyfile('config.py') app.register_blueprint(version1) + app.register_blueprint(auth) return app \ No newline at end of file diff --git a/app/__init__.pyc b/app/__init__.pyc new file mode 100644 index 0000000..0419f2d Binary files /dev/null and b/app/__init__.pyc differ diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py index af06015..c251c17 100644 --- a/app/api/v1/__init__.py +++ b/app/api/v1/__init__.py @@ -1,7 +1,12 @@ from flask import Blueprint from flask_restful import Api - +from .views import ParcelList, IndividualParcel, CancelParcel, UserOrders version1 = Blueprint('v1', __name__, url_prefix="/api/v1") api = Api(version1) + +api.add_resource(ParcelList, '/parcels') +api.add_resource(IndividualParcel, '/parcels/') +api.add_resource(UserOrders, '/user//parcels') +api.add_resource(CancelParcel, '/parcels//cancel') diff --git a/app/api/v1/models.py b/app/api/v1/models.py index e69de29..d86d622 100644 --- a/app/api/v1/models.py +++ b/app/api/v1/models.py @@ -0,0 +1,63 @@ + +parcels = [] + +class Parcels: + """ + Class with CRUD functionalities on the Parcels resource + """ + def __init__(self): + self.db = parcels + self.parcel_status = 'pending' + + def create_order(self, item, pickup, dest, pricing, username): + """ + instance method to generate new entry into delivery orders list + """ + payload = { + "id" : len(self.db) + 1, + "itemName" : item, + "pickupLocation" : pickup, + "destination" : dest, + "pricing" : pricing, + "authorId" : username, + "status" : self.parcel_status + } + self.db.append(payload) + return payload + + def order_list(self): + """ + retrieves entire list of delivery orders + """ + return self.db + + def retrieve_single_order(self, parcelID): + """ + retrieve a single order by id + """ + single_parc = [parc for parc in self.db if parc['id'] == parcelID] + return single_parc + + + def cancel_order(self, ParcelID): + """ + update parcel status to cancel + """ + parcel_to_cancel = [parc for parc in self.db if parc['id'] == ParcelID] + parcel_to_cancel[0]['status'] = 'cancelled' + return parcel_to_cancel + + def get_orders_by_user(self, AuthorID): + """ + retrieve all orders by a specific user given her/his username + """ + user_orders = [parc for parc in self.db if parc['authorId'] == AuthorID][0] + return user_orders + + def validate_ID(self, ID): + for parc in self.db: + list_of_keys = [key for (key, value) in parc.items() if value == ID] + if list_of_keys: + return True + else: + return False diff --git a/app/api/v1/views.py b/app/api/v1/views.py index 2842140..c985c72 100644 --- a/app/api/v1/views.py +++ b/app/api/v1/views.py @@ -1,19 +1,108 @@ from flask_restful import Resource from flask import make_response, jsonify, request +from .models import Parcels +order = Parcels() class ParcelList(Resource): - + """ + class for Create order and retrieve list of orders API endpoints + """ def post(self): - pass + """ + post method to add new order to list of orders + """ + data = request.get_json() + item = data['item'] + pickup = data['pickup'] + dest = data['dest'] + pricing = data['pricing'] + author = data['user_id'] + try: + if " " in item: + raise Exception + elif type(pricing) is not int: + raise Exception + else: + res = order.create_order(item, pickup, dest, pricing, author) + return make_response(jsonify({ + "message" : "delivery order created successfully", + "new delivery order" : res + }), 201) + except Exception: + return make_response(jsonify({ + "message" : "wrong input format" + }), 400) + def get(self): - pass + """ + get method to retrieve list of all orders + """ + resp = order.order_list() + return make_response(jsonify({ + "message" : "ok", + "Delivery Orders" : resp + }), 200) class IndividualParcel(Resource): + """ + class for API endpoints for retrieving single order and cancelling particular order + """ + def get(self, id): + """ + get method to retrieve order by id + """ + + single = order.validate_ID(id) + if single: + individ_order = order.retrieve_single_order(id) + return make_response(jsonify({ + "message" : "Ok", + "order" : individ_order + }), 200) + else: + response = { + "message" : "Invalid id" + } + return make_response(jsonify(response), 400) +class UserOrders(Resource): + """ + class for endpoint that restrieves all the orders made by a specific user + """ def get(self, id): - pass + if order.validate_ID(id): + user_orders = order.get_orders_by_user(id) + return make_response(jsonify({ + "message" : "Ok", + "orders by single user" : user_orders + }), 200) + else: + response = { + "message" : "Invalid user id" + } + return make_response(jsonify(response), 400) +class CancelParcel(Resource): + """ + class for endpoint to cancel parcel order + """ def put(self, id): - pass \ No newline at end of file + """ + PUT request to update parcel status to 'cancelled' + """ + try: + check_id = order.validate_ID(id) + if not check_id: + raise Exception + else: + cancel_parcel = order.cancel_order(check_id) + return make_response(jsonify({ + "message" : "order is cancelled", + "cancelled order" : cancel_parcel + }), 201) + except Exception: + return make_response(jsonify({ + "message" : "Cancel failed. no order by that id" + }), 400) \ No newline at end of file diff --git a/app/auth/__init__.py b/app/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/auth/v1/__init__.py b/app/auth/v1/__init__.py new file mode 100644 index 0000000..28921d7 --- /dev/null +++ b/app/auth/v1/__init__.py @@ -0,0 +1,10 @@ +from flask import Blueprint +from flask_restful import Api +from .views import Registration, SignIn + +auth = Blueprint('authV1', __name__, url_prefix="/auth/v1") + +api = Api(auth) + +api.add_resource(Registration, '/register') +api.add_resource(SignIn, '/login') diff --git a/app/auth/v1/models.py b/app/auth/v1/models.py new file mode 100644 index 0000000..fb4ff66 --- /dev/null +++ b/app/auth/v1/models.py @@ -0,0 +1,95 @@ +from datetime import datetime, timedelta +from flask import current_app +import jwt +from werkzeug.security import generate_password_hash, check_password_hash + +class MockDb(): + """ + class for a data structure database + """ + def __init__(self): + self.users = {} + self.orders = {} + self.user_no = 0 + self.entry_no = 0 + def drop(self): + self.__init__() + +db = MockDb() + +class Parent(): + """ + user class will inherit this class + """ + def update(self, data): + # Validate the contents before passing to mock database + for key in data: + setattr(self, key, data[key]) + setattr(self, 'last_updated', datetime.utcnow().isoformat()) + return self.lookup() + +class User(Parent): + """ + class to register user and generate tokens + """ + def __init__(self, email, password): + self.email = email + self.password = generate_password_hash(password) + self.id = None + self.created_at = datetime.utcnow().isoformat() + self.last_updated = datetime.utcnow().isoformat() + + def add_user(self): + """ + method to save a user's registration details + """ + setattr(self, 'id', db.user_no + 1) + db.users.update({self.id: self}) + db.user_no += 1 + db.orders.update({self.id: {}}) + return self.lookup() + + def validate_password(self, password): + """ + method to validate user password + """ + if check_password_hash(self.password, password): + return True + return False + + def lookup(self): + """ + method to jsonify object that represents user + """ + keys = ['email', 'id'] + return {key: getattr(self, key) for key in keys} + + def generate_token(self, userID): + """ + method that generates token during each login + """ + try: + payload = { + 'exp' : datetime.utcnow()+timedelta(minutes=5), + 'iat' : datetime.utcnow(), + 'id' : userID + } + token = jwt.encode( + payload, + current_app.config.get('SECRET_KEY'), + algorithm='HS256' + ) + return token + except Exception as err: + return str(err) + + @classmethod + def get_user_by_email(cls, email): + """ + method for getting a user by email + """ + for user_id in db.users: + user = db.users.get(user_id) + if user.email == email: + return user + return None \ No newline at end of file diff --git a/app/auth/v1/views.py b/app/auth/v1/views.py new file mode 100644 index 0000000..37afd45 --- /dev/null +++ b/app/auth/v1/views.py @@ -0,0 +1,70 @@ +from flask_restful import Resource +from flask import make_response,jsonify, request +from .models import User +from ...utilities.validation_functions import check_for_space, check_email_format + +class Registration(Resource): + """ + class that handles registration of new user + """ + def post(self): + data = request.get_json() + email = data['email'] + password = data['password'] + + if check_for_space(email): + if check_email_format(email): + if not User.get_user_by_email(email): + new_user = User(email=email, password=password) + new_user.add_user() + + return make_response(jsonify({ + 'message' : 'you have successfully registered an account' + }), 201) + else: + response = { + 'message': 'Account with provided email exists. please login' + } + + return make_response(jsonify(response), 409) + else: + response = { + 'message' : 'Invalid email format' + } + return make_response(jsonify(response), 400) + else: + response = { + 'message' : 'Whitespaces are invalid inputs' + } + return make_response(jsonify(response), 400) + + +class SignIn(Resource): + """ + class that handles logging into user accounts and token generation + """ + def post(self): + data = request.get_json() + email = data['email'] + password = data['password'] + + try: + user = User.get_user_by_email(email) + user_id = user.id + if user and user.validate_password(password): + auth_token = user.generate_token(user_id) + if auth_token: + response = { + 'message' : 'Successfully logged in' + } + return make_response(jsonify(response), 200) + else: + response = { + 'message' : 'Invalid password, please enter it again' + } + return make_response(jsonify(response), 401) + except Exception: + response = { + 'message' : 'wrong email format, please enter email again' + } + return make_response(jsonify(response), 400) \ No newline at end of file diff --git a/app/tests/__init__.py b/app/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/tests/v1/__init__.py b/app/tests/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/tests/v1/test_edgecases.py b/app/tests/v1/test_edgecases.py new file mode 100644 index 0000000..5132189 --- /dev/null +++ b/app/tests/v1/test_edgecases.py @@ -0,0 +1,74 @@ +from ... import create_app +import unittest +import json + +class TestEdgeCases(unittest.TestCase): + """ + class for testing invalid input data and + """ + def setUp(self): + """ + Initialize app and define test variables + """ + create_app().testing = True + self.app = create_app().test_client() + self.dummy = { + "item" : " ", + "pickup" : "muranga", + "dest" : "house", + "pricing": 250, + "user_id" : "12" + } + self.blank_email = { + "email" : " ", + "password" : "ghfgfg" + } + self.invalid_pattern = { + "email" : "house", + "password" : "xgss" + } + self.invalid_id = { + "item" : "seven ballons", + "pickup" : "Biashara street", + "dest" : "Kikuyu town", + "pricing": 250, + "user_id" : "12" + } + + def test_empty_strings_in_POST_create_order(self): + """ + Test whether API can create a new delivery order via POSt request + """ + response = self.app.post('/api/v1/parcels', data=json.dumps(self.dummy), content_type='application/json') + result = json.loads(response.data) + self.assertEqual(response.status_code, 400) + self.assertIn('wrong input format', str(result)) + + def test_whitespces_in_email_field(self): + response2 = self.app.post('/auth/v1/register', data=json.dumps(self.blank_email), content_type='application/json') + self.assertEqual(response2.status_code, 400) + res = json.loads(response2.data) + self.assertEqual("{'message': 'Whitespaces are invalid inputs'}", str(res)) + + def test_wrong_email_pattern(self): + response3 = self.app.post('/auth/v1/register', data=json.dumps(self.invalid_pattern), content_type='application/json') + self.assertEqual(response3.status_code, 400) + res = json.loads(response3.data) + self.assertEqual("{'message': 'Invalid email format'}", str(res)) + + def test_invalid_parcel_id(self): + response = self.app.post('/api/v1/parcels', data=json.dumps(self.invalid_id), content_type='application/json') + self.assertEqual(response.status_code, 201) + resul = self.app.get('/api/v1/parcels/30') + self.assertEqual(resul.status_code, 400) + # self.assertIn('b\'{"message":"Invalid id"}\\n\'', str(resul.data)) + + def test_invalidID_cancel_order(self): + response = self.app.post('/api/v1/parcels', data=json.dumps(self.invalid_id), content_type='application/json') + self.assertEqual(response.status_code, 201) + result = self.app.put('/api/v1/parcels/30/cancel') + self.assertEqual(result.status_code, 400) + # self.assertIn('{"message": "Cancel failed. no order by that id"}', str(result.data)) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/app/tests/v1/test_login.py b/app/tests/v1/test_login.py new file mode 100644 index 0000000..0155aad --- /dev/null +++ b/app/tests/v1/test_login.py @@ -0,0 +1,41 @@ +from ... import create_app +import unittest +import json + +class LoginTestCase(unittest.TestCase): + """ + test class for the registration endpoint + """ + def setUp(self): + create_app().testing = True + self.app = create_app().test_client() + self.mock_data = { + 'email' : 'test@chocoly.com', + 'password' : 'balerion' + } + self.not_user_data = { + 'email' : 'not_user@chocoly.com', + 'password' : 'silmarillion' + } + + def test_user_signin(self): + #test if a registered user can log in + res = self.app.post('/auth/v1/register', data=json.dumps(self.mock_data), content_type='application/json') + self.assertEqual(res.status_code, 201) + signin_res = self.app.post('/auth/v1/login', data=json.dumps(self.mock_data), content_type='application/json') + result = json.loads(signin_res.data) + self.assertIn('Successfully logged in', str(result)) + self.assertEqual(signin_res.status_code, 200) + self.assertTrue(result) + + def test_non_registered_user(self): + #test that unregistered user cannot log in + res = self.app.post('/auth/v1/login', data=json.dumps(self.mock_data), content_type='application/json') + result = json.loads(res.data) + self.assertIn("{'message': 'wrong email format, please enter email again'}", str(result)) + self.assertEqual(res.status_code, 400) + + +if __name__ == "__main__": + unittest.main() + \ No newline at end of file diff --git a/app/tests/v1/test_parcels.py b/app/tests/v1/test_parcels.py new file mode 100644 index 0000000..c2048c1 --- /dev/null +++ b/app/tests/v1/test_parcels.py @@ -0,0 +1,76 @@ +from ... import create_app +import unittest +import json + +class TestPracelCreation(unittest.TestCase): + """ + class for Parcels test case + """ + def setUp(self): + """ + Initialize app and define test variables + """ + create_app().testing = True + self.app = create_app().test_client() + self.data = { + "item" : "seven ballons", + "pickup" : "Biashara street", + "dest" : "Kikuyu town", + "pricing": 250, + "user_id" : "12" + } + + def test_POST_create_delivery_order(self): + """ + Test whether API can create a new delivery order via POSt request + """ + response = self.app.post('/api/v1/parcels', data=json.dumps(self.data), content_type='application/json') + result = json.loads(response.data) + self.assertEqual(response.status_code, 201) + self.assertIn('delivery order created', str(result)) + + def test_GET_delivery_orders_list(self): + """ + Test if API can retrieve a list of delivery orders + """ + response = self.app.post('/api/v1/parcels', data=json.dumps(self.data), content_type='application/json') + self.assertEqual(response.status_code, 201) + response = self.app.get('/api/v1/parcels', content_type='application/json') + self.assertEqual(response.status_code, 200) + result = json.loads(response.data) + self.assertIn('seven ballons', str(result)) + + def test_GET_single_delivery_order(self): + """ + Test if API can retrieve a single delivery order by its id + """ + response = self.app.post('/api/v1/parcels', data=json.dumps(self.data), content_type='application/json') + self.assertEqual(response.status_code, 201) + result = self.app.get('/api/v1/parcels/1') + self.assertEqual(result.status_code, 200) + self.assertIn('seven ballons', str(result.data)) + + def test_GET_orders_by_single_user(self): + """ + Test if API can retrieve orders made by a spefic user + """ + response = self.app.post('/api/v1/parcels', data=json.dumps(self.data), content_type='application/json') + self.assertEqual(response.status_code, 201) + res = self.app.get('/api/v1/user/12/parcels') + self.assertEqual(res.status_code, 200) + self.assertIn('orders by single user', str(res.data)) + + def test_PUT_cancel_delivery_order(self): + """ + Test if API can cancel order by changing order status + """ + response = self.app.post('/api/v1/parcels', data=json.dumps(self.data), content_type='application/json') + self.assertEqual(response.status_code, 201) + result = self.app.put('/api/v1/parcels/1/cancel') + self.assertEqual(result.status_code, 201) + self.assertIn('order is cancelled', str(result.data)) + +# make the tests you have written executable + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/app/tests/v1/test_register.py b/app/tests/v1/test_register.py new file mode 100644 index 0000000..b3d08d2 --- /dev/null +++ b/app/tests/v1/test_register.py @@ -0,0 +1,37 @@ +from ... import create_app +import unittest +import json + +class AuthTestCase(unittest.TestCase): + """ + test class for the registration endpoint + """ + def setUp(self): + create_app().testing = True + self.app = create_app().test_client() + self.mock_data = { + 'email' : 'test@hotmail.com', + 'password' : 'holy_water' + } + self.mock_data2 = { + 'email' : 'qarth@hotmail.com', + 'password' : 'jade_sea' + } + + + def test_signup(self): + response2 = self.app.post('/auth/v1/register', data=json.dumps(self.mock_data), content_type='application/json') + res = json.loads(response2.data) + self.assertEqual("{'message': 'you have successfully registered an account'}", str(res)) + + + def test_if_registered(self): + response1 = self.app.post('/auth/v1/register', data=json.dumps(self.mock_data2), content_type='application/json') + self.assertEqual(response1.status_code, 201) + duplicate_signup = self.app.post('/auth/v1/register', data=json.dumps(self.mock_data2), content_type='application/json') + self.assertEqual(duplicate_signup.status_code, 409) + res = json.loads(duplicate_signup.data) + self.assertIn('Account with provided email exists. please login', str(res)) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/app/utilities/JWT_token.py b/app/utilities/JWT_token.py new file mode 100644 index 0000000..61c8a35 --- /dev/null +++ b/app/utilities/JWT_token.py @@ -0,0 +1,18 @@ +""" +from flask import current_app +import jwt + + +def decode_token(token): + + try: + #attempt to decode token using SECRET_KEY variable + payload = jwt.decode(token, current_app.config.get('SECRET_KEY')) + return payload['id'] + except jwt.ExpiredSignatureError: + # expired token returns an error string + return "Token expired. please login again to generate fresh token" + except jwt.InvalidTokenError: + #the token is not valid, throw error + return "Unworthy token. Please login to get fresh authorization" +""" \ No newline at end of file diff --git a/app/utilities/__init__.py b/app/utilities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/utilities/validation_functions.py b/app/utilities/validation_functions.py new file mode 100644 index 0000000..e5d4e5e --- /dev/null +++ b/app/utilities/validation_functions.py @@ -0,0 +1,15 @@ +import re + +def check_for_space(varib): + output = varib.strip(" ") + if output: + return True + else: + return False + +def check_email_format(varib): + match = re.search(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9]+\.[a-zA-Z0-9.]*\.*[com|org|edu]{3}$)",varib) + if match: + return True + else: + return False diff --git a/coverage.xml b/coverage.xml new file mode 100644 index 0000000..3113ece --- /dev/null +++ b/coverage.xml @@ -0,0 +1,415 @@ + + + + + + /home/ipaullly/dev/parcels/sendIT/app + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/instance/__init__.py b/instance/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/instance/__init__.pyc b/instance/__init__.pyc new file mode 100644 index 0000000..92fb1da Binary files /dev/null and b/instance/__init__.pyc differ diff --git a/instance/config.py b/instance/config.py new file mode 100644 index 0000000..d370e04 --- /dev/null +++ b/instance/config.py @@ -0,0 +1,7 @@ +class DevConfig: + """ + configuration class for development variables + """ + DEBUG = True + SECRET_KEY = '12asgRx-67fdfsh-bsghsdj-bdjd7678bd' + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e69de29..57df97a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1,37 @@ +aniso8601==4.0.1 +astroid==2.0.4 +atomicwrites==1.2.1 +attrs==18.2.0 +certifi==2018.10.15 +chardet==3.0.4 +Click==7.0 +coverage==4.5.1 +coveralls==1.5.1 +docopt==0.6.2 +Flask==1.0.2 +Flask-RESTful==0.3.6 +gunicorn==19.9.0 +idna==2.7 +isort==4.3.4 +itsdangerous==1.1.0 +Jinja2==2.10 +lazy-object-proxy==1.3.1 +MarkupSafe==1.0 +mccabe==0.6.1 +more-itertools==4.3.0 +pluggy==0.8.0 +psycopg2-binary==2.7.6 +py==1.7.0 +PyJWT==1.6.4 +pylint==2.1.1 +pytest==3.10.0 +pytest-cov==2.6.0 +python-coveralls==2.9.1 +pytz==2018.7 +PyYAML==3.13 +requests==2.20.1 +six==1.11.0 +typed-ast==1.1.0 +urllib3==1.24.1 +Werkzeug==0.14.1 +wrapt==1.10.11 diff --git a/runtime.txt b/runtime.txt new file mode 100644 index 0000000..486fcce --- /dev/null +++ b/runtime.txt @@ -0,0 +1 @@ +python-3.6.5