diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b17bfec --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.pyc +*__pycache__ +/env +.coverage +.env diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2fe84b4 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: python +python: +- "3.6" +cache: pip +install: +- pip install -r requirements.txt +before_script: + - export SECRET_KEY="secret" +script: +- pytest +- pytest --cov=app +after_success: +- coveralls diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..d7de1ab --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,15 @@ +from flask import Flask, Blueprint +from flask_restful import Api +from instance.config import app_config +from .api.v1 import myblue + +def create_app(config_name): + app = Flask(__name__, instance_relative_config=True) + app.config.from_object(app_config["development"]) + app.config.from_pyfile('config.py') + app.register_blueprint(myblue) + + app.config["TESTING"] = True + + + return app diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py new file mode 100644 index 0000000..4286de6 --- /dev/null +++ b/app/api/v1/__init__.py @@ -0,0 +1,10 @@ +from flask import Flask, Blueprint +from flask_restful import Api, Resource +from .views import SignUp, Login, Product, Sale +myblue = Blueprint("api", __name__, url_prefix="/storemanager/api/v1") + +api = Api(myblue) +api.add_resource(SignUp, '/auth/signup') +api.add_resource(Login, '/auth/login') +api.add_resource(Product, '/products') +api.add_resource(Sale, '/sales') diff --git a/app/api/v1/models.py b/app/api/v1/models.py new file mode 100644 index 0000000..0eae331 --- /dev/null +++ b/app/api/v1/models.py @@ -0,0 +1,50 @@ +users = [] +products = [] +sales = [] + +class UserAuth(): + def __init__(self, name, email, password, role): + self.name = username + self.email = email + self.password = password + self.role = role + + def save_user(self): + id = len(users) + 1 + user = { + 'id' : self.id, + 'name' : self.name, + 'email': self.email, + 'password' : self.password, + 'role' : self.role + } + users.append(user) + +class PostProduct(): + def __init__(self, id, name, category, desc, currstock, minstock, price): + self.id = id + self.name = name + self.category = category + self.desc = desc + self.currstock = currstock + self.minstock = minstock + self.price = price + + def add_product(self): + payload = { + 'id' : self.id, + 'name': self.name, + 'category' : self.category, + 'desc': self.desc, + 'currstock' : self.currstock, + 'minstock' : self.minstock, + 'price': self.price + } + + products.append(payload) + print(products) + +def collapse(): + users = [] + products = [] + sales = [] diff --git a/app/api/v1/utils.py b/app/api/v1/utils.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/v1/views.py b/app/api/v1/views.py new file mode 100644 index 0000000..2f5c2d3 --- /dev/null +++ b/app/api/v1/views.py @@ -0,0 +1,167 @@ +from flask import jsonify, make_response, request +from flask_restful import Resource +from functools import wraps +from instance.config import Config +import datetime +import jwt +import json + +from .models import * + +def token_required(func): + @wraps(func) + def decorated(*args, **kwargs): + token = None + if 'x-access-token' in request.headers: + token = request.headers['x-access-token'] + if not token: + return make_response(jsonify({ + "Message": "the access token is missing, Login"}, 401)) + try: + data = jwt.decode(token, Config.SECRET_KEY) + for user in users: + if user['email'] == data['email']: + current_user = user + + except: + + print(Config.SECRET_KEY) + return make_response(jsonify({ + "Message": "This token is invalid" + }, 403)) + + return func(current_user, *args, **kwargs) + return decorated + +class Product(Resource): + @token_required + def post(current_user, self): + if current_user["role"] != "admin": + return make_response(jsonify({ + "Message": "you have no clearance for this endpoint"} + ), 403) + id = len(products) + 1 + data = request.get_json() + name = data["name"] + category = data["category"] + desc = data["desc"] + currstock = data["currstock"] + minstock = data["minstock"] + price = data["price"] + + prod = PostProduct(id, name, category, desc, currstock, minstock, price) + prod.add_product() + return make_response(jsonify({ + "Status": "ok", + "Message": "Product posted successfully", + "Products": products + } + ), 201) + +class Sale(Resource): + def get(self): + return make_response(jsonify({ + "Status": "ok", + "Message": "All products fetched successfully", + "sales": sales + } + )) + + @token_required + def post(current_user, self): + total = 0 + data = request.get_json() + print(data) + if current_user["role"] != "attendant": + return make_response(jsonify({ + "Message": "You must be an attendant to access this endpoint" + } + )) + id = data['id'] + for product in products: + if product["currstock"] > 0: + if product["id"] == id: + sale = { + "saleid": len(sales) + 1, + "product": product + } + product["currstock"] = product["currstock"] - 1 + sales.append(sale) + for sale in sales: + if product["id"] in sale.values(): + total = total + int(product["price"]) + return make_response(jsonify({ + "Status": "ok", + "Message": "sale is successfull", + "Sales": sales, + "total cost": total + }), 201) + else: + return make_response(jsonify({ + "Status": "non-existent", + "Message": "item not found" + }), 404) + else: + return make_response(jsonify({ + "Status": "not found", + "Message": "items have run out" + + }), 404) +# class Sale(Resource): +# def get(self): +# return make_response(jsonify({ +# "Status": "ok", +# "Message": "All products fetched successfully", +# "sales": sales +# } +# )) + + +class SignUp(Resource): + def post(self): + data = request.get_json() + id = len(users) + 1 + name = data["name"] + email = data["email"] + password = data["password"] + role = data["role"] + + user = { + "id": id, + "name": name, + "email": email, + "password": password, + "role": role + } + users.append(user) + return make_response(jsonify({ + "Status": "ok", + "Message": "user successfully created", + "user": users + } + ), 201) + +class Login(Resource): + def post(self): + data = request.get_json() + if not data: + return make_response(jsonify({ + "Message": "Ensure you have inserted your credentials" + } + ), 401) + email = data["email"] + password = data["password"] + + for user in users: + if email == user["email"] and password == user["password"]: + token = jwt.encode({ + "email": email, + "exp": datetime.datetime.utcnow() + datetime.timedelta + (minutes=5) + }, Config.SECRET_KEY) + return make_response(jsonify({ + "token": token.decode("UTF-8")}), 200) + return make_response(jsonify({ + "Message": "Login failed, wrong entries" + } + ), 401) 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_endpoints.py b/app/tests/v1/test_endpoints.py new file mode 100644 index 0000000..f0678a2 --- /dev/null +++ b/app/tests/v1/test_endpoints.py @@ -0,0 +1,123 @@ +import unittest +import json +from app import create_app +from instance.config import app_config +from app.api.v1.models import collapse + + +class TestEndpoints(unittest.TestCase): + """docstring for setting up testEndpoints.""" + def setUp(self): + self.app = create_app(app_config['testing']) + self.test_client = self.app.test_client() + self.app_context = self.app.app_context() + self.user_admin_details = json.dumps({ + "name": "kevin", + "email": "kevin@email.com", + "password": "kevin", + "role": "admin" + }) + admin_signup = self.test_client.post( + "/storemanager/api/v1/auth/signup", + data=self.user_admin_details, headers={ + 'content-type': 'application/json'}) + self.user_attendant_details = json.dumps({ + "name": "brian", + "email": "brian@email.com", + "password": "brian", + "role": "attendant" + }) + attendant_signup = self.test_client.post("/storemanager/api/v1/auth/signup", + data=self.user_attendant_details, + headers={ + 'content-type': 'application/json' + }) + self.login_admin = json.dumps({ + "email": "kevin@email.com", + "password": "kevin" + }) + admin_login = self.test_client.post("/storemanager/api/v1/auth/login", + data=self.login_admin, headers={ + 'content-type': 'application/json' + }) + self.token_for_admin = json.loads(admin_login.data.decode())["token"] + self.login_attendant = json.dumps({ + "email": "brian@email.com", + "password": "brian" + }) + attendant_login = self.test_client.post("/storemanager/api/v1/auth/login", + data=self.login_attendant, + headers={ + 'content-type': 'application/json' + }) + self.token_for_attendant = json.loads(attendant_login.data.decode())["token"] + + def tearDown(self): + """removes all the context and dicts""" + collapse() + # self.app_context.pop() + def test_signup(self): + response = self.test_client.post("/storemanager/api/v1/auth/signup", + data=self.user_admin_details, + content_type='application/json') + self.assertEqual(response.status_code, 201) + def test_empty_login(self): + data = json.dumps( + { + "email": "", + "password": "" + } + ) + response = self.test_client.post("storemanager/api/v1/auth/login", + data=data, + content_type='application/json') + self.assertEqual(response.status_code, 401) + + def test_wrong_login(self): + data = json.dumps({ + "email": "blah@email.com", + "password": "blahblah" + }) + response = self.test_client.post("storemanager/api/v1/auth/login", + data=data, + content_type='application/json') + + self.assertEqual(response.status_code, 401) + + def test_login_granted(self): + + response = self.test_client.post("/storemanager/api/v1/auth/login", + data=self.login_admin, + headers={ + 'content-type': 'application/json' + }) + self.assertEqual(response.status_code, 200) + + def test_post_product(self): + response = self.test_client.post("storemanager/api/v1/products", + data=json.dumps({ + 'name': 'minji', + 'category': 'food', + 'desc': 'great food', + 'currstock': 200, + 'minstock': 20, + 'price': 30 + }), + headers={ + 'content-type': 'application/json', + "x-access-token": self.token_for_admin + }) + self.assertEqual(response.status_code, 201) + def test_post_sale(self): + data = json.dumps({ + "id": 1 + }) + response = self.test_client.post("storemanager/api/v1/sales", + data=data, headers={ + 'content-type': 'application/json', + 'x-access-token': self.token_for_attendant + }) + self.assertEqual(response.status_code, 201) + def test_get_all_sales(self): + response = self.test_client.get("storemanager/api/v1/sales") + self.assertEqual(response.status_code, 200) diff --git a/instance/__init__.py b/instance/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/instance/config.py b/instance/config.py new file mode 100644 index 0000000..3f20a36 --- /dev/null +++ b/instance/config.py @@ -0,0 +1,20 @@ +class Config(): + + debug = False + SECRET_KEY = "secretkey" + +class Develop(Config): + """Configuration for the development enviroment""" + debug = True + + +class Testing(Config): + """Configuration for the testing enviroment""" + WTF_CSRF_ENABLED = False + debug = True + + +app_config={ +"development": Develop, +"testing": Testing +} diff --git a/procfile b/procfile new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..57ffa2c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,22 @@ +aniso8601==3.0.2 +atomicwrites==1.2.1 +attrs==18.2.0 +Blueprints==2.3.0.2 +Click==7.0 +coverage==4.5.1 +Flask==1.0.2 +Flask-RESTful==0.3.6 +funcsigs==1.0.2 +itsdangerous==0.24 +Jinja2==2.10 +MarkupSafe==1.0 +more-itertools==4.3.0 +pathlib2==2.3.2 +pluggy==0.8.0 +py==1.7.0 +PyJWT==1.6.4 +pytest==3.9.1 +pytz==2018.5 +scandir==1.9.0 +six==1.11.0 +Werkzeug==0.14.1 diff --git a/run.py b/run.py new file mode 100644 index 0000000..24df9c5 --- /dev/null +++ b/run.py @@ -0,0 +1,6 @@ +from app import create_app + +app = create_app("development") + +if __name__ == '__main__': + app.run()