From 1d9e5593b23cc70133a342edaac91d65dbcfbec2 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Sun, 14 Oct 2018 14:48:26 +0300 Subject: [PATCH 001/100] [Chore #161203416] Create the application's project folder --- Procfile | 0 app/__init__.py | 0 app/api/__init__.py | 0 app/api/v1/__init__.py | 0 requirements.txt | 0 run.py | 0 travis.yml | 0 7 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 Procfile create mode 100644 app/__init__.py create mode 100644 app/api/__init__.py create mode 100644 app/api/v1/__init__.py create mode 100644 requirements.txt create mode 100644 run.py create mode 100644 travis.yml diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..e69de29 diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 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..e69de29 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/run.py b/run.py new file mode 100644 index 0000000..e69de29 diff --git a/travis.yml b/travis.yml new file mode 100644 index 0000000..e69de29 From 02bdb3b2bef2472e83afb3b71d68e425b8bf325e Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Mon, 15 Oct 2018 09:37:37 +0300 Subject: [PATCH 002/100] [Chore #161209925] Add requirements.txt file --- requirements.txt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/requirements.txt b/requirements.txt index e69de29..b467780 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1,18 @@ +aniso8601==3.0.2 +astroid==2.0.4 +Click==7.0 +Flask==1.0.2 +Flask-RESTful==0.3.6 +isort==4.3.4 +itsdangerous==0.24 +Jinja2==2.10 +lazy-object-proxy==1.3.1 +MarkupSafe==1.0 +mccabe==0.6.1 +pylint==2.1.1 +python-dotenv==0.9.1 +pytz==2018.5 +six==1.11.0 +typed-ast==1.1.0 +Werkzeug==0.14.1 +wrapt==1.10.11 From 28dd3361a3ccce28cb03f1de07b0086262c82b00 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Mon, 15 Oct 2018 09:46:24 +0300 Subject: [PATCH 003/100] [Chore #161209988] Setup Application factory to create the application --- app/__init__.py | 13 +++++++++++++ run.py | 9 +++++++++ 2 files changed, 22 insertions(+) diff --git a/app/__init__.py b/app/__init__.py index e69de29..714882f 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -0,0 +1,13 @@ +from flask import Flask +from instance.config import config + + +def create_app(config_name): + app = Flask(__name__) + app.config.from_object(config[config_name]) + + + from .api.v1 import v1_blueprint as v1_blueprint + app.register_blueprint(v1_blueprint, url_prefix='/api/v1') + + return app diff --git a/run.py b/run.py index e69de29..02ff611 100644 --- a/run.py +++ b/run.py @@ -0,0 +1,9 @@ +import os +from app import create_app + + +app = create_app(os.getenv('APP_SETTINGS') or 'default') + + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file From 0864c42d401166a8c624adc433a324b942f77338 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Mon, 15 Oct 2018 09:48:13 +0300 Subject: [PATCH 004/100] [Chore #161209988] Create a blueprint for version 1 of the application --- app/api/v1/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py index e69de29..5618f81 100644 --- a/app/api/v1/__init__.py +++ b/app/api/v1/__init__.py @@ -0,0 +1,7 @@ +from flask import Blueprint + + +v1_blueprint = Blueprint('v1_blueprint', __name__) + + +from . import views \ No newline at end of file From 5a6a25fc70fb6f65de23b131c5b0eb93be367a3b Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Mon, 15 Oct 2018 15:01:20 +0300 Subject: [PATCH 005/100] [Chore #161216309] Add travis.yml file for Travis CI --- travis.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/travis.yml b/travis.yml index e69de29..9c169d6 100644 --- a/travis.yml +++ b/travis.yml @@ -0,0 +1,17 @@ +language: python + +# python version +python: + - "3.6.6" + +# command to install dependencies +install: + - pip install -r requirements.txt + +# command to run tests +script: + - coverage run --source=app.api.v1 -m pytest app/tests/v1 && coverage report + +# Post coverage results to coverage.io +after-success: + - coveralls \ No newline at end of file From 0d6d048de3fce73410342799fb14cc435c6abb66 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Mon, 15 Oct 2018 15:13:38 +0300 Subject: [PATCH 006/100] [Chore #161209925] Add Continous Integration packages --- requirements.txt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/requirements.txt b/requirements.txt index b467780..0e3f9ff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,18 +1,32 @@ aniso8601==3.0.2 astroid==2.0.4 +atomicwrites==1.2.1 +attrs==18.2.0 +certifi==2018.8.24 +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 +idna==2.7 isort==4.3.4 itsdangerous==0.24 Jinja2==2.10 lazy-object-proxy==1.3.1 MarkupSafe==1.0 mccabe==0.6.1 +more-itertools==4.3.0 +pluggy==0.7.1 +py==1.7.0 pylint==2.1.1 +pytest==3.8.2 python-dotenv==0.9.1 pytz==2018.5 +requests==2.19.1 six==1.11.0 typed-ast==1.1.0 +urllib3==1.23 Werkzeug==0.14.1 wrapt==1.10.11 From 37f7585852d551706c3eece3f82125a7c9abb022 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Mon, 15 Oct 2018 15:18:40 +0300 Subject: [PATCH 007/100] [Chore #161201973] Create test for app creation This test ensures that the application is running as expected and that the application runs under the testing environment during tests --- app/tests/__init__.py | 0 app/tests/v1/__init__.py | 0 app/tests/v1/test_endpoints.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 app/tests/__init__.py create mode 100644 app/tests/v1/__init__.py create mode 100644 app/tests/v1/test_endpoints.py 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..d557c13 --- /dev/null +++ b/app/tests/v1/test_endpoints.py @@ -0,0 +1,28 @@ +import unittest +from flask import current_app + + +#local imports +from app import create_app + +class ConfigTestCase(unittest.TestCase): + def setUp(self): + self.app = create_app('testing') + self.app_context = self.app.app_context() + self.app_context.push() + + + def tearDown(self): + self.app_context.pop() + + + def test_app_exists(self): + self.assertFalse(current_app is None) + + + def test_app_is_testing(self): + self.assertTrue(current_app.config['TESTING']) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 0de12a2add6a70f4b4c6d65f09b6da7f07ec339c Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Mon, 15 Oct 2018 15:19:48 +0300 Subject: [PATCH 008/100] [Chore #161209988] Change environment for running the application --- run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run.py b/run.py index 02ff611..0f6f0f7 100644 --- a/run.py +++ b/run.py @@ -2,7 +2,7 @@ from app import create_app -app = create_app(os.getenv('APP_SETTINGS') or 'default') +app = create_app(os.getenv('APP_SETTINGS')) if __name__ == '__main__': From 106afa8588be5820ea2edaae2ac82815b7ad140d Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Mon, 15 Oct 2018 15:52:29 +0300 Subject: [PATCH 009/100] [Chore #161216309] Rename travis file --- travis.yml => .travis.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename travis.yml => .travis.yml (100%) diff --git a/travis.yml b/.travis.yml similarity index 100% rename from travis.yml rename to .travis.yml From 0a8647b92499d8d7e2a2bde55bbb4ec0b3fa74ed Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Mon, 15 Oct 2018 16:21:10 +0300 Subject: [PATCH 010/100] [Chore #161209988] Allow instance configurations This is to eliminate the travis error which prevents the application from building successfully --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 894a44c..c53fa05 100644 --- a/.gitignore +++ b/.gitignore @@ -57,7 +57,7 @@ local_settings.py db.sqlite3 # Flask stuff: -instance/ +#instance/ .webassets-cache # Scrapy stuff: From 866c537ebd54e7e389411c47f9acafb284054041 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Mon, 15 Oct 2018 16:24:32 +0300 Subject: [PATCH 011/100] [Chore #161216309] Remove extra spacing in .travis.yml --- .travis.yml | 2 +- instance/config.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 instance/config.py diff --git a/.travis.yml b/.travis.yml index 9c169d6..6d92675 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ install: # command to run tests script: - - coverage run --source=app.api.v1 -m pytest app/tests/v1 && coverage report + - coverage run --source=app.api.v1 -m pytest app/tests/v1 && coverage report # Post coverage results to coverage.io after-success: diff --git a/instance/config.py b/instance/config.py new file mode 100644 index 0000000..3cb456a --- /dev/null +++ b/instance/config.py @@ -0,0 +1,35 @@ +import os + + +class Config(object): + """Parent configuration class.""" + DEBUG = True + + +class Development(Config): + """Configurations for Development.""" + DEBUG = True + + +class Testing(Config): + """Configurations for Testing, with a separate test database.""" + TESTING = True + DEBUG = True + +class Staging(Config): + """Configurations for Staging.""" + DEBUG = True + + +class Production(Config): + """Configurations for Production.""" + DEBUG = False + TESTING = False + + +config = { + 'development': Development, + 'testing': Testing, + 'staging': Staging, + 'production': Production, +} \ No newline at end of file From 86bb1e3b9e41e21799dcbbaac42540f2d93811de Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Mon, 15 Oct 2018 16:42:05 +0300 Subject: [PATCH 012/100] [Chore #161203416] Add files for routes and endpoints --- app/api/v1/resources/endpoints.py | 2 ++ app/api/v1/views.py | 13 +++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 app/api/v1/resources/endpoints.py create mode 100644 app/api/v1/views.py diff --git a/app/api/v1/resources/endpoints.py b/app/api/v1/resources/endpoints.py new file mode 100644 index 0000000..de6ed60 --- /dev/null +++ b/app/api/v1/resources/endpoints.py @@ -0,0 +1,2 @@ +from flask import Flask, jsonify +from flask_restful import Resource diff --git a/app/api/v1/views.py b/app/api/v1/views.py new file mode 100644 index 0000000..0ee2351 --- /dev/null +++ b/app/api/v1/views.py @@ -0,0 +1,13 @@ +""" + Define API endpoints as routes +""" +from flask_restful import Api, Resource + + +# local imports +from . import v1_blueprint +from .resources import endpoints + + +API = Api(v1_blueprint) +API.add_resource(endpoints.add_product, '/') From 869bd5f18aa861a3aea79854dbbbf91b6217c85b Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Mon, 15 Oct 2018 17:00:05 +0300 Subject: [PATCH 013/100] [Chore #161203416] Remove unwanted lines in views.py --- app/api/v1/views.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/api/v1/views.py b/app/api/v1/views.py index 0ee2351..6fd9516 100644 --- a/app/api/v1/views.py +++ b/app/api/v1/views.py @@ -9,5 +9,4 @@ from .resources import endpoints -API = Api(v1_blueprint) -API.add_resource(endpoints.add_product, '/') +API = Api(v1_blueprint) \ No newline at end of file From ec7ae66d66a92aca4c176030f97d916b75b43d98 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Mon, 15 Oct 2018 17:08:55 +0300 Subject: [PATCH 014/100] [Chore #161216309] Move coveralls command to Script --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 6d92675..06bdbc6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ install: # command to run tests script: - coverage run --source=app.api.v1 -m pytest app/tests/v1 && coverage report + - coveralls # Post coverage results to coverage.io after-success: From bcd108c1ee5fc3156cc465078dc104775e0b9208 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Mon, 15 Oct 2018 18:04:11 +0300 Subject: [PATCH 015/100] [Chore #161216309] Add Travis CI badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 41ee15b..3683bcc 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # store-manager-api +https://travis-ci.com/calebrotich10/store-manager-api.svg?branch=develop Store Manager is a web application that helps store owners manage sales and product inventory records. This application is meant for use in a single store. This repository contains the API endpoints for the application. From 55cb4778b7096ebe27d026b79982b6a95d7c4f9a Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Mon, 15 Oct 2018 18:09:20 +0300 Subject: [PATCH 016/100] [Chore #161216309] Change badge to Markdown Change the Travis CI badge from Image URL to markdown --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3683bcc..eef64d3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ # store-manager-api -https://travis-ci.com/calebrotich10/store-manager-api.svg?branch=develop + +# Continous Integration badges +[![Build Status](https://travis-ci.com/calebrotich10/store-manager-api.svg?branch=develop)](https://travis-ci.com/calebrotich10/store-manager-api) + Store Manager is a web application that helps store owners manage sales and product inventory records. This application is meant for use in a single store. This repository contains the API endpoints for the application. From fc59405e7685e8431ee11bc34f7b88d9cc43a74c Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Mon, 15 Oct 2018 18:12:41 +0300 Subject: [PATCH 017/100] [Chore #161223919] Edit CI title --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eef64d3..bf3719a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # store-manager-api -# Continous Integration badges +## Continous Integration badges [![Build Status](https://travis-ci.com/calebrotich10/store-manager-api.svg?branch=develop)](https://travis-ci.com/calebrotich10/store-manager-api) Store Manager is a web application that helps store owners manage sales and product inventory records. This application is meant for use in a single store. This repository contains the API endpoints for the application. From 63943fa7c0f11302556ba634ff243c1b2b3c8899 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Mon, 15 Oct 2018 18:17:17 +0300 Subject: [PATCH 018/100] [Chore #161223919] Add badge for coveralls.io --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index bf3719a..81a914e 100644 --- a/README.md +++ b/README.md @@ -3,4 +3,6 @@ ## Continous Integration badges [![Build Status](https://travis-ci.com/calebrotich10/store-manager-api.svg?branch=develop)](https://travis-ci.com/calebrotich10/store-manager-api) +[![Coverage Status](https://coveralls.io/repos/github/calebrotich10/store-manager-api/badge.svg?branch=develop)](https://coveralls.io/github/calebrotich10/store-manager-api?branch=develop) + Store Manager is a web application that helps store owners manage sales and product inventory records. This application is meant for use in a single store. This repository contains the API endpoints for the application. From 2189b3ce2a0bbe3c6559efa0398290b497bc298a Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Mon, 15 Oct 2018 18:19:31 +0300 Subject: [PATCH 019/100] [Chore #161223919] Organize badges --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 81a914e..34b2dd3 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # store-manager-api ## Continous Integration badges -[![Build Status](https://travis-ci.com/calebrotich10/store-manager-api.svg?branch=develop)](https://travis-ci.com/calebrotich10/store-manager-api) +[![Build Status](https://travis-ci.com/calebrotich10/store-manager-api.svg?branch=develop)](https://travis-ci.com/calebrotich10/store-manager-api) [![Coverage Status](https://coveralls.io/repos/github/calebrotich10/store-manager-api/badge.svg?branch=develop)](https://coveralls.io/github/calebrotich10/store-manager-api?branch=develop) + -[![Coverage Status](https://coveralls.io/repos/github/calebrotich10/store-manager-api/badge.svg?branch=develop)](https://coveralls.io/github/calebrotich10/store-manager-api?branch=develop) Store Manager is a web application that helps store owners manage sales and product inventory records. This application is meant for use in a single store. This repository contains the API endpoints for the application. From 31d413665a94666491817b712fbd7dab7065344d Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Mon, 15 Oct 2018 18:36:39 +0300 Subject: [PATCH 020/100] [Chore #161223919] Add code climate badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 34b2dd3..dae3b67 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # store-manager-api ## Continous Integration badges -[![Build Status](https://travis-ci.com/calebrotich10/store-manager-api.svg?branch=develop)](https://travis-ci.com/calebrotich10/store-manager-api) [![Coverage Status](https://coveralls.io/repos/github/calebrotich10/store-manager-api/badge.svg?branch=develop)](https://coveralls.io/github/calebrotich10/store-manager-api?branch=develop) +[![Build Status](https://travis-ci.com/calebrotich10/store-manager-api.svg?branch=develop)](https://travis-ci.com/calebrotich10/store-manager-api) [![Coverage Status](https://coveralls.io/repos/github/calebrotich10/store-manager-api/badge.svg?branch=develop)](https://coveralls.io/github/calebrotich10/store-manager-api?branch=develop) [![Maintainability](https://api.codeclimate.com/v1/badges/e87820f417b8d15c3a64/maintainability)](https://codeclimate.com/github/calebrotich10/store-manager-api/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/e87820f417b8d15c3a64/test_coverage)](https://codeclimate.com/github/calebrotich10/store-manager-api/test_coverage) From 92903935e70714d58d9b6ce8b3992073ff83b528 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 16 Oct 2018 11:31:10 +0300 Subject: [PATCH 021/100] [Chore #161201973] Create a base test file The base test class file will be inherited by all the other test files --- app/tests/v1/base_test.py | 44 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 app/tests/v1/base_test.py diff --git a/app/tests/v1/base_test.py b/app/tests/v1/base_test.py new file mode 100644 index 0000000..bf912fe --- /dev/null +++ b/app/tests/v1/base_test.py @@ -0,0 +1,44 @@ +""" + Contains the base test class for the + other test classes +""" +import unittest + +# local imports +from app import create_app +from instance.config import config + + +class TestBaseClass(unittest.TestCase): + """Base test class""" + + + def setUp(self): + """Create and setup the application + + for testing purposes + """ + self.app = create_app('testing') + self.BASE_URL = 'api/v1' + self.app_context = self.app.app_context() + self.app_context.push() + self.app_test_client = self.app.test_client() + self.app.testing = True + + self.PRODUCT = { + 'product_name': 'Phone Model 1', + 'product_price': 55000, + 'category': 'Phones' + } + + + def tearDown(self): + """Destroy the application that + + is created for testing + """ + self.app_context.pop() + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 245e430b70003255cb5a8d7789cd186ba210496f Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 16 Oct 2018 11:31:31 +0300 Subject: [PATCH 022/100] [Chore #161201973] Rename test file Rename the test file from a more generalized one to more specific name --- app/tests/v1/test_app_working.py | 18 ++++++++++++++++++ app/tests/v1/test_endpoints.py | 28 ---------------------------- 2 files changed, 18 insertions(+), 28 deletions(-) create mode 100644 app/tests/v1/test_app_working.py delete mode 100644 app/tests/v1/test_endpoints.py diff --git a/app/tests/v1/test_app_working.py b/app/tests/v1/test_app_working.py new file mode 100644 index 0000000..4574c93 --- /dev/null +++ b/app/tests/v1/test_app_working.py @@ -0,0 +1,18 @@ +""" + Module contains tests for the working + of the application +""" +from flask import current_app + +#local imports +from . import base_test + +class TestConfigCase(base_test.TestBaseClass): + + + def test_app_exists(self): + self.assertFalse(current_app is None) + + + def test_app_is_testing(self): + self.assertTrue(current_app.config['TESTING']) \ No newline at end of file diff --git a/app/tests/v1/test_endpoints.py b/app/tests/v1/test_endpoints.py deleted file mode 100644 index d557c13..0000000 --- a/app/tests/v1/test_endpoints.py +++ /dev/null @@ -1,28 +0,0 @@ -import unittest -from flask import current_app - - -#local imports -from app import create_app - -class ConfigTestCase(unittest.TestCase): - def setUp(self): - self.app = create_app('testing') - self.app_context = self.app.app_context() - self.app_context.push() - - - def tearDown(self): - self.app_context.pop() - - - def test_app_exists(self): - self.assertFalse(current_app is None) - - - def test_app_is_testing(self): - self.assertTrue(current_app.config['TESTING']) - - -if __name__ == "__main__": - unittest.main() \ No newline at end of file From 225da57f84b25c23b961b7230f972312a93ee1dd Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 16 Oct 2018 11:46:20 +0300 Subject: [PATCH 023/100] [Chore #161201973] Create test for add (post) new product --- app/tests/v1/helper_functions.py | 9 ++++++++ app/tests/v1/test_admin_endpoints.py | 32 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 app/tests/v1/helper_functions.py create mode 100644 app/tests/v1/test_admin_endpoints.py diff --git a/app/tests/v1/helper_functions.py b/app/tests/v1/helper_functions.py new file mode 100644 index 0000000..cc285bc --- /dev/null +++ b/app/tests/v1/helper_functions.py @@ -0,0 +1,9 @@ +import json + +def convert_response_to_json(response): + """Helper function + + Converts the response to a json type + """ + json_response = json.loads(response.data.decode('utf-8')) + return json_response \ No newline at end of file diff --git a/app/tests/v1/test_admin_endpoints.py b/app/tests/v1/test_admin_endpoints.py new file mode 100644 index 0000000..5c94268 --- /dev/null +++ b/app/tests/v1/test_admin_endpoints.py @@ -0,0 +1,32 @@ +"""Module contains tests for admin + +specific endpoints +""" +from flask import current_app + +from . import base_test +from . import helper_functions + +class TestAdminEndpoints(base_test.TestBaseClass): + """ Class contains tests for admin specific endpoints """ + + + def test_add_new_product(self): + """Test POST /products""" + + # send a dummy data response for testing + response = self.app_test_client.post('{}/products'.format( + self.BASE_URL), json=self.PRODUCT, headers={ + 'Content-Type': 'application/json'}) + + self.assertEqual(response.status_code, 201) + self.assertEqual(helper_functions.convert_response_to_json( + response)['product']['product_name'], self.PRODUCT['product_name']) + self.assertEqual(helper_functions.convert_response_to_json( + response)['product']['product_id'], 1) + self.assertEqual(helper_functions.convert_response_to_json( + response)['product']['product_price'], 55000) + self.assertEqual(helper_functions.convert_response_to_json( + response)['product']['category'], self.PRODUCT['category']) + self.assertEqual(helper_functions.convert_response_to_json( + response)['message'], 'Product added successfully') \ No newline at end of file From 07b40cad272d9c1935da4c16d57b286b554fb3f4 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 16 Oct 2018 13:03:21 +0300 Subject: [PATCH 024/100] [Chore #161209988] Update .gitignore file --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index c53fa05..a050e84 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,6 @@ venv.bak/ # mypy .mypy_cache/ + +# VS code settings +.vscode/ From f8ab0174df7d11195b3ec8dfd95602723933960a Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 16 Oct 2018 13:28:52 +0300 Subject: [PATCH 025/100] [Feature #161201833] Create add_new_product endpoint Create a post endpoint for the admin to add a new product --- app/api/v1/resources/admin_endpoints.py | 56 ++++++++++++++++++++++++ app/api/v1/resources/helper_functions.py | 51 +++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 app/api/v1/resources/admin_endpoints.py create mode 100644 app/api/v1/resources/helper_functions.py diff --git a/app/api/v1/resources/admin_endpoints.py b/app/api/v1/resources/admin_endpoints.py new file mode 100644 index 0000000..81a7a01 --- /dev/null +++ b/app/api/v1/resources/admin_endpoints.py @@ -0,0 +1,56 @@ +from flask import Flask, jsonify, request, abort, make_response +from flask_restful import Resource + +from . import helper_functions +from app.api.v1.models import products + +class Product(Resource): + + + def post(self): + """POST /products endpoint""" + + data = request.get_json() + helper_functions.abort_if_no_json_from_request(data) + try: + product_name = data['product_name'] + product_price = data['product_price'] + category = data['category'] + except KeyError: + # If order is missing required item_name or item_price + helper_functions.abort_if_missing_required_param() + + if product_price < 1: + abort(make_response(jsonify( + message="Bad request. Price of the product should be a positive integer above 0." + ), 400)) + + if not isinstance(product_name, str): + abort(make_response(jsonify( + message="Bad request. Product name should be a string" + ), 400)) + + if not isinstance(category, str): + abort(make_response(jsonify( + message="Bad request. The Category should be a string" + ), 400)) + + if products.PRODUCTS: + # If products are in the store already + try: + # Check if a product with a similar name exists + existing_product = [ + product for product in products.PRODUCTS if product['product_name'] == product_name][0] + + abort(make_response(jsonify({ + "message": "Product with a similar name already exists", + "product": existing_product}), 400)) + + except IndexError: + # If there is no product with the same name + response = helper_functions.add_new_product(product_name, product_price, category) + else: + # If there are no products in the store + response = helper_functions.add_new_product(product_name, product_price, category) + + return response diff --git a/app/api/v1/resources/helper_functions.py b/app/api/v1/resources/helper_functions.py new file mode 100644 index 0000000..1cc3bb3 --- /dev/null +++ b/app/api/v1/resources/helper_functions.py @@ -0,0 +1,51 @@ +from datetime import datetime +from flask import abort, jsonify, make_response + +from app.api.v1.models import products + +def abort_if_no_json_from_request(req_data): + """ + Helper function. + Aborts if a json could not be obtained from request data + """ + if req_data is None: + # If a json was not obtained from the request + abort(make_response(jsonify( + message="Bad request. Request data must be in json format"), 400)) + + +def abort_if_missing_required_param(): + """ + Helper function. + Aborts if request data is missing a required argument + """ + abort(make_response(jsonify( + message="Bad request. Missing required param"), 400)) + + +def add_new_product(name, price, category): + """ + Helper function + Makes a new order, and prepares the response as a json + + """ + if name and price and category: + # If all the required parameters are available + product_id = len(products.PRODUCTS) + 1 + product = { + 'product_id': product_id, + 'product_name': name, + 'product_price': price, + 'category': category, + 'date_added': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + + products.PRODUCTS.append(product) + response = jsonify({ + "message": "Product added successfully", + "product": products.PRODUCTS[-1]}) + response.status_code = 201 + else: + # if any of the required params is None + abort_if_missing_required_param() + return response From 8db3b4381483ebd8b869d7698da2d5622bebd1a1 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 16 Oct 2018 13:30:48 +0300 Subject: [PATCH 026/100] [Feature #161201833] Create route for POST /products --- app/api/v1/resources/__init__.py | 0 app/api/v1/resources/endpoints.py | 2 -- app/api/v1/views.py | 13 +++++-------- 3 files changed, 5 insertions(+), 10 deletions(-) create mode 100644 app/api/v1/resources/__init__.py delete mode 100644 app/api/v1/resources/endpoints.py diff --git a/app/api/v1/resources/__init__.py b/app/api/v1/resources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/v1/resources/endpoints.py b/app/api/v1/resources/endpoints.py deleted file mode 100644 index de6ed60..0000000 --- a/app/api/v1/resources/endpoints.py +++ /dev/null @@ -1,2 +0,0 @@ -from flask import Flask, jsonify -from flask_restful import Resource diff --git a/app/api/v1/views.py b/app/api/v1/views.py index 6fd9516..dc753c3 100644 --- a/app/api/v1/views.py +++ b/app/api/v1/views.py @@ -1,12 +1,9 @@ -""" - Define API endpoints as routes -""" -from flask_restful import Api, Resource +"""Define API endpoints as routes""" +from flask_restful import Api, Resource -# local imports from . import v1_blueprint -from .resources import endpoints - +from .resources import admin_endpoints -API = Api(v1_blueprint) \ No newline at end of file +API = Api(v1_blueprint) +API.add_resource(admin_endpoints.Product, '/products') \ No newline at end of file From 44a40223ef596f626e9e7df48981965493a01eb1 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 16 Oct 2018 13:31:48 +0300 Subject: [PATCH 027/100] [Chore #161248446] Create a products list --- app/api/v1/models/products.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 app/api/v1/models/products.py diff --git a/app/api/v1/models/products.py b/app/api/v1/models/products.py new file mode 100644 index 0000000..a1d93c3 --- /dev/null +++ b/app/api/v1/models/products.py @@ -0,0 +1 @@ +PRODUCTS = [] \ No newline at end of file From 67e6391ebf0053596f6f565b38284911f5f61b31 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 16 Oct 2018 14:06:02 +0300 Subject: [PATCH 028/100] [Feature #161201833] Refactor code Add docstrings and more descriptive function and variable names after the tests passed --- app/api/v1/models/products.py | 5 ++++ app/api/v1/resources/admin_endpoints.py | 14 ++++++++--- app/api/v1/resources/helper_functions.py | 30 +++++++++++------------- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/app/api/v1/models/products.py b/app/api/v1/models/products.py index a1d93c3..3373348 100644 --- a/app/api/v1/models/products.py +++ b/app/api/v1/models/products.py @@ -1 +1,6 @@ +"""This module contains the data + +store and data logic of the application +""" + PRODUCTS = [] \ No newline at end of file diff --git a/app/api/v1/resources/admin_endpoints.py b/app/api/v1/resources/admin_endpoints.py index 81a7a01..66afea9 100644 --- a/app/api/v1/resources/admin_endpoints.py +++ b/app/api/v1/resources/admin_endpoints.py @@ -1,3 +1,7 @@ +"""This module contains endpoints + +that are specific to the admin +""" from flask import Flask, jsonify, request, abort, make_response from flask_restful import Resource @@ -5,20 +9,24 @@ from app.api.v1.models import products class Product(Resource): + """Class contains the tests for admin + + specific endpoints + """ def post(self): """POST /products endpoint""" data = request.get_json() - helper_functions.abort_if_no_json_from_request(data) + helper_functions.no_json_in_request(data) try: product_name = data['product_name'] product_price = data['product_price'] category = data['category'] except KeyError: - # If order is missing required item_name or item_price - helper_functions.abort_if_missing_required_param() + # If product is missing required parameter + helper_functions.missing_a_required_parameter() if product_price < 1: abort(make_response(jsonify( diff --git a/app/api/v1/resources/helper_functions.py b/app/api/v1/resources/helper_functions.py index 1cc3bb3..fb96484 100644 --- a/app/api/v1/resources/helper_functions.py +++ b/app/api/v1/resources/helper_functions.py @@ -3,32 +3,30 @@ from app.api.v1.models import products -def abort_if_no_json_from_request(req_data): - """ - Helper function. - Aborts if a json could not be obtained from request data +def no_json_in_request(data): + """Aborts if the data does + + not contain a json object """ - if req_data is None: + + if data is None: # If a json was not obtained from the request abort(make_response(jsonify( message="Bad request. Request data must be in json format"), 400)) -def abort_if_missing_required_param(): - """ - Helper function. - Aborts if request data is missing a required argument +def missing_a_required_parameter(): + """Aborts if request data is missing a + + required argument """ abort(make_response(jsonify( - message="Bad request. Missing required param"), 400)) + message="Bad request. Request missing a required argument"), 400)) def add_new_product(name, price, category): - """ - Helper function - Makes a new order, and prepares the response as a json + """Creates a new product""" - """ if name and price and category: # If all the required parameters are available product_id = len(products.PRODUCTS) + 1 @@ -46,6 +44,6 @@ def add_new_product(name, price, category): "product": products.PRODUCTS[-1]}) response.status_code = 201 else: - # if any of the required params is None - abort_if_missing_required_param() + # if any of the required parameterss is missing or none + missing_a_required_parameter() return response From b3ed0062f1d7e5e42adfade47bebee73ee8f65c3 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 16 Oct 2018 17:33:48 +0300 Subject: [PATCH 029/100] [Chore #161201973] Create test for GET all products endpoint --- app/tests/v1/test_general_user_endpoints.py | 23 +++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 app/tests/v1/test_general_user_endpoints.py diff --git a/app/tests/v1/test_general_user_endpoints.py b/app/tests/v1/test_general_user_endpoints.py new file mode 100644 index 0000000..92ee003 --- /dev/null +++ b/app/tests/v1/test_general_user_endpoints.py @@ -0,0 +1,23 @@ +"""Module contains tests to endpoints that + +are general to both the admin and the normal user +""" + +from . import base_test +from . import helper_functions + +class TestGeneralUsersEndpoints(base_test.TestBaseClass): + """Class contains the general user, i.e. both admin + + and normal user, endpoints' tests + """ + + def test_retrieve_all_products(self): + """Test GET /products - when products exist""" + + response = self.app_test_client.get('{}/products'.format(self.BASE_URL)) + + self.assertEqual(response.status_code, 200) + self.assertEqual(helper_functions.convert_response_to_json( + response)['products'][0]['product_name'], self.PRODUCT['product_name']) + self.assertEqual(len(helper_functions.convert_response_to_json(response)['products']), 1) \ No newline at end of file From 90539e2b8c2afba186eaac3ff11a2bf7b5ee7467 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 16 Oct 2018 17:37:39 +0300 Subject: [PATCH 030/100] [Finishes bug #161254684] Fix class name conflict Conflict between class names in the tests folder --- app/api/v1/resources/admin_endpoints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/v1/resources/admin_endpoints.py b/app/api/v1/resources/admin_endpoints.py index 66afea9..126cbb9 100644 --- a/app/api/v1/resources/admin_endpoints.py +++ b/app/api/v1/resources/admin_endpoints.py @@ -8,7 +8,7 @@ from . import helper_functions from app.api.v1.models import products -class Product(Resource): +class AdminActs(Resource): """Class contains the tests for admin specific endpoints From f3844fe6b78df4118cf9a0153fbfb6cfcd173a05 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 16 Oct 2018 17:46:38 +0300 Subject: [PATCH 031/100] [Feature #161201851] Create fetch all products endpoint Creates a get endpoint where the admin and the store attendant can use to view all the products in the store --- .../v1/resources/general_users_endpoints.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 app/api/v1/resources/general_users_endpoints.py diff --git a/app/api/v1/resources/general_users_endpoints.py b/app/api/v1/resources/general_users_endpoints.py new file mode 100644 index 0000000..f26ef0a --- /dev/null +++ b/app/api/v1/resources/general_users_endpoints.py @@ -0,0 +1,28 @@ +"""This module contains endpoints + +for both the admin and the normal user +""" +from flask import jsonify, abort, make_response +from flask_restful import Resource + +from . import helper_functions +from app.api.v1.models import products + +class GeneralUsersActs(Resource): + """Class contains the tests for both + + the admin and the normal user endpoints + """ + + + def get(self): + """GET /products endpoint""" + + if not products.PRODUCTS: + # If no products exist in the store yet + abort(make_response( + jsonify(message="There are no products in the store yet"), 404)) + # if at least one order exists + response = jsonify({'products': products.PRODUCTS}) + response.status_code = 200 + return response \ No newline at end of file From 955d4c69b462019b65f7bc5a87997c20f613dc40 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 16 Oct 2018 17:47:36 +0300 Subject: [PATCH 032/100] [Feature #161201851] Create route for GET /products endpoint --- app/api/v1/views.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/api/v1/views.py b/app/api/v1/views.py index dc753c3..25e77b7 100644 --- a/app/api/v1/views.py +++ b/app/api/v1/views.py @@ -3,7 +3,9 @@ from flask_restful import Api, Resource from . import v1_blueprint -from .resources import admin_endpoints +from .resources import admin_endpoints, general_users_endpoints API = Api(v1_blueprint) -API.add_resource(admin_endpoints.Product, '/products') \ No newline at end of file + +API.add_resource(admin_endpoints.AdminActs, '/products') +API.add_resource(general_users_endpoints.GeneralUsersActs, '/products') \ No newline at end of file From 549377c3d687f3013b2a845fd493fe1de116b295 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 16 Oct 2018 18:54:11 +0300 Subject: [PATCH 033/100] [Chore #161216309] Add repo token to .coveralls.yml --- .coveralls.yml | 1 + .travis.yml | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 .coveralls.yml diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000..03d4c15 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1 @@ +repo-token: PN8lvkujHNkBoTPuaOhBEjt2UpIVGRCQC \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 06bdbc6..6d92675 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,6 @@ install: # command to run tests script: - coverage run --source=app.api.v1 -m pytest app/tests/v1 && coverage report - - coveralls # Post coverage results to coverage.io after-success: From 08c3a9d3f0944e902c4e31da440588da7170e6ec Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 16 Oct 2018 19:09:55 +0300 Subject: [PATCH 034/100] [Chore #161216309] Change python version --- .coveralls.yml | 1 - .travis.yml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 .coveralls.yml diff --git a/.coveralls.yml b/.coveralls.yml deleted file mode 100644 index 03d4c15..0000000 --- a/.coveralls.yml +++ /dev/null @@ -1 +0,0 @@ -repo-token: PN8lvkujHNkBoTPuaOhBEjt2UpIVGRCQC \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 6d92675..fb9acf2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: python # python version python: - - "3.6.6" + - "3.6" # command to install dependencies install: From 5b4ac1f5939fb2fa908c44b778000ddedb0a951a Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 16 Oct 2018 19:14:30 +0300 Subject: [PATCH 035/100] [Chore #161209925] Add python-coveralls and gunicorn packages --- requirements.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0e3f9ff..8b8c1c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,11 +5,12 @@ attrs==18.2.0 certifi==2018.8.24 chardet==3.0.4 Click==7.0 -coverage==4.5.1 +coverage==4.0.3 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==0.24 @@ -22,8 +23,10 @@ pluggy==0.7.1 py==1.7.0 pylint==2.1.1 pytest==3.8.2 +python-coveralls==2.9.1 python-dotenv==0.9.1 pytz==2018.5 +PyYAML==3.13 requests==2.19.1 six==1.11.0 typed-ast==1.1.0 From 1cb07a509d3fab97c5159a06a79aa7645f6f79e9 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 16 Oct 2018 19:20:34 +0300 Subject: [PATCH 036/100] [Chore #161216309] Add coveralls command under script in travis file --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index fb9acf2..0538406 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ install: # command to run tests script: - coverage run --source=app.api.v1 -m pytest app/tests/v1 && coverage report + - coveralls # Post coverage results to coverage.io after-success: From 69fe9e093dc321c88020b07cf13807a084d370ae Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Wed, 17 Oct 2018 08:38:45 +0300 Subject: [PATCH 037/100] [Chore #161216309] Configure code climate --- .travis.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0538406..9fafbf6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,7 @@ +env: + global: + - CC_TEST_REPORTER_ID=18007e474cdfcf70da58a682805aff9f88216339b0dce34454dc0bfcc988ea91 + language: python # python version @@ -8,11 +12,18 @@ python: install: - pip install -r requirements.txt +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 + # command to run tests script: - coverage run --source=app.api.v1 -m pytest app/tests/v1 && coverage report - - coveralls + +after_script: + - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT # Post coverage results to coverage.io -after-success: +after_success: - coveralls \ No newline at end of file From 5f6d9893210c6e8626d824eadbd30a254180dc4c Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Wed, 17 Oct 2018 12:44:59 +0300 Subject: [PATCH 038/100] [Chore #161201973] Create test for GET specific product endpoint --- app/tests/v1/test_general_user_endpoints.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/app/tests/v1/test_general_user_endpoints.py b/app/tests/v1/test_general_user_endpoints.py index 92ee003..ee4e53d 100644 --- a/app/tests/v1/test_general_user_endpoints.py +++ b/app/tests/v1/test_general_user_endpoints.py @@ -15,9 +15,20 @@ class TestGeneralUsersEndpoints(base_test.TestBaseClass): def test_retrieve_all_products(self): """Test GET /products - when products exist""" - response = self.app_test_client.get('{}/products'.format(self.BASE_URL)) + response = self.app_test_client.get( + '{}/products'.format(self.BASE_URL)) self.assertEqual(response.status_code, 200) self.assertEqual(helper_functions.convert_response_to_json( response)['products'][0]['product_name'], self.PRODUCT['product_name']) - self.assertEqual(len(helper_functions.convert_response_to_json(response)['products']), 1) \ No newline at end of file + self.assertEqual(len(helper_functions.convert_response_to_json(response)['products']), 1) + + def test_retrieve_specific_product(self): + """Test GET /products/id - when product exist""" + + response = self.app_test_client.get( + '{}/products/1'.format(self.BASE_URL)) + + self.assertEqual(response.status_code, 200) + self.assertEqual(helper_functions.convert_response_to_json( + response)['product_name'], self.PRODUCT['product_name']) \ No newline at end of file From 9826908de3066bb9fe4b47167186e54a0352f7ad Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 16 Oct 2018 18:54:11 +0300 Subject: [PATCH 039/100] [Chore #161216309] Add repo token to .coveralls.yml --- .coveralls.yml | 1 + .travis.yml | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 .coveralls.yml diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000..03d4c15 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1 @@ +repo-token: PN8lvkujHNkBoTPuaOhBEjt2UpIVGRCQC \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 06bdbc6..6d92675 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,6 @@ install: # command to run tests script: - coverage run --source=app.api.v1 -m pytest app/tests/v1 && coverage report - - coveralls # Post coverage results to coverage.io after-success: From 6b4e63fb0ebf7c03f76a1f009a1db494901534f9 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 16 Oct 2018 19:09:55 +0300 Subject: [PATCH 040/100] [Chore #161216309] Change python version --- .coveralls.yml | 1 - .travis.yml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 .coveralls.yml diff --git a/.coveralls.yml b/.coveralls.yml deleted file mode 100644 index 03d4c15..0000000 --- a/.coveralls.yml +++ /dev/null @@ -1 +0,0 @@ -repo-token: PN8lvkujHNkBoTPuaOhBEjt2UpIVGRCQC \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 6d92675..fb9acf2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: python # python version python: - - "3.6.6" + - "3.6" # command to install dependencies install: From 261a32eb273c88c403225b142b2627a732f6afa0 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 16 Oct 2018 19:14:30 +0300 Subject: [PATCH 041/100] [Chore #161209925] Add python-coveralls and gunicorn packages --- requirements.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0e3f9ff..8b8c1c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,11 +5,12 @@ attrs==18.2.0 certifi==2018.8.24 chardet==3.0.4 Click==7.0 -coverage==4.5.1 +coverage==4.0.3 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==0.24 @@ -22,8 +23,10 @@ pluggy==0.7.1 py==1.7.0 pylint==2.1.1 pytest==3.8.2 +python-coveralls==2.9.1 python-dotenv==0.9.1 pytz==2018.5 +PyYAML==3.13 requests==2.19.1 six==1.11.0 typed-ast==1.1.0 From d71641ba06522472669668c9398acefe01fcf64c Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 16 Oct 2018 19:20:34 +0300 Subject: [PATCH 042/100] [Chore #161216309] Add coveralls command under script in travis file --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index fb9acf2..0538406 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ install: # command to run tests script: - coverage run --source=app.api.v1 -m pytest app/tests/v1 && coverage report + - coveralls # Post coverage results to coverage.io after-success: From b2b58a4c13a6f8cf9e8e2dac8f2699e9184205bb Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Wed, 17 Oct 2018 08:38:45 +0300 Subject: [PATCH 043/100] [Chore #161216309] Configure code climate --- .travis.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0538406..9fafbf6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,7 @@ +env: + global: + - CC_TEST_REPORTER_ID=18007e474cdfcf70da58a682805aff9f88216339b0dce34454dc0bfcc988ea91 + language: python # python version @@ -8,11 +12,18 @@ python: install: - pip install -r requirements.txt +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 + # command to run tests script: - coverage run --source=app.api.v1 -m pytest app/tests/v1 && coverage report - - coveralls + +after_script: + - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT # Post coverage results to coverage.io -after-success: +after_success: - coveralls \ No newline at end of file From 165140f24d25701dc6c6e0dcf52f5dda218fc293 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Wed, 17 Oct 2018 12:50:10 +0300 Subject: [PATCH 044/100] [Feature #161201912] Create fetch specific product endpoint Creates an endpoint that allows the admin and the store attendant to fetch the details of a specific product --- .../v1/resources/general_users_endpoints.py | 14 +++++++++- app/api/v1/resources/helper_functions.py | 26 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/app/api/v1/resources/general_users_endpoints.py b/app/api/v1/resources/general_users_endpoints.py index f26ef0a..f9402cb 100644 --- a/app/api/v1/resources/general_users_endpoints.py +++ b/app/api/v1/resources/general_users_endpoints.py @@ -22,7 +22,19 @@ def get(self): # If no products exist in the store yet abort(make_response( jsonify(message="There are no products in the store yet"), 404)) - # if at least one order exists + # if at least one product exists response = jsonify({'products': products.PRODUCTS}) response.status_code = 200 + return response + +class SpecificProduct(Resource): + + def get(self, product_id): + """GET /products/""" + + + product = helper_functions.retrieve_specific_product(product_id) + response = jsonify(product) + response.status_code = 200 + return response \ No newline at end of file diff --git a/app/api/v1/resources/helper_functions.py b/app/api/v1/resources/helper_functions.py index fb96484..d8d17a8 100644 --- a/app/api/v1/resources/helper_functions.py +++ b/app/api/v1/resources/helper_functions.py @@ -47,3 +47,29 @@ def add_new_product(name, price, category): # if any of the required parameterss is missing or none missing_a_required_parameter() return response + + + +def retrieve_specific_product(product_id): + """ + Helper method to fetch product from list of products, + given valid id + """ + specific_product = None + for product in products.PRODUCTS: + if product['product_id'] == product_id: + specific_product = product + break + if not specific_product: + abort_if_product_is_not_found(product_id) + + return specific_product + + +def abort_if_product_is_not_found(product_id): + """ + Helper method to search for Product + Abort if product not found and throw error + """ + abort(make_response(jsonify( + message="Product with id {} not found".format(product_id)), 404)) From 28b83a550e7fca2814e5af5f2acd1493ba78ff14 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Wed, 17 Oct 2018 12:51:57 +0300 Subject: [PATCH 045/100] [Feature #161201912] Create route for GET /products/ endpoint --- app/api/v1/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/api/v1/views.py b/app/api/v1/views.py index 25e77b7..777d42a 100644 --- a/app/api/v1/views.py +++ b/app/api/v1/views.py @@ -8,4 +8,5 @@ API = Api(v1_blueprint) API.add_resource(admin_endpoints.AdminActs, '/products') -API.add_resource(general_users_endpoints.GeneralUsersActs, '/products') \ No newline at end of file +API.add_resource(general_users_endpoints.GeneralUsersActs, '/products') +API.add_resource(general_users_endpoints.SpecificProduct, '/products/') \ No newline at end of file From 95850840e8671140e3bf20a4ec9a5f60d407084a Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Wed, 17 Oct 2018 18:13:45 +0300 Subject: [PATCH 046/100] [Chore #161201973] Create test for add sale record This tests the POST sale record endpoint --- app/tests/v1/base_test.py | 7 ++++ .../v1/test_store_attendant_endpoints.py | 32 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 app/tests/v1/test_store_attendant_endpoints.py diff --git a/app/tests/v1/base_test.py b/app/tests/v1/base_test.py index bf912fe..167b5d1 100644 --- a/app/tests/v1/base_test.py +++ b/app/tests/v1/base_test.py @@ -31,6 +31,13 @@ def setUp(self): 'category': 'Phones' } + self.SALE_ORDERS = { + 'product_name': 'Phone Model 1', + 'product_price': 55000, + 'quantity': 6, + 'amount': (55000 * 6) + } + def tearDown(self): """Destroy the application that diff --git a/app/tests/v1/test_store_attendant_endpoints.py b/app/tests/v1/test_store_attendant_endpoints.py new file mode 100644 index 0000000..78d23fe --- /dev/null +++ b/app/tests/v1/test_store_attendant_endpoints.py @@ -0,0 +1,32 @@ +"""Module contains tests for store attendant + +specific endpoints +""" +from flask import current_app + +from . import base_test +from . import helper_functions + +class TestAdminEndpoints(base_test.TestBaseClass): + """ Class contains tests for store attendant specific endpoints """ + + + def test_create_sale_order(self): + """Test POST /saleorder""" + + # send a dummy data response for testing + response = self.app_test_client.post('{}/saleorder'.format( + self.BASE_URL), json=self.SALE_ORDERS, headers={ + 'Content-Type': 'application/json'}) + + self.assertEqual(response.status_code, 201) + self.assertEqual(helper_functions.convert_response_to_json( + response)['sale_order']['product_name'], self.SALE_ORDERS['product_name']) + self.assertEqual(helper_functions.convert_response_to_json( + response)['sale_order']['product_price'], self.SALE_ORDERS['product_price']) + self.assertEqual(helper_functions.convert_response_to_json( + response)['sale_order']['quantity'], self.SALE_ORDERS['quantity']) + self.assertEqual(helper_functions.convert_response_to_json( + response)['sale_order']['amount'], self.SALE_ORDERS['amount']) + self.assertEqual(helper_functions.convert_response_to_json( + response)['message'], 'Sale record added successfully') \ No newline at end of file From e45cd77bcc665d0d57860ec221de155a85531b36 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Thu, 18 Oct 2018 00:11:26 +0300 Subject: [PATCH 047/100] [Chore #161248446] Create sale orders list --- app/api/v1/models/products.py | 4 ++-- app/api/v1/models/sale_orders.py | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 app/api/v1/models/sale_orders.py diff --git a/app/api/v1/models/products.py b/app/api/v1/models/products.py index 3373348..da894e9 100644 --- a/app/api/v1/models/products.py +++ b/app/api/v1/models/products.py @@ -1,6 +1,6 @@ -"""This module contains the data +"""This module contains the data store -store and data logic of the application +and data logic of the store's products """ PRODUCTS = [] \ No newline at end of file diff --git a/app/api/v1/models/sale_orders.py b/app/api/v1/models/sale_orders.py new file mode 100644 index 0000000..2b1cc76 --- /dev/null +++ b/app/api/v1/models/sale_orders.py @@ -0,0 +1,6 @@ +"""This module contains the data store + +and data logic of the store attendant's sale orders +""" + +SALE_ORDERS = [] \ No newline at end of file From 08799ad439a8d768c5c565fccf427cc8c19fdabb Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Thu, 18 Oct 2018 00:16:23 +0300 Subject: [PATCH 048/100] [Feature #161201919] Create POST /saleorder endpoint Creates an endpoint that allows the store attendant to create a sale order --- app/api/v1/resources/helper_functions.py | 29 +++++++++- .../v1/resources/store_attendant_endpoints.py | 54 +++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 app/api/v1/resources/store_attendant_endpoints.py diff --git a/app/api/v1/resources/helper_functions.py b/app/api/v1/resources/helper_functions.py index d8d17a8..54ee9a1 100644 --- a/app/api/v1/resources/helper_functions.py +++ b/app/api/v1/resources/helper_functions.py @@ -1,7 +1,7 @@ from datetime import datetime from flask import abort, jsonify, make_response -from app.api.v1.models import products +from app.api.v1.models import products, sale_orders def no_json_in_request(data): """Aborts if the data does @@ -73,3 +73,30 @@ def abort_if_product_is_not_found(product_id): """ abort(make_response(jsonify( message="Product with id {} not found".format(product_id)), 404)) + + +def add_new_sale_record(name, price, quantity, amount): + + """Creates a new sale record""" + + if name and price and quantity and amount: + # If all the required parameters are available + sale_order_id = len(sale_orders.SALE_ORDERS) + 1 + sale_order = { + 'sale_order_id': sale_order_id, + 'product_name': name, + 'product_price': price, + 'quantity': quantity, + 'amount': amount, + 'date_added': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + + sale_orders.SALE_ORDERS.append(sale_order) + response = jsonify({ + "message": "Sale record added successfully", + "sale_order": sale_orders.SALE_ORDERS[-1]}) + response.status_code = 201 + else: + # if any of the required parameters is missing or none + missing_a_required_parameter() + return response \ No newline at end of file diff --git a/app/api/v1/resources/store_attendant_endpoints.py b/app/api/v1/resources/store_attendant_endpoints.py new file mode 100644 index 0000000..23524f7 --- /dev/null +++ b/app/api/v1/resources/store_attendant_endpoints.py @@ -0,0 +1,54 @@ +"""This module contains endpoints + +that are specific to the store attendant +""" +from flask import Flask, jsonify, request, abort, make_response +from flask_restful import Resource + +from . import helper_functions +from app.api.v1.models import products + +class SaleRecords(Resource): + """Class contains the tests for store attendant + + specific endpoints + """ + + + def post(self): + """POST /saleorder endpoint""" + + data = request.get_json() + helper_functions.no_json_in_request(data) + try: + product_name = data['product_name'] + product_price = data['product_price'] + quantity = data['quantity'] + amount = (product_price * quantity) + except KeyError: + # If product is missing required parameter + helper_functions.missing_a_required_parameter() + + if product_price < 1: + abort(make_response(jsonify( + message="Bad request. Price of the product should be a positive integer above 0." + ), 400)) + + if not isinstance(product_name, str): + abort(make_response(jsonify( + message="Bad request. Product name should be a string" + ), 400)) + + if not isinstance(product_price, int): + abort(make_response(jsonify( + message="Bad request. The product price should be digits" + ), 400)) + + if not isinstance(quantity, int): + abort(make_response(jsonify( + message="Bad request. The quantity should be specified in digits" + ), 400)) + + response = helper_functions.add_new_sale_record(product_name, product_price, quantity, amount) + + return response From bf1e2879b25f6caa05bebd3a8bd60793c2098c60 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Thu, 18 Oct 2018 00:18:44 +0300 Subject: [PATCH 049/100] [Feature #161201919] Create route for POST /saleorder endpoint --- app/api/v1/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/api/v1/views.py b/app/api/v1/views.py index 777d42a..231fec6 100644 --- a/app/api/v1/views.py +++ b/app/api/v1/views.py @@ -3,10 +3,11 @@ from flask_restful import Api, Resource from . import v1_blueprint -from .resources import admin_endpoints, general_users_endpoints +from .resources import admin_endpoints, general_users_endpoints, store_attendant_endpoints API = Api(v1_blueprint) API.add_resource(admin_endpoints.AdminActs, '/products') API.add_resource(general_users_endpoints.GeneralUsersActs, '/products') -API.add_resource(general_users_endpoints.SpecificProduct, '/products/') \ No newline at end of file +API.add_resource(general_users_endpoints.SpecificProduct, '/products/') +API.add_resource(store_attendant_endpoints.SaleRecords, '/saleorder') \ No newline at end of file From 9cd9faf2dd5266f12da02625aaf9361d1c40e33e Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Thu, 18 Oct 2018 11:18:45 +0300 Subject: [PATCH 050/100] [Chore #161201973] Create test for GET /saleorder endpoint A test for the endpoint that will be used by the admin to view all the sale records made by the store attendants. The test fails. --- app/tests/v1/test_admin_endpoints.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/app/tests/v1/test_admin_endpoints.py b/app/tests/v1/test_admin_endpoints.py index 5c94268..eb6b8fe 100644 --- a/app/tests/v1/test_admin_endpoints.py +++ b/app/tests/v1/test_admin_endpoints.py @@ -2,6 +2,9 @@ specific endpoints """ + +import json + from flask import current_app from . import base_test @@ -29,4 +32,25 @@ def test_add_new_product(self): self.assertEqual(helper_functions.convert_response_to_json( response)['product']['category'], self.PRODUCT['category']) self.assertEqual(helper_functions.convert_response_to_json( - response)['message'], 'Product added successfully') \ No newline at end of file + response)['message'], 'Product added successfully') + + + def test_fetch_sale_orders(self): + """Test GET /saleorder - when sale order exists""" + + self.app_test_client.post( + '{}/saleorder'.format(self.BASE_URL), data=json.dumps(dict( + sale_order_id = 1, + product_name = "Test Product", + product_price = 20, + quantity = 1, + amount = 20 + )), content_type='application/json') + + response = self.app_test_client.get( + '{}/saleorder'.format(self.BASE_URL) + ) + + self.assertEqual(response.status_code, 200) + self.assertEqual(helper_functions.convert_response_to_json( + response)['sale_orders'][0]['product_name'], "Test Product") From 46b82bbaf9f9850854c8664fded51be6623e0c0c Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Thu, 18 Oct 2018 11:35:02 +0300 Subject: [PATCH 051/100] [Feature #161201927] Create GET /saleorders endpoint Creates an endpoint for the admin to fetch all the sale orders made by the store attendants --- app/api/v1/resources/admin_endpoints.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/app/api/v1/resources/admin_endpoints.py b/app/api/v1/resources/admin_endpoints.py index 126cbb9..77e5f84 100644 --- a/app/api/v1/resources/admin_endpoints.py +++ b/app/api/v1/resources/admin_endpoints.py @@ -6,9 +6,9 @@ from flask_restful import Resource from . import helper_functions -from app.api.v1.models import products +from app.api.v1.models import products, sale_orders -class AdminActs(Resource): +class ProductsManagement(Resource): """Class contains the tests for admin specific endpoints @@ -62,3 +62,17 @@ def post(self): response = helper_functions.add_new_product(product_name, product_price, category) return response + + +class SaleAttendantsManagement(Resource): + def get(self): + """GET /saleorder endpoint""" + + if not sale_orders.SALE_ORDERS: + # If no sale orders exist in the store yet + abort(make_response( + jsonify(message="There are no sale orders made yet"), 404)) + # If at least one sale order exists + response = jsonify({'sale_orders': sale_orders.SALE_ORDERS}) + response.status_code = 200 + return response From 8153ab63b92af291ea5244524fbd8eef5faf53d2 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Thu, 18 Oct 2018 11:39:13 +0300 Subject: [PATCH 052/100] [Feature #161201927] Create route for GET /saleorder This route services the admin's action of viewing all sale orders --- app/api/v1/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/api/v1/views.py b/app/api/v1/views.py index 231fec6..894c383 100644 --- a/app/api/v1/views.py +++ b/app/api/v1/views.py @@ -7,7 +7,8 @@ API = Api(v1_blueprint) -API.add_resource(admin_endpoints.AdminActs, '/products') +API.add_resource(admin_endpoints.ProductsManagement, '/products') API.add_resource(general_users_endpoints.GeneralUsersActs, '/products') API.add_resource(general_users_endpoints.SpecificProduct, '/products/') -API.add_resource(store_attendant_endpoints.SaleRecords, '/saleorder') \ No newline at end of file +API.add_resource(store_attendant_endpoints.SaleRecords, '/saleorder') +API.add_resource(admin_endpoints.SaleAttendantsManagement, '/saleorder') \ No newline at end of file From af03bff450c4100d13c8a22b32fd38c4bb7a093d Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Thu, 18 Oct 2018 11:58:45 +0300 Subject: [PATCH 053/100] [Release #161309750] Add gunicorn configuration to Procfile Create the configuration setting used by Heroku in the Procfile file --- Procfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Procfile b/Procfile index e69de29..80a980e 100644 --- a/Procfile +++ b/Procfile @@ -0,0 +1 @@ +web: gunicorn run:app \ No newline at end of file From 940d0200d0e09bde9ac2547cbc1b456a1beeb476 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Thu, 18 Oct 2018 13:01:45 +0300 Subject: [PATCH 054/100] [Release #161309750] Change environment setting in run.py --- run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run.py b/run.py index 0f6f0f7..0f454dc 100644 --- a/run.py +++ b/run.py @@ -2,7 +2,7 @@ from app import create_app -app = create_app(os.getenv('APP_SETTINGS')) +app = create_app('production') if __name__ == '__main__': From 8e3aa6f86d320f4dee8774ef2399d9c81c1704f2 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Thu, 18 Oct 2018 18:16:57 +0300 Subject: [PATCH 055/100] [Chore #161201973] Refactor tests to increase test coverage --- .../v1/resources/store_attendant_endpoints.py | 9 ++- app/tests/v1/test_admin_endpoints.py | 79 +++++++++++++++++++ app/tests/v1/test_general_user_endpoints.py | 1 - .../v1/test_store_attendant_endpoints.py | 72 ++++++++++++++++- 4 files changed, 155 insertions(+), 6 deletions(-) diff --git a/app/api/v1/resources/store_attendant_endpoints.py b/app/api/v1/resources/store_attendant_endpoints.py index 23524f7..3a020cf 100644 --- a/app/api/v1/resources/store_attendant_endpoints.py +++ b/app/api/v1/resources/store_attendant_endpoints.py @@ -29,6 +29,11 @@ def post(self): # If product is missing required parameter helper_functions.missing_a_required_parameter() + if not isinstance(product_price, int): + abort(make_response(jsonify( + message="Bad request. The product price should be digits" + ), 400)) + if product_price < 1: abort(make_response(jsonify( message="Bad request. Price of the product should be a positive integer above 0." @@ -39,10 +44,6 @@ def post(self): message="Bad request. Product name should be a string" ), 400)) - if not isinstance(product_price, int): - abort(make_response(jsonify( - message="Bad request. The product price should be digits" - ), 400)) if not isinstance(quantity, int): abort(make_response(jsonify( diff --git a/app/tests/v1/test_admin_endpoints.py b/app/tests/v1/test_admin_endpoints.py index eb6b8fe..b864bd7 100644 --- a/app/tests/v1/test_admin_endpoints.py +++ b/app/tests/v1/test_admin_endpoints.py @@ -34,6 +34,85 @@ def test_add_new_product(self): self.assertEqual(helper_functions.convert_response_to_json( response)['message'], 'Product added successfully') + def test_add_new_product_parameter_missing(self): + """Test POST /products + + with one of the required parameters missing + """ + response = self.app_test_client.post('{}/products'.format( + self.BASE_URL), json={'product_name': 'Nyundo'}, headers={ + 'Content-Type': 'application/json'}) + + self.assertEqual(response.status_code, 400) + self.assertEqual(helper_functions.convert_response_to_json( + response)['message'], 'Bad request. Request missing a required argument') + + def test_add_new_product_price_under_one(self): + """Test POST /products + + with the price of the product below minimum + """ + response = self.app_test_client.post('{}/products'.format( + self.BASE_URL), json={ + 'product_id': 1, 'product_name': "Hammer", 'product_price': 0, 'category':'Tools' + }, headers={'Content-Type': 'application/json'}) + + self.assertEqual(response.status_code, 400) + self.assertEqual(helper_functions.convert_response_to_json( + response)['message'], + 'Bad request. Price of the product should be a positive integer above 0.') + + + def test_add_new_product_with_product_name_not_string(self): + """Test POST /products + + with the product name not a string + """ + response = self.app_test_client.post('{}/products'.format( + self.BASE_URL), json={ + 'product_id': 1, 'product_name': 200, 'product_price': 200, 'category':'Tools' + }, headers={'Content-Type': 'application/json'}) + + self.assertEqual(response.status_code, 400) + self.assertEqual(helper_functions.convert_response_to_json( + response)['message'], + 'Bad request. Product name should be a string') + + def test_add_new_product_with_category_not_string(self): + """Test POST /products + + with the category not a string + """ + response = self.app_test_client.post('{}/products'.format( + self.BASE_URL), json={ + 'product_id': 1, 'product_name': "Hammer", 'product_price': 200, 'category': 200 + }, headers={'Content-Type': 'application/json'}) + + self.assertEqual(response.status_code, 400) + self.assertEqual(helper_functions.convert_response_to_json( + response)['message'], + 'Bad request. The Category should be a string') + + def test_add_new_product_with_product_name_already_existing(self): + """Test POST /products + + with the product name already existing + """ + self.app_test_client.post('{}/products'.format( + self.BASE_URL), json={ + 'product_id': 1, 'product_name': "Hammer", 'product_price': 200, 'category': "Tools" + }, headers={'Content-Type': 'application/json'}) + + response = self.app_test_client.post('{}/products'.format( + self.BASE_URL), json={ + 'product_id': 1, 'product_name': "Hammer", 'product_price': 200, 'category': "Tools" + }, headers={'Content-Type': 'application/json'}) + + self.assertEqual(response.status_code, 400) + self.assertEqual(helper_functions.convert_response_to_json( + response)['message'], + 'Product with a similar name already exists') + def test_fetch_sale_orders(self): """Test GET /saleorder - when sale order exists""" diff --git a/app/tests/v1/test_general_user_endpoints.py b/app/tests/v1/test_general_user_endpoints.py index ee4e53d..9391925 100644 --- a/app/tests/v1/test_general_user_endpoints.py +++ b/app/tests/v1/test_general_user_endpoints.py @@ -21,7 +21,6 @@ def test_retrieve_all_products(self): self.assertEqual(response.status_code, 200) self.assertEqual(helper_functions.convert_response_to_json( response)['products'][0]['product_name'], self.PRODUCT['product_name']) - self.assertEqual(len(helper_functions.convert_response_to_json(response)['products']), 1) def test_retrieve_specific_product(self): """Test GET /products/id - when product exist""" diff --git a/app/tests/v1/test_store_attendant_endpoints.py b/app/tests/v1/test_store_attendant_endpoints.py index 78d23fe..d72e9cc 100644 --- a/app/tests/v1/test_store_attendant_endpoints.py +++ b/app/tests/v1/test_store_attendant_endpoints.py @@ -29,4 +29,74 @@ def test_create_sale_order(self): self.assertEqual(helper_functions.convert_response_to_json( response)['sale_order']['amount'], self.SALE_ORDERS['amount']) self.assertEqual(helper_functions.convert_response_to_json( - response)['message'], 'Sale record added successfully') \ No newline at end of file + response)['message'], 'Sale record added successfully') + + + def test_create_sale_order_parameter_missing(self): + """Test POST /saleorder + + with one of the required parameters missing + """ + response = self.app_test_client.post('{}/saleorder'.format( + self.BASE_URL), json={'product_name': 'Nyundo'}, headers={ + 'Content-Type': 'application/json'}) + + self.assertEqual(response.status_code, 400) + self.assertEqual(helper_functions.convert_response_to_json( + response)['message'], 'Bad request. Request missing a required argument') + + + def test_create_sale_order_price_below_one(self): + """Test POST /saleorder + + with the price not a valid integer + """ + response = self.app_test_client.post('{}/saleorder'.format( + self.BASE_URL), json={'product_name': 'Nyundo', 'product_price': -1, 'quantity': 1}, headers={ + 'Content-Type': 'application/json'}) + + self.assertEqual(response.status_code, 400) + self.assertEqual(helper_functions.convert_response_to_json( + response)['message'], 'Bad request. Price of the product should be a positive integer above 0.') + + + def test_create_sale_order_invalid_product_name(self): + """Test POST /saleorder + + with the product name not a string + """ + response = self.app_test_client.post('{}/saleorder'.format( + self.BASE_URL), json={'product_name': 3, 'product_price': 300, 'quantity': 1}, headers={ + 'Content-Type': 'application/json'}) + + self.assertEqual(response.status_code, 400) + self.assertEqual(helper_functions.convert_response_to_json( + response)['message'], 'Bad request. Product name should be a string') + + + def test_create_sale_order_price_not_digits(self): + """Test POST /saleorder + + with the price not a valid integer + """ + response = self.app_test_client.post('{}/saleorder'.format( + self.BASE_URL), json={'product_name': "Nyundo", 'product_price': "300", 'quantity': 1}, headers={ + 'Content-Type': 'application/json'}) + + self.assertEqual(response.status_code, 400) + self.assertEqual(helper_functions.convert_response_to_json( + response)['message'], 'Bad request. The product price should be digits') + + + def test_create_sale_order_invalid_quantity(self): + """Test POST /saleorder + + with the quantity not a valid integer + """ + response = self.app_test_client.post('{}/saleorder'.format( + self.BASE_URL), json={'product_name': "Nyundo", 'product_price': 300, 'quantity': "1"}, headers={ + 'Content-Type': 'application/json'}) + + self.assertEqual(response.status_code, 400) + self.assertEqual(helper_functions.convert_response_to_json( + response)['message'], 'Bad request. The quantity should be specified in digits') \ No newline at end of file From 85181d986106684b8fbc55754632b1ca2613710b Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Fri, 19 Oct 2018 10:58:40 +0300 Subject: [PATCH 056/100] [Chore #161201973] Create test for fetch specific sale order endpoint This ensures that there are no errors in retrieving a specific sale order --- app/tests/v1/test_general_user_endpoints.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/tests/v1/test_general_user_endpoints.py b/app/tests/v1/test_general_user_endpoints.py index 9391925..b586ff7 100644 --- a/app/tests/v1/test_general_user_endpoints.py +++ b/app/tests/v1/test_general_user_endpoints.py @@ -3,6 +3,8 @@ are general to both the admin and the normal user """ +import json + from . import base_test from . import helper_functions @@ -12,6 +14,7 @@ class TestGeneralUsersEndpoints(base_test.TestBaseClass): and normal user, endpoints' tests """ + def test_retrieve_all_products(self): """Test GET /products - when products exist""" @@ -30,4 +33,13 @@ def test_retrieve_specific_product(self): self.assertEqual(response.status_code, 200) self.assertEqual(helper_functions.convert_response_to_json( - response)['product_name'], self.PRODUCT['product_name']) \ No newline at end of file + response)['product_name'], self.PRODUCT['product_name']) + + def test_retrieve_specific_sale_order(self): + """Test GET /saleorder/id - when saleorder exists""" + + + response = self.app_test_client.get( + '{}/saleorder/1'.format(self.BASE_URL)) + + self.assertEqual(response.status_code, 200) From 5aca05f1dbb0b16a8191dc15861492efc7b12568 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Fri, 19 Oct 2018 11:06:20 +0300 Subject: [PATCH 057/100] [Feature #161337098] Create a GET /saleorder/ endpoint --- .../v1/resources/general_users_endpoints.py | 15 +++++++++++--- app/api/v1/resources/helper_functions.py | 20 ++++++++++++++++--- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/app/api/v1/resources/general_users_endpoints.py b/app/api/v1/resources/general_users_endpoints.py index f9402cb..427a43b 100644 --- a/app/api/v1/resources/general_users_endpoints.py +++ b/app/api/v1/resources/general_users_endpoints.py @@ -8,7 +8,7 @@ from . import helper_functions from app.api.v1.models import products -class GeneralUsersActs(Resource): +class AllProducts(Resource): """Class contains the tests for both the admin and the normal user endpoints @@ -31,10 +31,19 @@ class SpecificProduct(Resource): def get(self, product_id): """GET /products/""" - product = helper_functions.retrieve_specific_product(product_id) response = jsonify(product) response.status_code = 200 - return response \ No newline at end of file + return response + + +class SpecificSaleOrder(Resource): + + def get(self, sale_order_id): + """GET /saleorder/""" + + sale_order = helper_functions.retrieve_specific_sale_order(sale_order_id) + response = jsonify(sale_order) + response.status_code = 200 \ No newline at end of file diff --git a/app/api/v1/resources/helper_functions.py b/app/api/v1/resources/helper_functions.py index 54ee9a1..fad9749 100644 --- a/app/api/v1/resources/helper_functions.py +++ b/app/api/v1/resources/helper_functions.py @@ -61,18 +61,32 @@ def retrieve_specific_product(product_id): specific_product = product break if not specific_product: - abort_if_product_is_not_found(product_id) + abort_if_item_is_not_found(product_id) return specific_product +def retrieve_specific_sale_order(sale_id): + """ + Helper method to fetch sale order from list of sale orders, + given a valid id + """ + specific_sale_order = None + for sale_order in sale_orders.SALE_ORDERS: + if sale_order['sale_order_id'] == sale_id: + specific_sale_order = sale_order + break + if not specific_sale_order: + abort_if_item_is_not_found(sale_id) + + return specific_sale_order -def abort_if_product_is_not_found(product_id): +def abort_if_item_is_not_found(item_id): """ Helper method to search for Product Abort if product not found and throw error """ abort(make_response(jsonify( - message="Product with id {} not found".format(product_id)), 404)) + message="Item with id {} not found".format(item_id)), 404)) def add_new_sale_record(name, price, quantity, amount): From 95fb8c9b5c00b34961c949fda3ab06c01ac90247 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Fri, 19 Oct 2018 11:07:17 +0300 Subject: [PATCH 058/100] [Feature #161337098] Create a route for GET /saleorder/ --- app/api/v1/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/api/v1/views.py b/app/api/v1/views.py index 894c383..1ada4c9 100644 --- a/app/api/v1/views.py +++ b/app/api/v1/views.py @@ -8,7 +8,8 @@ API = Api(v1_blueprint) API.add_resource(admin_endpoints.ProductsManagement, '/products') -API.add_resource(general_users_endpoints.GeneralUsersActs, '/products') +API.add_resource(general_users_endpoints.AllProducts, '/products') API.add_resource(general_users_endpoints.SpecificProduct, '/products/') API.add_resource(store_attendant_endpoints.SaleRecords, '/saleorder') -API.add_resource(admin_endpoints.SaleAttendantsManagement, '/saleorder') \ No newline at end of file +API.add_resource(admin_endpoints.SaleAttendantsManagement, '/saleorder') +API.add_resource(general_users_endpoints.SpecificSaleOrder, '/saleorder/') \ No newline at end of file From fcfd6d3d833fd40366b55c5b506ba3d260972965 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Fri, 19 Oct 2018 23:17:58 +0300 Subject: [PATCH 059/100] [Chore #161223919] Add endpoints listings --- README.md | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dae3b67..46e0211 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,59 @@ # store-manager-api -## Continous Integration badges -[![Build Status](https://travis-ci.com/calebrotich10/store-manager-api.svg?branch=develop)](https://travis-ci.com/calebrotich10/store-manager-api) [![Coverage Status](https://coveralls.io/repos/github/calebrotich10/store-manager-api/badge.svg?branch=develop)](https://coveralls.io/github/calebrotich10/store-manager-api?branch=develop) [![Maintainability](https://api.codeclimate.com/v1/badges/e87820f417b8d15c3a64/maintainability)](https://codeclimate.com/github/calebrotich10/store-manager-api/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/e87820f417b8d15c3a64/test_coverage)](https://codeclimate.com/github/calebrotich10/store-manager-api/test_coverage) - +#### Continous Integration badges +[![Build Status](https://travis-ci.com/calebrotich10/store-manager-api.svg?branch=develop)](https://travis-ci.com/calebrotich10/store-manager-api) [![Coverage Status](https://coveralls.io/repos/github/calebrotich10/store-manager-api/badge.svg?branch=develop)](https://coveralls.io/github/calebrotich10/store-manager-api?branch=develop) [![Maintainability](https://api.codeclimate.com/v1/badges/e87820f417b8d15c3a64/maintainability)](https://codeclimate.com/github/calebrotich10/store-manager-api/maintainability) Store Manager is a web application that helps store owners manage sales and product inventory records. This application is meant for use in a single store. This repository contains the API endpoints for the application. + +#### Endpoints + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Http MethodEndpointFunctionality
POSTapi/v1/auth/signupCreates a new user account
POSTapi/v1/productsUsed by the admin to add a new product
POSTapi/v1/saleorderUsed by the sale attendant to add a new sale order
GETapi/v1/auth/signinAuthenticates and creates a token for the users
GETapi/v1/products/<product_id>Enables a user to fetch a specific product
GETapi/v1/saleorder/<sale_order_id>Enables a user to fetch a specific sale order
GETapi/v1/productsEnables a user to fetch all products
GETapi/v1/saleorderEnables a user to fetch all sale orders
+ +#### Deployment +[Heroku](https://store-manager-api.herokuapp.com/api/v1/products) From 489b91401ddc3ce47366f2f949933a23e201cee8 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Sat, 20 Oct 2018 19:03:15 +0300 Subject: [PATCH 060/100] [Chore #161201973] Create tests for the auth endpoints This tests registration and authentication process --- app/tests/v1/base_test.py | 34 +++ app/tests/v1/test_admin_endpoints.py | 70 ++++-- app/tests/v1/test_auth_endpoints.py | 219 ++++++++++++++++++ app/tests/v1/test_general_user_endpoints.py | 20 +- .../v1/test_store_attendant_endpoints.py | 48 ++-- 5 files changed, 356 insertions(+), 35 deletions(-) create mode 100644 app/tests/v1/test_auth_endpoints.py diff --git a/app/tests/v1/base_test.py b/app/tests/v1/base_test.py index 167b5d1..029ac7d 100644 --- a/app/tests/v1/base_test.py +++ b/app/tests/v1/base_test.py @@ -7,6 +7,7 @@ # local imports from app import create_app from instance.config import config +from . import helper_functions class TestBaseClass(unittest.TestCase): @@ -46,6 +47,39 @@ def tearDown(self): """ self.app_context.pop() + def register_test_admin_account(self): + #Register attendant + """Registers an admin test user account""" + + res = self.app_test_client.post("api/v1/auth/signup", + json={ + "email": "user@gmail.com", + "role": "Admin", + "password": "Password12#" + }, + headers={ + "Content-Type": "application/json" + }) + + return res + + def login_test_admin(self): + """Validates the test account for the admin""" + + # Login the test account for the admin + resp = self.app_test_client.post("api/v1/auth/login", + json={ + "email": "user@gmail.com", + "password": "Password12#" + }, + headers={ + "Content-Type": "application/json" + }) + + auth_token = helper_functions.convert_response_to_json( + resp)['token'] + + return auth_token if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/app/tests/v1/test_admin_endpoints.py b/app/tests/v1/test_admin_endpoints.py index b864bd7..50de744 100644 --- a/app/tests/v1/test_admin_endpoints.py +++ b/app/tests/v1/test_admin_endpoints.py @@ -13,14 +13,15 @@ class TestAdminEndpoints(base_test.TestBaseClass): """ Class contains tests for admin specific endpoints """ - def test_add_new_product(self): """Test POST /products""" - + self.register_test_admin_account() + token = self.login_test_admin() + # send a dummy data response for testing response = self.app_test_client.post('{}/products'.format( - self.BASE_URL), json=self.PRODUCT, headers={ - 'Content-Type': 'application/json'}) + self.BASE_URL), json=self.PRODUCT, headers=dict(Authorization="Bearer " + token), + content_type='application/json') self.assertEqual(response.status_code, 201) self.assertEqual(helper_functions.convert_response_to_json( @@ -39,9 +40,12 @@ def test_add_new_product_parameter_missing(self): with one of the required parameters missing """ + self.register_test_admin_account() + token = self.login_test_admin() + response = self.app_test_client.post('{}/products'.format( - self.BASE_URL), json={'product_name': 'Nyundo'}, headers={ - 'Content-Type': 'application/json'}) + self.BASE_URL), json={'product_name': 'Nyundo'}, headers=dict(Authorization="Bearer " + token), + content_type='application/json') self.assertEqual(response.status_code, 400) self.assertEqual(helper_functions.convert_response_to_json( @@ -52,10 +56,14 @@ def test_add_new_product_price_under_one(self): with the price of the product below minimum """ + self.register_test_admin_account() + token = self.login_test_admin() + response = self.app_test_client.post('{}/products'.format( self.BASE_URL), json={ 'product_id': 1, 'product_name': "Hammer", 'product_price': 0, 'category':'Tools' - }, headers={'Content-Type': 'application/json'}) + }, headers=dict(Authorization="Bearer " + token), + content_type='application/json') self.assertEqual(response.status_code, 400) self.assertEqual(helper_functions.convert_response_to_json( @@ -68,10 +76,14 @@ def test_add_new_product_with_product_name_not_string(self): with the product name not a string """ + self.register_test_admin_account() + token = self.login_test_admin() + response = self.app_test_client.post('{}/products'.format( self.BASE_URL), json={ 'product_id': 1, 'product_name': 200, 'product_price': 200, 'category':'Tools' - }, headers={'Content-Type': 'application/json'}) + }, headers=dict(Authorization="Bearer " + token), + content_type='application/json') self.assertEqual(response.status_code, 400) self.assertEqual(helper_functions.convert_response_to_json( @@ -83,10 +95,14 @@ def test_add_new_product_with_category_not_string(self): with the category not a string """ + self.register_test_admin_account() + token = self.login_test_admin() + response = self.app_test_client.post('{}/products'.format( self.BASE_URL), json={ 'product_id': 1, 'product_name': "Hammer", 'product_price': 200, 'category': 200 - }, headers={'Content-Type': 'application/json'}) + }, headers=dict(Authorization="Bearer " + token), + content_type='application/json') self.assertEqual(response.status_code, 400) self.assertEqual(helper_functions.convert_response_to_json( @@ -98,15 +114,20 @@ def test_add_new_product_with_product_name_already_existing(self): with the product name already existing """ + self.register_test_admin_account() + token = self.login_test_admin() + self.app_test_client.post('{}/products'.format( self.BASE_URL), json={ 'product_id': 1, 'product_name': "Hammer", 'product_price': 200, 'category': "Tools" - }, headers={'Content-Type': 'application/json'}) + }, headers=dict(Authorization="Bearer " + token), + content_type='application/json') response = self.app_test_client.post('{}/products'.format( self.BASE_URL), json={ 'product_id': 1, 'product_name': "Hammer", 'product_price': 200, 'category': "Tools" - }, headers={'Content-Type': 'application/json'}) + }, headers=dict(Authorization="Bearer " + token), + content_type='application/json') self.assertEqual(response.status_code, 400) self.assertEqual(helper_functions.convert_response_to_json( @@ -116,20 +137,29 @@ def test_add_new_product_with_product_name_already_existing(self): def test_fetch_sale_orders(self): """Test GET /saleorder - when sale order exists""" + self.register_test_admin_account() + token = self.login_test_admin() self.app_test_client.post( - '{}/saleorder'.format(self.BASE_URL), data=json.dumps(dict( - sale_order_id = 1, - product_name = "Test Product", - product_price = 20, - quantity = 1, - amount = 20 - )), content_type='application/json') - + '{}/saleorder'.format(self.BASE_URL), json={ + 'sale_order_id': 1, + 'product_name': "Test Product", + 'product_price': 20, + 'quantity': 1, + 'amount': 20 + }, + headers=dict(Authorization="Bearer " + token), + content_type='application/json') + response = self.app_test_client.get( - '{}/saleorder'.format(self.BASE_URL) + '{}/saleorder'.format(self.BASE_URL), + + headers=dict(Authorization="Bearer " + token), + content_type='application/json' ) self.assertEqual(response.status_code, 200) self.assertEqual(helper_functions.convert_response_to_json( response)['sale_orders'][0]['product_name'], "Test Product") + self.assertEqual(helper_functions.convert_response_to_json( + response)['sale_orders'][0]['product_price'], 20) diff --git a/app/tests/v1/test_auth_endpoints.py b/app/tests/v1/test_auth_endpoints.py new file mode 100644 index 0000000..18bdf5f --- /dev/null +++ b/app/tests/v1/test_auth_endpoints.py @@ -0,0 +1,219 @@ +"""Module contains tests to endpoints that + +are used for user registration and authentication +""" + +import json + +from . import base_test +from . import helper_functions + +class TestAuthEndpoints(base_test.TestBaseClass): + """ Class contains tests for auth endpoints """ + + def test_add_new_user(self): + res = self.app_test_client.post("api/v1/auth/signup", + json={ + "email": "test_add_new_user@gmail.com", + "role": "Admin", + "password": "Password12#" + }, + headers={ + "Content-Type": "application/json" + }) + + data = json.loads(res.data.decode()) + print(data) + + self.assertEqual(data['email'], "test_add_new_user@gmail.com") + self.assertEqual(res.status_code, 202) + + def test_add_new_user_no_data(self): + res = self.app_test_client.post("api/v1/auth/signup", + json={ + + }, + headers={ + "Content-Type": "application/json" + }) + + data = json.loads(res.data.decode()) + print(res) + + self.assertEqual(data['message'], "Missing required credentials") + self.assertEqual(res.status_code, 400) + + def test_add_new_user_missing_params(self): + res = self.app_test_client.post("api/v1/auth/signup", + json={ + "email": "", + "role": "Admin", + "password": "Password12#" + }, + headers={ + "Content-Type": "application/json" + }) + + data = json.loads(res.data.decode()) + print(data) + + self.assertEqual(data['message'], "You are missing a required credential") + self.assertEqual(res.status_code, 400) + + def test_add_new_user_invalid_email(self): + res = self.app_test_client.post("api/v1/auth/signup", + json={ + "email": "test_add_new_user", + "role": "Admin", + "password": "Password12#" + }, + headers={ + "Content-Type": "application/json" + }) + + data = json.loads(res.data.decode()) + print(data) + + self.assertEqual(data['message'], "Invalid email") + self.assertEqual(res.status_code, 400) + + def test_add_new_user_no_digit_password(self): + res = self.app_test_client.post("api/v1/auth/signup", + json={ + "email": "test_add_new_user_invalid_email@gmail.com", + "role": "Admin", + "password": "No#digit" + }, + headers={ + "Content-Type": "application/json" + }) + + data = json.loads(res.data.decode()) + print(data) + + self.assertEqual(data['message'], "Password must have a digit") + self.assertEqual(res.status_code, 400) + + def test_add_new_user_short_password(self): + res = self.app_test_client.post("api/v1/auth/signup", + json={ + "email": "test_add_new_user_invalid_email@gmail.com", + "role": "Admin", + "password": "Shor#" + }, + headers={ + "Content-Type": "application/json" + }) + + data = json.loads(res.data.decode()) + print(data) + + self.assertEqual(data['message'], "Password must be long than 6 characters or less than 12") + self.assertEqual(res.status_code, 400) + + def test_add_new_user_no_special_ch_password(self): + res = self.app_test_client.post("api/v1/auth/signup", + json={ + "email": "test_add_new_user_invalid_email@gmail.com", + "role": "Admin", + "password": "NoSplCh12" + }, + headers={ + "Content-Type": "application/json" + }) + + data = json.loads(res.data.decode()) + print(data) + + self.assertEqual(data['message'], "Password must have a special charater") + self.assertEqual(res.status_code, 400) + + def test_add_new_user_no_upper_case_password(self): + res = self.app_test_client.post("api/v1/auth/signup", + json={ + "email": "test_add_new_user_invalid_email@gmail.com", + "role": "Admin", + "password": "noupper12#" + }, + headers={ + "Content-Type": "application/json" + }) + + data = json.loads(res.data.decode()) + print(data) + + self.assertEqual(data['message'], "Password must have an upper case character") + self.assertEqual(res.status_code, 400) + + def test_add_new_user_no_lower_case_password(self): + res = self.app_test_client.post("api/v1/auth/signup", + json={ + "email": "test_add_new_user_invalid_email@gmail.com", + "role": "Admin", + "password": "NOLOWER12#" + }, + headers={ + "Content-Type": "application/json" + }) + + data = json.loads(res.data.decode()) + print(data) + + self.assertEqual(data['message'], "Password must have a lower case character") + self.assertEqual(res.status_code, 400) + + def test_add_new_user_existing(self): + """Test POST /auth/signup""" + self.register_test_admin_account() + response = self.register_test_admin_account() + data = json.loads(response.data.decode()) + + self.assertEqual(data['message'], "User already exists") + self.assertEqual(response.status_code, 406) + + def test_login_existing_user(self): + self.register_test_admin_account() + resp = self.app_test_client.post("api/v1/auth/login", + json={ + "email": "user@gmail.com", + "password": "Password12#" + }, + headers={ + "Content-Type": "application/json" + }) + + self.assertTrue(helper_functions.convert_response_to_json( + resp)['token']) + self.assertTrue(helper_functions.convert_response_to_json( + resp)['message'], "You are successfully logged in!") + self.assertEqual(resp.status_code, 200) + + def test_login_no_credentials(self): + self.register_test_admin_account() + resp = self.app_test_client.post("api/v1/auth/login", + json={ + + }, + headers={ + "Content-Type": "application/json" + }) + + self.assertTrue(helper_functions.convert_response_to_json( + resp)['message'], "Kindly enter your credentials") + self.assertEqual(resp.status_code, 400) + + def test_login_non_matching_credentials(self): + self.register_test_admin_account() + resp = self.app_test_client.post("api/v1/auth/login", + json={ + "email": "non_matching_credentials_user_1018@gmail.com", + "password": "neverexpecteduser" + }, + headers={ + "Content-Type": "application/json" + }) + + self.assertTrue(helper_functions.convert_response_to_json( + resp)['message'], "Wrong credentials provided") + self.assertEqual(resp.status_code, 403) + \ No newline at end of file diff --git a/app/tests/v1/test_general_user_endpoints.py b/app/tests/v1/test_general_user_endpoints.py index b586ff7..2ce1d26 100644 --- a/app/tests/v1/test_general_user_endpoints.py +++ b/app/tests/v1/test_general_user_endpoints.py @@ -38,8 +38,24 @@ def test_retrieve_specific_product(self): def test_retrieve_specific_sale_order(self): """Test GET /saleorder/id - when saleorder exists""" - + self.register_test_admin_account() + token = self.login_test_admin() + + self.app_test_client.post( + '{}/saleorder'.format(self.BASE_URL), json={ + 'sale_order_id': 1, + 'product_name': "Test Product", + 'product_price': 20, + 'quantity': 1, + 'amount': 20 + }, + headers=dict(Authorization="Bearer " + token), + content_type='application/json') + response = self.app_test_client.get( - '{}/saleorder/1'.format(self.BASE_URL)) + '{}/saleorder/1'.format(self.BASE_URL), + headers=dict(Authorization="Bearer " + token), + content_type='application/json' + ) self.assertEqual(response.status_code, 200) diff --git a/app/tests/v1/test_store_attendant_endpoints.py b/app/tests/v1/test_store_attendant_endpoints.py index d72e9cc..56791ac 100644 --- a/app/tests/v1/test_store_attendant_endpoints.py +++ b/app/tests/v1/test_store_attendant_endpoints.py @@ -13,11 +13,13 @@ class TestAdminEndpoints(base_test.TestBaseClass): def test_create_sale_order(self): """Test POST /saleorder""" - + self.register_test_admin_account() + token = self.login_test_admin() + # send a dummy data response for testing response = self.app_test_client.post('{}/saleorder'.format( - self.BASE_URL), json=self.SALE_ORDERS, headers={ - 'Content-Type': 'application/json'}) + self.BASE_URL), json=self.SALE_ORDERS, headers=dict(Authorization="Bearer " + token), + content_type='application/json') self.assertEqual(response.status_code, 201) self.assertEqual(helper_functions.convert_response_to_json( @@ -37,9 +39,12 @@ def test_create_sale_order_parameter_missing(self): with one of the required parameters missing """ + self.register_test_admin_account() + token = self.login_test_admin() + response = self.app_test_client.post('{}/saleorder'.format( - self.BASE_URL), json={'product_name': 'Nyundo'}, headers={ - 'Content-Type': 'application/json'}) + self.BASE_URL), json={'product_name': 'Nyundo'}, headers=dict(Authorization="Bearer " + token), + content_type='application/json') self.assertEqual(response.status_code, 400) self.assertEqual(helper_functions.convert_response_to_json( @@ -51,9 +56,13 @@ def test_create_sale_order_price_below_one(self): with the price not a valid integer """ + self.register_test_admin_account() + token = self.login_test_admin() + response = self.app_test_client.post('{}/saleorder'.format( - self.BASE_URL), json={'product_name': 'Nyundo', 'product_price': -1, 'quantity': 1}, headers={ - 'Content-Type': 'application/json'}) + self.BASE_URL), json={'product_name': 'Nyundo', 'product_price': -1, 'quantity': 1}, + headers=dict(Authorization="Bearer " + token), + content_type='application/json') self.assertEqual(response.status_code, 400) self.assertEqual(helper_functions.convert_response_to_json( @@ -65,9 +74,13 @@ def test_create_sale_order_invalid_product_name(self): with the product name not a string """ + self.register_test_admin_account() + token = self.login_test_admin() + response = self.app_test_client.post('{}/saleorder'.format( - self.BASE_URL), json={'product_name': 3, 'product_price': 300, 'quantity': 1}, headers={ - 'Content-Type': 'application/json'}) + self.BASE_URL), json={'product_name': 3, 'product_price': 300, 'quantity': 1}, + headers=dict(Authorization="Bearer " + token), + content_type='application/json') self.assertEqual(response.status_code, 400) self.assertEqual(helper_functions.convert_response_to_json( @@ -79,9 +92,14 @@ def test_create_sale_order_price_not_digits(self): with the price not a valid integer """ + + self.register_test_admin_account() + token = self.login_test_admin() + response = self.app_test_client.post('{}/saleorder'.format( - self.BASE_URL), json={'product_name': "Nyundo", 'product_price': "300", 'quantity': 1}, headers={ - 'Content-Type': 'application/json'}) + self.BASE_URL), json={'product_name': "Nyundo", 'product_price': "300", 'quantity': 1}, + headers=dict(Authorization="Bearer " + token), + content_type='application/json') self.assertEqual(response.status_code, 400) self.assertEqual(helper_functions.convert_response_to_json( @@ -93,9 +111,13 @@ def test_create_sale_order_invalid_quantity(self): with the quantity not a valid integer """ + self.register_test_admin_account() + token = self.login_test_admin() + response = self.app_test_client.post('{}/saleorder'.format( - self.BASE_URL), json={'product_name': "Nyundo", 'product_price': 300, 'quantity': "1"}, headers={ - 'Content-Type': 'application/json'}) + self.BASE_URL), json={'product_name': "Nyundo", 'product_price': 300, 'quantity': "1"}, + headers=dict(Authorization="Bearer " + token), + content_type='application/json') self.assertEqual(response.status_code, 400) self.assertEqual(helper_functions.convert_response_to_json( From 370e21e8ef130116c2d7550ef0ed585e2d0145ab Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Sat, 20 Oct 2018 19:05:46 +0300 Subject: [PATCH 061/100] [Chore #161209988] Create an authentication blueprint --- app/__init__.py | 12 +++++++++++- app/api/v1/__init__.py | 4 +++- run.py | 1 + 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 714882f..acbc658 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,13 +1,23 @@ +import os + from flask import Flask +from flask_jwt_extended import JWTManager + from instance.config import config +jwt = JWTManager() def create_app(config_name): app = Flask(__name__) app.config.from_object(config[config_name]) + app.config['JWT_SECRET_KEY'] = os.getenv('JWT_SECRET_KEY', default='SdaHv342nx!jknr837bjwd?c,lsajjjhw673hdsbgeh') + jwt.init_app(app) - from .api.v1 import v1_blueprint as v1_blueprint + from .api.v1 import endpoint_v1_blueprint as v1_blueprint app.register_blueprint(v1_blueprint, url_prefix='/api/v1') + from .api.v1 import auth_v1_blueprint as v1_blueprint + app.register_blueprint(v1_blueprint, url_prefix='/api/v1/auth') + return app diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py index 5618f81..e6978c0 100644 --- a/app/api/v1/__init__.py +++ b/app/api/v1/__init__.py @@ -1,7 +1,9 @@ from flask import Blueprint -v1_blueprint = Blueprint('v1_blueprint', __name__) +endpoint_v1_blueprint = Blueprint('endpoint_v1_blueprint', __name__) +auth_v1_blueprint = Blueprint('auth_v1_blueprint', __name__) + from . import views \ No newline at end of file diff --git a/run.py b/run.py index 0f454dc..3da315a 100644 --- a/run.py +++ b/run.py @@ -1,4 +1,5 @@ import os + from app import create_app From 9bea170708fed3bb2f163b89da417ec462b3a690 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Sat, 20 Oct 2018 19:25:14 +0300 Subject: [PATCH 062/100] [Chore #161209925] Add authentication packages Add packages such as JWT, Flask-JWT-Extended and validate-email --- requirements.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/requirements.txt b/requirements.txt index 8b8c1c7..805cb1f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,26 +1,33 @@ aniso8601==3.0.2 +asn1crypto==0.24.0 astroid==2.0.4 atomicwrites==1.2.1 attrs==18.2.0 certifi==2018.8.24 +cffi==1.11.5 chardet==3.0.4 Click==7.0 coverage==4.0.3 coveralls==1.5.1 +cryptography==2.3.1 docopt==0.6.2 Flask==1.0.2 +Flask-JWT-Extended==3.13.1 Flask-RESTful==0.3.6 gunicorn==19.9.0 idna==2.7 isort==4.3.4 itsdangerous==0.24 Jinja2==2.10 +jwt==0.5.4 lazy-object-proxy==1.3.1 MarkupSafe==1.0 mccabe==0.6.1 more-itertools==4.3.0 pluggy==0.7.1 py==1.7.0 +pycparser==2.19 +PyJWT==1.6.4 pylint==2.1.1 pytest==3.8.2 python-coveralls==2.9.1 @@ -31,5 +38,6 @@ requests==2.19.1 six==1.11.0 typed-ast==1.1.0 urllib3==1.23 +validate-email==1.3 Werkzeug==0.14.1 wrapt==1.10.11 From 3a4635803ca4815aaa38fce0e0b8eae6ecc64ee3 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Sat, 20 Oct 2018 19:42:00 +0300 Subject: [PATCH 063/100] [Feature #161362891] Add /auth endpoints Adds registration and login endpoint --- app/api/v1/resources/admin_endpoints.py | 18 +++++-- app/api/v1/resources/auth.py | 50 +++++++++++++++++++ .../v1/resources/general_users_endpoints.py | 3 +- app/api/v1/resources/helper_functions.py | 13 +++-- .../v1/resources/store_attendant_endpoints.py | 3 +- app/api/v1/utils/validator.py | 39 +++++++++++++++ 6 files changed, 117 insertions(+), 9 deletions(-) create mode 100644 app/api/v1/resources/auth.py create mode 100644 app/api/v1/utils/validator.py diff --git a/app/api/v1/resources/admin_endpoints.py b/app/api/v1/resources/admin_endpoints.py index 77e5f84..579dfc6 100644 --- a/app/api/v1/resources/admin_endpoints.py +++ b/app/api/v1/resources/admin_endpoints.py @@ -4,9 +4,10 @@ """ from flask import Flask, jsonify, request, abort, make_response from flask_restful import Resource +from flask_jwt_extended import get_jwt_identity, jwt_required from . import helper_functions -from app.api.v1.models import products, sale_orders +from app.api.v1.models import products, sale_orders, users class ProductsManagement(Resource): """Class contains the tests for admin @@ -14,10 +15,13 @@ class ProductsManagement(Resource): specific endpoints """ - + @jwt_required def post(self): """POST /products endpoint""" - + + user_email = get_jwt_identity() + helper_functions.abort_if_user_is_not_admin(user_email) + data = request.get_json() helper_functions.no_json_in_request(data) try: @@ -65,9 +69,15 @@ def post(self): class SaleAttendantsManagement(Resource): + + + @jwt_required def get(self): """GET /saleorder endpoint""" - + + user_email = get_jwt_identity() + helper_functions.abort_if_user_is_not_admin(user_email) + if not sale_orders.SALE_ORDERS: # If no sale orders exist in the store yet abort(make_response( diff --git a/app/api/v1/resources/auth.py b/app/api/v1/resources/auth.py new file mode 100644 index 0000000..68c3505 --- /dev/null +++ b/app/api/v1/resources/auth.py @@ -0,0 +1,50 @@ +import datetime +import jwt +from werkzeug.security import generate_password_hash, check_password_hash +from functools import wraps + +from flask import Flask, jsonify, request, make_response +from flask_restful import Resource +from flask_jwt_extended import ( + jwt_required, create_access_token, + get_jwt_identity +) + +from instance import config +from app.api.v1.utils.validator import Validator +from app.api.v1.models import users + +class SignUp(Resource): + def post(self): + data = request.get_json() + if not data: + return make_response(jsonify({ + "message": "Missing required credentials" + }), 400) + email = data["email"] + password = generate_password_hash(data["password"], method='sha256') + role = data["role"] + Validator.validate_credentials(self, data) + user = users.User_Model(email, password, role) + res = user.save() + return make_response(res, 202) + + +class Login(Resource): + def post(self): + data = request.get_json() + if not data: + return make_response(jsonify({ + "message": "Kindly enter your credentials", + }), 400) + email = data["email"] + password = data["password"] + for user in users.USERS: + if email == user["email"] and check_password_hash(user["password"], password): + expires = datetime.timedelta(minutes=30) + token = create_access_token(identity=email, expires_delta=expires) + return {'token': token, "message": "You are successfully logged in"}, 200 + + return make_response(jsonify({ + "message": "Wrong credentials provided" + }), 403) diff --git a/app/api/v1/resources/general_users_endpoints.py b/app/api/v1/resources/general_users_endpoints.py index 427a43b..64c6415 100644 --- a/app/api/v1/resources/general_users_endpoints.py +++ b/app/api/v1/resources/general_users_endpoints.py @@ -4,6 +4,7 @@ """ from flask import jsonify, abort, make_response from flask_restful import Resource +from flask_jwt_extended import get_jwt_identity, jwt_required from . import helper_functions from app.api.v1.models import products @@ -14,7 +15,6 @@ class AllProducts(Resource): the admin and the normal user endpoints """ - def get(self): """GET /products endpoint""" @@ -41,6 +41,7 @@ def get(self, product_id): class SpecificSaleOrder(Resource): + @jwt_required def get(self, sale_order_id): """GET /saleorder/""" diff --git a/app/api/v1/resources/helper_functions.py b/app/api/v1/resources/helper_functions.py index fad9749..637ff5a 100644 --- a/app/api/v1/resources/helper_functions.py +++ b/app/api/v1/resources/helper_functions.py @@ -1,7 +1,8 @@ from datetime import datetime + from flask import abort, jsonify, make_response -from app.api.v1.models import products, sale_orders +from app.api.v1.models import products, sale_orders, users def no_json_in_request(data): """Aborts if the data does @@ -23,7 +24,6 @@ def missing_a_required_parameter(): abort(make_response(jsonify( message="Bad request. Request missing a required argument"), 400)) - def add_new_product(name, price, category): """Creates a new product""" @@ -113,4 +113,11 @@ def add_new_sale_record(name, price, quantity, amount): else: # if any of the required parameters is missing or none missing_a_required_parameter() - return response \ No newline at end of file + return response + +def abort_if_user_is_not_admin(user): + user_role = [users['role'] for users in users.USERS if users['email'] == user][0] + if user_role!= "Admin": + abort(make_response(jsonify( + message="Unauthorized. This action is not for you" + ), 401)) \ No newline at end of file diff --git a/app/api/v1/resources/store_attendant_endpoints.py b/app/api/v1/resources/store_attendant_endpoints.py index 3a020cf..1fa013f 100644 --- a/app/api/v1/resources/store_attendant_endpoints.py +++ b/app/api/v1/resources/store_attendant_endpoints.py @@ -4,6 +4,7 @@ """ from flask import Flask, jsonify, request, abort, make_response from flask_restful import Resource +from flask_jwt_extended import get_jwt_identity, jwt_required from . import helper_functions from app.api.v1.models import products @@ -14,7 +15,7 @@ class SaleRecords(Resource): specific endpoints """ - + @jwt_required def post(self): """POST /saleorder endpoint""" diff --git a/app/api/v1/utils/validator.py b/app/api/v1/utils/validator.py new file mode 100644 index 0000000..cd0b486 --- /dev/null +++ b/app/api/v1/utils/validator.py @@ -0,0 +1,39 @@ +import re + +from flask import make_response, jsonify, abort +from validate_email import validate_email + +from app.api.v1.models import users + + +class Validator: + def validate_credentials(self, data): + self.email = data["email"] + self.password = data["password"] + self.role = data["role"] + valid_email = validate_email(self.email) + for user in users.USERS: + if self.email == user["email"]: + Message = "User already exists" + abort(406, Message) + if self.email == "" or self.password == "" or self.role == "": + Message = "You are missing a required credential" + abort(400, Message) + if not valid_email: + Message = "Invalid email" + abort(400, Message) + elif len(self.password) < 6 or len(self.password) > 12: + Message = "Password must be long than 6 characters or less than 12" + abort(400, Message) + elif not any(char.isdigit() for char in self.password): + Message = "Password must have a digit" + abort(400, Message) + elif not any(char.isupper() for char in self.password): + Message = "Password must have an upper case character" + abort(400, Message) + elif not any(char.islower() for char in self.password): + Message = "Password must have a lower case character" + abort(400, Message) + elif not re.search("^.*(?=.*[@#$%^&+=]).*$", self.password): + Message = "Password must have a special charater" + abort(400, Message) From f59d144bcd54006f7447da8f47d20ad401910147 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Sat, 20 Oct 2018 19:42:50 +0300 Subject: [PATCH 064/100] [Chore #161248446] Create users list --- app/api/v1/models/users.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 app/api/v1/models/users.py diff --git a/app/api/v1/models/users.py b/app/api/v1/models/users.py new file mode 100644 index 0000000..c8e1fcb --- /dev/null +++ b/app/api/v1/models/users.py @@ -0,0 +1,22 @@ +from flask import make_response, jsonify +USERS = [] + + +class User_Model(): + def __init__(self, email, password, role): + self.id = len(USERS) + 1 + self.email = email + self.password = password + self.role = role + + def save(self): + new_user = { + "id": self.id, + "email": self.email, + "password": self.password, + "role": self.role + } + + USERS.append(new_user) + response = jsonify(new_user) + return response \ No newline at end of file From 317ba8619ff741a2e5db973ca2e61ef3aeccd6ff Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Sat, 20 Oct 2018 19:44:02 +0300 Subject: [PATCH 065/100] [Feature #161362891] Create routes for /auth endpoint --- app/api/v1/views.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/api/v1/views.py b/app/api/v1/views.py index 1ada4c9..4c71a85 100644 --- a/app/api/v1/views.py +++ b/app/api/v1/views.py @@ -2,14 +2,19 @@ from flask_restful import Api, Resource -from . import v1_blueprint -from .resources import admin_endpoints, general_users_endpoints, store_attendant_endpoints +from . import endpoint_v1_blueprint, auth_v1_blueprint +from .resources import admin_endpoints, general_users_endpoints, store_attendant_endpoints, auth + +API = Api(endpoint_v1_blueprint) +AUTH_API = Api(auth_v1_blueprint) -API = Api(v1_blueprint) API.add_resource(admin_endpoints.ProductsManagement, '/products') API.add_resource(general_users_endpoints.AllProducts, '/products') API.add_resource(general_users_endpoints.SpecificProduct, '/products/') API.add_resource(store_attendant_endpoints.SaleRecords, '/saleorder') API.add_resource(admin_endpoints.SaleAttendantsManagement, '/saleorder') -API.add_resource(general_users_endpoints.SpecificSaleOrder, '/saleorder/') \ No newline at end of file +API.add_resource(general_users_endpoints.SpecificSaleOrder, '/saleorder/') + +AUTH_API.add_resource(auth.SignUp, '/signup') +AUTH_API.add_resource(auth.Login, '/login') \ No newline at end of file From a34dcfdb40b2b698cdcb69f7281a47ca54cf00b4 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Sat, 20 Oct 2018 23:35:53 +0300 Subject: [PATCH 066/100] [Chore #161209925] Remove jwt --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 805cb1f..5af5f19 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,6 @@ idna==2.7 isort==4.3.4 itsdangerous==0.24 Jinja2==2.10 -jwt==0.5.4 lazy-object-proxy==1.3.1 MarkupSafe==1.0 mccabe==0.6.1 From 1fb3b0c77883a0e4b633e7d5865d6519c8efa45e Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Mon, 22 Oct 2018 21:35:27 +0300 Subject: [PATCH 067/100] [Chore #161223919] Add documentation and credits Addition of more details such as the credits, author and the documentation link --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 46e0211..45aa55b 100644 --- a/README.md +++ b/README.md @@ -55,5 +55,15 @@ Store Manager is a web application that helps store owners manage sales and prod + #### Deployment [Heroku](https://store-manager-api.herokuapp.com/api/v1/products) + +#### Documentation +https://documenter.getpostman.com/view/5265531/RWguxwzy + +#### Author +[Caleb Rotich](https://github.com/calebrotich10) + +#### Credits +This application was build as part of the Andela NBO 33 challenge From 25fccbc2d0dba3574d54553246373c5045ba5cda Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 23 Oct 2018 10:13:24 +0300 Subject: [PATCH 068/100] [Chore #161248446] Create classes for the models Create classes and methods for the products and the sale_orders model --- app/api/v1/models/products.py | 20 +++++++++++++++++++- app/api/v1/models/sale_orders.py | 22 +++++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/app/api/v1/models/products.py b/app/api/v1/models/products.py index da894e9..9eb36d2 100644 --- a/app/api/v1/models/products.py +++ b/app/api/v1/models/products.py @@ -3,4 +3,22 @@ and data logic of the store's products """ -PRODUCTS = [] \ No newline at end of file +PRODUCTS = [] + +class Products(): + def __init__(self, product_name, product_price, category): + self.id = len(PRODUCTS) + 1 + self.product_name = product_name + self.product_price = product_price + self.category = category + + def save(self): + new_product = { + "product_id": self.id, + "product_name": self.product_name, + "product_price": self.product_price, + "category": self.category + } + + PRODUCTS.append(new_product) + return new_product \ No newline at end of file diff --git a/app/api/v1/models/sale_orders.py b/app/api/v1/models/sale_orders.py index 2b1cc76..ca259b0 100644 --- a/app/api/v1/models/sale_orders.py +++ b/app/api/v1/models/sale_orders.py @@ -2,5 +2,25 @@ and data logic of the store attendant's sale orders """ +from flask import jsonify -SALE_ORDERS = [] \ No newline at end of file +SALE_ORDERS = [] + +class SaleOrder(): + def __init__(self, product_name, product_price, quantity): + self.id = len(SALE_ORDERS) + 1 + self.product_name = product_name + self.product_price = product_price + self.quantity = quantity + + def save(self): + new_sale_order = { + "sale_order_id": self.id, + "product_name": self.product_name, + "product_price": self.product_price, + "quantity": self.quantity, + "amount": (self.product_price * self.quantity) + } + + SALE_ORDERS.append(new_sale_order) + return new_sale_order \ No newline at end of file From bf7896e48d2022e73533f6b611429195ba924218 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 23 Oct 2018 10:26:28 +0300 Subject: [PATCH 069/100] [Chore #161216309] Modify test command at .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9fafbf6..9f2fc29 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ before_script: # command to run tests script: - - coverage run --source=app.api.v1 -m pytest app/tests/v1 && coverage report + - coverage run --source=app.api.v1 -m pytest app/tests/v1 -v -W error::UserWarning && coverage report after_script: - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT From f115f86727d2be068a4fbfa17ecc3eba0238f7c8 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 23 Oct 2018 10:45:54 +0300 Subject: [PATCH 070/100] [Chore #161408153] Implement OOP for CRUD actions Convert from procedural flow to an OOP flow. Change in authentication aspects. --- app/api/v1/resources/admin_endpoints.py | 42 +++++---- .../v1/resources/general_users_endpoints.py | 50 ++++++++--- app/api/v1/resources/helper_functions.py | 90 ------------------- .../v1/resources/store_attendant_endpoints.py | 19 ++-- 4 files changed, 77 insertions(+), 124 deletions(-) diff --git a/app/api/v1/resources/admin_endpoints.py b/app/api/v1/resources/admin_endpoints.py index 579dfc6..305c346 100644 --- a/app/api/v1/resources/admin_endpoints.py +++ b/app/api/v1/resources/admin_endpoints.py @@ -2,25 +2,28 @@ that are specific to the admin """ +import os from flask import Flask, jsonify, request, abort, make_response from flask_restful import Resource from flask_jwt_extended import get_jwt_identity, jwt_required +from functools import wraps +import jwt from . import helper_functions from app.api.v1.models import products, sale_orders, users +from . import verify + class ProductsManagement(Resource): """Class contains the tests for admin specific endpoints """ - - @jwt_required def post(self): """POST /products endpoint""" - - user_email = get_jwt_identity() - helper_functions.abort_if_user_is_not_admin(user_email) + + logged_user = verify.verify_tokens() + helper_functions.abort_if_user_is_not_admin(logged_user) data = request.get_json() helper_functions.no_json_in_request(data) @@ -60,29 +63,38 @@ def post(self): except IndexError: # If there is no product with the same name - response = helper_functions.add_new_product(product_name, product_price, category) + added_product = products.Products(product_name, product_price, category) + response = added_product.save() else: # If there are no products in the store - response = helper_functions.add_new_product(product_name, product_price, category) + added_product = products.Products(product_name, product_price, category) + response = added_product.save() - return response + return make_response(jsonify({ + "message": "Product added successfully", + "product": response + }), 201) class SaleAttendantsManagement(Resource): + """Class contains the tests managing + sale orders + """ - @jwt_required def get(self): """GET /saleorder endpoint""" - user_email = get_jwt_identity() - helper_functions.abort_if_user_is_not_admin(user_email) - + verify.verify_tokens() + if not sale_orders.SALE_ORDERS: - # If no sale orders exist in the store yet + # If no products exist in the store yet abort(make_response( jsonify(message="There are no sale orders made yet"), 404)) - # If at least one sale order exists - response = jsonify({'sale_orders': sale_orders.SALE_ORDERS}) + # if at least one product exists + response = jsonify({ + 'message': "Successfully fetched all the sale orders", + 'sale_orders': sale_orders.SALE_ORDERS + }) response.status_code = 200 return response diff --git a/app/api/v1/resources/general_users_endpoints.py b/app/api/v1/resources/general_users_endpoints.py index 64c6415..8a47821 100644 --- a/app/api/v1/resources/general_users_endpoints.py +++ b/app/api/v1/resources/general_users_endpoints.py @@ -5,9 +5,10 @@ from flask import jsonify, abort, make_response from flask_restful import Resource from flask_jwt_extended import get_jwt_identity, jwt_required +from . import verify from . import helper_functions -from app.api.v1.models import products +from app.api.v1.models import products, sale_orders class AllProducts(Resource): """Class contains the tests for both @@ -17,34 +18,57 @@ class AllProducts(Resource): def get(self): """GET /products endpoint""" - + verify.verify_tokens() if not products.PRODUCTS: # If no products exist in the store yet abort(make_response( jsonify(message="There are no products in the store yet"), 404)) # if at least one product exists - response = jsonify({'products': products.PRODUCTS}) + response = jsonify({ + 'message': "Successfully fetched all the products", + 'products': products.PRODUCTS + }) response.status_code = 200 return response class SpecificProduct(Resource): def get(self, product_id): - """GET /products/""" - - product = helper_functions.retrieve_specific_product(product_id) - response = jsonify(product) - response.status_code = 200 - return response + verify.verify_tokens() + + for product in products.PRODUCTS: + if product["product_id"] == product_id: + return make_response(jsonify({ + "message": "{} retrieved successfully".format(product["product_name"]), + "product": product["product_name"] + } + ), 200) + + else: + return make_response(jsonify({ + "message": "Product with id {} not found".format(product_id) + } + ), 404) class SpecificSaleOrder(Resource): - @jwt_required def get(self, sale_order_id): """GET /saleorder/""" - sale_order = helper_functions.retrieve_specific_sale_order(sale_order_id) - response = jsonify(sale_order) - response.status_code = 200 \ No newline at end of file + verify.verify_tokens() + + for sale_order in sale_orders.SALE_ORDERS: + if sale_order["sale_order_id"] == sale_order_id: + return make_response(jsonify({ + "message": "{} retrieved successfully".format(sale_order["product_name"]), + "product": sale_order["product_name"] + } + ), 200) + + else: + return make_response(jsonify({ + "message": "Sale Order with id {} not found".format(sale_order_id) + } + ), 404) \ No newline at end of file diff --git a/app/api/v1/resources/helper_functions.py b/app/api/v1/resources/helper_functions.py index 637ff5a..c79ce4b 100644 --- a/app/api/v1/resources/helper_functions.py +++ b/app/api/v1/resources/helper_functions.py @@ -24,96 +24,6 @@ def missing_a_required_parameter(): abort(make_response(jsonify( message="Bad request. Request missing a required argument"), 400)) -def add_new_product(name, price, category): - """Creates a new product""" - - if name and price and category: - # If all the required parameters are available - product_id = len(products.PRODUCTS) + 1 - product = { - 'product_id': product_id, - 'product_name': name, - 'product_price': price, - 'category': category, - 'date_added': datetime.now().strftime('%Y-%m-%d %H:%M:%S') - } - - products.PRODUCTS.append(product) - response = jsonify({ - "message": "Product added successfully", - "product": products.PRODUCTS[-1]}) - response.status_code = 201 - else: - # if any of the required parameterss is missing or none - missing_a_required_parameter() - return response - - - -def retrieve_specific_product(product_id): - """ - Helper method to fetch product from list of products, - given valid id - """ - specific_product = None - for product in products.PRODUCTS: - if product['product_id'] == product_id: - specific_product = product - break - if not specific_product: - abort_if_item_is_not_found(product_id) - - return specific_product - -def retrieve_specific_sale_order(sale_id): - """ - Helper method to fetch sale order from list of sale orders, - given a valid id - """ - specific_sale_order = None - for sale_order in sale_orders.SALE_ORDERS: - if sale_order['sale_order_id'] == sale_id: - specific_sale_order = sale_order - break - if not specific_sale_order: - abort_if_item_is_not_found(sale_id) - - return specific_sale_order - -def abort_if_item_is_not_found(item_id): - """ - Helper method to search for Product - Abort if product not found and throw error - """ - abort(make_response(jsonify( - message="Item with id {} not found".format(item_id)), 404)) - - -def add_new_sale_record(name, price, quantity, amount): - - """Creates a new sale record""" - - if name and price and quantity and amount: - # If all the required parameters are available - sale_order_id = len(sale_orders.SALE_ORDERS) + 1 - sale_order = { - 'sale_order_id': sale_order_id, - 'product_name': name, - 'product_price': price, - 'quantity': quantity, - 'amount': amount, - 'date_added': datetime.now().strftime('%Y-%m-%d %H:%M:%S') - } - - sale_orders.SALE_ORDERS.append(sale_order) - response = jsonify({ - "message": "Sale record added successfully", - "sale_order": sale_orders.SALE_ORDERS[-1]}) - response.status_code = 201 - else: - # if any of the required parameters is missing or none - missing_a_required_parameter() - return response def abort_if_user_is_not_admin(user): user_role = [users['role'] for users in users.USERS if users['email'] == user][0] diff --git a/app/api/v1/resources/store_attendant_endpoints.py b/app/api/v1/resources/store_attendant_endpoints.py index 1fa013f..ae2be35 100644 --- a/app/api/v1/resources/store_attendant_endpoints.py +++ b/app/api/v1/resources/store_attendant_endpoints.py @@ -7,7 +7,8 @@ from flask_jwt_extended import get_jwt_identity, jwt_required from . import helper_functions -from app.api.v1.models import products +from app.api.v1.models import products, sale_orders +from . import verify class SaleRecords(Resource): """Class contains the tests for store attendant @@ -15,17 +16,17 @@ class SaleRecords(Resource): specific endpoints """ - @jwt_required def post(self): """POST /saleorder endpoint""" - + + verify.verify_tokens() + data = request.get_json() helper_functions.no_json_in_request(data) try: product_name = data['product_name'] product_price = data['product_price'] quantity = data['quantity'] - amount = (product_price * quantity) except KeyError: # If product is missing required parameter helper_functions.missing_a_required_parameter() @@ -51,6 +52,12 @@ def post(self): message="Bad request. The quantity should be specified in digits" ), 400)) - response = helper_functions.add_new_sale_record(product_name, product_price, quantity, amount) + # response = helper_functions.add_new_sale_record(product_name, product_price, quantity, amount) + sale_order = sale_orders.SaleOrder(product_name, product_price, quantity) + response = sale_order.save() + - return response + return make_response(jsonify({ + "message": "Checkout complete", + "saleorder": response + }), 201) From 15ba00d88b2c26126bd98212a759687dd47b4931 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 23 Oct 2018 10:47:33 +0300 Subject: [PATCH 071/100] [Fixes bug #161408196] Fix auth token decode and encode --- app/api/v1/resources/auth.py | 24 ++++++++++++++++-------- app/api/v1/resources/verify.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 8 deletions(-) create mode 100644 app/api/v1/resources/verify.py diff --git a/app/api/v1/resources/auth.py b/app/api/v1/resources/auth.py index 68c3505..dd19ecd 100644 --- a/app/api/v1/resources/auth.py +++ b/app/api/v1/resources/auth.py @@ -1,3 +1,4 @@ +import os import datetime import jwt from werkzeug.security import generate_password_hash, check_password_hash @@ -35,16 +36,23 @@ def post(self): data = request.get_json() if not data: return make_response(jsonify({ - "message": "Kindly enter your credentials", - }), 400) + "message": "Kindly enter your credentials" + } + ), 400) email = data["email"] password = data["password"] + for user in users.USERS: if email == user["email"] and check_password_hash(user["password"], password): - expires = datetime.timedelta(minutes=30) - token = create_access_token(identity=email, expires_delta=expires) - return {'token': token, "message": "You are successfully logged in"}, 200 - + token = jwt.encode({ + "email": email, + "exp": datetime.datetime.utcnow() + datetime.timedelta + (minutes=5) + }, os.getenv('JWT_SECRET_KEY', default='SdaHv342nx!jknr837bjwd?c,lsajjjhw673hdsbgeh')) + return make_response(jsonify({ + "message": "You are successfully logged in", + "token": token.decode("UTF-8")}), 200) return make_response(jsonify({ - "message": "Wrong credentials provided" - }), 403) + "message": "Wrong credentials provided" + } + ), 403) diff --git a/app/api/v1/resources/verify.py b/app/api/v1/resources/verify.py new file mode 100644 index 0000000..8b0e3f6 --- /dev/null +++ b/app/api/v1/resources/verify.py @@ -0,0 +1,28 @@ +import os +import jwt +from functools import wraps + +from flask import request, make_response, jsonify, abort +from ..models import users + + +def verify_tokens(): + token = None + if 'Authorization' in request.headers: + token = request.headers['Authorization'] + if not token: + abort(make_response(jsonify({ + "Message": "You need to login"}), 401)) + try: + data = jwt.decode(token, os.getenv('JWT_SECRET_KEY', default='SdaHv342nx!jknr837bjwd?c,lsajjjhw673hdsbgeh')) + for user in users.USERS: + if user['email'] == data['email']: + logged_user = user + + except: + print(os.getenv('JWT_SECRET_KEY', default='SdaHv342nx!jknr837bjwd?c,lsajjjhw673hdsbgeh')) + abort(make_response(jsonify({ + "Message": "This token is invalid" + }), 403)) + + return logged_user["email"] \ No newline at end of file From c6a0ee0932b90e47fbb5d851c1951233b05fdbe1 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 23 Oct 2018 10:50:22 +0300 Subject: [PATCH 072/100] [Chore #161201973] Refactor tests Modify tests to comply with the change in token decode and encode. Add a test case to increase coverage. --- app/tests/v1/test_admin_endpoints.py | 18 +++--- app/tests/v1/test_general_user_endpoints.py | 58 +++++++++++++++++-- .../v1/test_store_attendant_endpoints.py | 22 +++---- 3 files changed, 72 insertions(+), 26 deletions(-) diff --git a/app/tests/v1/test_admin_endpoints.py b/app/tests/v1/test_admin_endpoints.py index 50de744..f0c8ad1 100644 --- a/app/tests/v1/test_admin_endpoints.py +++ b/app/tests/v1/test_admin_endpoints.py @@ -20,7 +20,7 @@ def test_add_new_product(self): # send a dummy data response for testing response = self.app_test_client.post('{}/products'.format( - self.BASE_URL), json=self.PRODUCT, headers=dict(Authorization="Bearer " + token), + self.BASE_URL), json=self.PRODUCT, headers=dict(Authorization=token), content_type='application/json') self.assertEqual(response.status_code, 201) @@ -44,7 +44,7 @@ def test_add_new_product_parameter_missing(self): token = self.login_test_admin() response = self.app_test_client.post('{}/products'.format( - self.BASE_URL), json={'product_name': 'Nyundo'}, headers=dict(Authorization="Bearer " + token), + self.BASE_URL), json={'product_name': 'Nyundo'}, headers=dict(Authorization=token), content_type='application/json') self.assertEqual(response.status_code, 400) @@ -62,7 +62,7 @@ def test_add_new_product_price_under_one(self): response = self.app_test_client.post('{}/products'.format( self.BASE_URL), json={ 'product_id': 1, 'product_name': "Hammer", 'product_price': 0, 'category':'Tools' - }, headers=dict(Authorization="Bearer " + token), + }, headers=dict(Authorization=token), content_type='application/json') self.assertEqual(response.status_code, 400) @@ -82,7 +82,7 @@ def test_add_new_product_with_product_name_not_string(self): response = self.app_test_client.post('{}/products'.format( self.BASE_URL), json={ 'product_id': 1, 'product_name': 200, 'product_price': 200, 'category':'Tools' - }, headers=dict(Authorization="Bearer " + token), + }, headers=dict(Authorization=token), content_type='application/json') self.assertEqual(response.status_code, 400) @@ -101,7 +101,7 @@ def test_add_new_product_with_category_not_string(self): response = self.app_test_client.post('{}/products'.format( self.BASE_URL), json={ 'product_id': 1, 'product_name': "Hammer", 'product_price': 200, 'category': 200 - }, headers=dict(Authorization="Bearer " + token), + }, headers=dict(Authorization=token), content_type='application/json') self.assertEqual(response.status_code, 400) @@ -120,13 +120,13 @@ def test_add_new_product_with_product_name_already_existing(self): self.app_test_client.post('{}/products'.format( self.BASE_URL), json={ 'product_id': 1, 'product_name': "Hammer", 'product_price': 200, 'category': "Tools" - }, headers=dict(Authorization="Bearer " + token), + }, headers=dict(Authorization=token), content_type='application/json') response = self.app_test_client.post('{}/products'.format( self.BASE_URL), json={ 'product_id': 1, 'product_name': "Hammer", 'product_price': 200, 'category': "Tools" - }, headers=dict(Authorization="Bearer " + token), + }, headers=dict(Authorization=token), content_type='application/json') self.assertEqual(response.status_code, 400) @@ -148,13 +148,13 @@ def test_fetch_sale_orders(self): 'quantity': 1, 'amount': 20 }, - headers=dict(Authorization="Bearer " + token), + headers=dict(Authorization=token), content_type='application/json') response = self.app_test_client.get( '{}/saleorder'.format(self.BASE_URL), - headers=dict(Authorization="Bearer " + token), + headers=dict(Authorization=token), content_type='application/json' ) diff --git a/app/tests/v1/test_general_user_endpoints.py b/app/tests/v1/test_general_user_endpoints.py index 2ce1d26..e85082d 100644 --- a/app/tests/v1/test_general_user_endpoints.py +++ b/app/tests/v1/test_general_user_endpoints.py @@ -18,8 +18,14 @@ class TestGeneralUsersEndpoints(base_test.TestBaseClass): def test_retrieve_all_products(self): """Test GET /products - when products exist""" + self.register_test_admin_account() + token = self.login_test_admin() + response = self.app_test_client.get( - '{}/products'.format(self.BASE_URL)) + '{}/products'.format(self.BASE_URL), + headers=dict(Authorization=token), + content_type='application/json' + ) self.assertEqual(response.status_code, 200) self.assertEqual(helper_functions.convert_response_to_json( @@ -27,13 +33,19 @@ def test_retrieve_all_products(self): def test_retrieve_specific_product(self): """Test GET /products/id - when product exist""" - + + self.register_test_admin_account() + token = self.login_test_admin() + response = self.app_test_client.get( - '{}/products/1'.format(self.BASE_URL)) + '{}/products/1'.format(self.BASE_URL), + headers=dict(Authorization=token), + content_type='application/json' + ) self.assertEqual(response.status_code, 200) self.assertEqual(helper_functions.convert_response_to_json( - response)['product_name'], self.PRODUCT['product_name']) + response)['product'], self.PRODUCT['product_name']) def test_retrieve_specific_sale_order(self): """Test GET /saleorder/id - when saleorder exists""" @@ -49,13 +61,47 @@ def test_retrieve_specific_sale_order(self): 'quantity': 1, 'amount': 20 }, - headers=dict(Authorization="Bearer " + token), + headers=dict(Authorization=token), content_type='application/json') response = self.app_test_client.get( '{}/saleorder/1'.format(self.BASE_URL), - headers=dict(Authorization="Bearer " + token), + headers=dict(Authorization=token), content_type='application/json' ) self.assertEqual(response.status_code, 200) + + + def test_missing_token(self): + """Test GET /products - when token is missing""" + + self.register_test_admin_account() + token = "" + + response = self.app_test_client.get( + '{}/products'.format(self.BASE_URL), + headers=dict(Authorization=token), + content_type='application/json' + ) + + self.assertEqual(response.status_code, 401) + self.assertEqual(helper_functions.convert_response_to_json( + response)["Message"], "You need to login") + + + def test_invalid_token(self): + """Test GET /products - when token is missing""" + + self.register_test_admin_account() + token = "sample_invalid-token-afskdghkfhwkedaf-ksfakjfwey" + + response = self.app_test_client.get( + '{}/products'.format(self.BASE_URL), + headers=dict(Authorization=token), + content_type='application/json' + ) + + self.assertEqual(response.status_code, 403) + self.assertEqual(helper_functions.convert_response_to_json( + response)["Message"], "This token is invalid") \ No newline at end of file diff --git a/app/tests/v1/test_store_attendant_endpoints.py b/app/tests/v1/test_store_attendant_endpoints.py index 56791ac..c78d9b1 100644 --- a/app/tests/v1/test_store_attendant_endpoints.py +++ b/app/tests/v1/test_store_attendant_endpoints.py @@ -18,20 +18,20 @@ def test_create_sale_order(self): # send a dummy data response for testing response = self.app_test_client.post('{}/saleorder'.format( - self.BASE_URL), json=self.SALE_ORDERS, headers=dict(Authorization="Bearer " + token), + self.BASE_URL), json=self.SALE_ORDERS, headers=dict(Authorization=token), content_type='application/json') self.assertEqual(response.status_code, 201) self.assertEqual(helper_functions.convert_response_to_json( - response)['sale_order']['product_name'], self.SALE_ORDERS['product_name']) + response)['saleorder']['product_name'], self.SALE_ORDERS['product_name']) self.assertEqual(helper_functions.convert_response_to_json( - response)['sale_order']['product_price'], self.SALE_ORDERS['product_price']) + response)['saleorder']['product_price'], self.SALE_ORDERS['product_price']) self.assertEqual(helper_functions.convert_response_to_json( - response)['sale_order']['quantity'], self.SALE_ORDERS['quantity']) + response)['saleorder']['quantity'], self.SALE_ORDERS['quantity']) self.assertEqual(helper_functions.convert_response_to_json( - response)['sale_order']['amount'], self.SALE_ORDERS['amount']) + response)['saleorder']['amount'], self.SALE_ORDERS['amount']) self.assertEqual(helper_functions.convert_response_to_json( - response)['message'], 'Sale record added successfully') + response)['message'], 'Checkout complete') def test_create_sale_order_parameter_missing(self): @@ -43,7 +43,7 @@ def test_create_sale_order_parameter_missing(self): token = self.login_test_admin() response = self.app_test_client.post('{}/saleorder'.format( - self.BASE_URL), json={'product_name': 'Nyundo'}, headers=dict(Authorization="Bearer " + token), + self.BASE_URL), json={'product_name': 'Nyundo'}, headers=dict(Authorization=token), content_type='application/json') self.assertEqual(response.status_code, 400) @@ -61,7 +61,7 @@ def test_create_sale_order_price_below_one(self): response = self.app_test_client.post('{}/saleorder'.format( self.BASE_URL), json={'product_name': 'Nyundo', 'product_price': -1, 'quantity': 1}, - headers=dict(Authorization="Bearer " + token), + headers=dict(Authorization=token), content_type='application/json') self.assertEqual(response.status_code, 400) @@ -79,7 +79,7 @@ def test_create_sale_order_invalid_product_name(self): response = self.app_test_client.post('{}/saleorder'.format( self.BASE_URL), json={'product_name': 3, 'product_price': 300, 'quantity': 1}, - headers=dict(Authorization="Bearer " + token), + headers=dict(Authorization=token), content_type='application/json') self.assertEqual(response.status_code, 400) @@ -98,7 +98,7 @@ def test_create_sale_order_price_not_digits(self): response = self.app_test_client.post('{}/saleorder'.format( self.BASE_URL), json={'product_name': "Nyundo", 'product_price': "300", 'quantity': 1}, - headers=dict(Authorization="Bearer " + token), + headers=dict(Authorization=token), content_type='application/json') self.assertEqual(response.status_code, 400) @@ -116,7 +116,7 @@ def test_create_sale_order_invalid_quantity(self): response = self.app_test_client.post('{}/saleorder'.format( self.BASE_URL), json={'product_name': "Nyundo", 'product_price': 300, 'quantity': "1"}, - headers=dict(Authorization="Bearer " + token), + headers=dict(Authorization=token), content_type='application/json') self.assertEqual(response.status_code, 400) From 2da39e19d79c83512181356c751469d1f0cc6aa4 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 23 Oct 2018 11:56:37 +0300 Subject: [PATCH 073/100] [Chore #161223919] Add app testing information Technologies used, running tests and the installation process added to the application --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 45aa55b..6541db5 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,23 @@ Store Manager is a web application that helps store owners manage sales and prod +#### Installing the application +1. Open a command terminal in your preferred folder +2. Run command `git clone https://github.com/calebrotich10/store-manager-api.git` to have a copy locally +3. `cd store-manager-api` +4. Create a virtual environment for the application `virtualenv venv` +5. Install dependencies from the `requirements.txt` file `pip3 install -r requirements.txt` +6. Export environment variables to your environment ```export JWT_SECRET_KEY=your-secret-key```, ```export FLASK_APP="run.py"``` +6. Run the application using flask command `flask run` or using python3 `python3 run.py` + +#### Running tests +Inside the virtual environment created above, run command: `coverage run --source=app.api.v1 -m pytest app/tests/v1 -v -W error::UserWarning && coverage report` + +#### Technologies used +1. `JWT` for authentication +2. `pytest` for running tests +3. Python based framework `flask` +4. Flask packages #### Deployment [Heroku](https://store-manager-api.herokuapp.com/api/v1/products) From ab06317b2cb272c8eb99f95c0e67697ab95f8ee5 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 23 Oct 2018 15:39:25 +0300 Subject: [PATCH 074/100] [Chore #161414025] Refactor code to meet standards Reduce the number of lines in s single functionality to the accepted number, provide better responses to requests --- app/api/v1/resources/admin_endpoints.py | 41 ++------------ .../v1/resources/general_users_endpoints.py | 4 +- app/api/v1/resources/helper_functions.py | 26 ++++++++- app/api/v1/resources/verify.py | 55 ++++++++++++------- app/tests/v1/test_general_user_endpoints.py | 2 +- 5 files changed, 70 insertions(+), 58 deletions(-) diff --git a/app/api/v1/resources/admin_endpoints.py b/app/api/v1/resources/admin_endpoints.py index 305c346..b551a8a 100644 --- a/app/api/v1/resources/admin_endpoints.py +++ b/app/api/v1/resources/admin_endpoints.py @@ -3,11 +3,12 @@ that are specific to the admin """ import os +import jwt +from functools import wraps + from flask import Flask, jsonify, request, abort, make_response from flask_restful import Resource from flask_jwt_extended import get_jwt_identity, jwt_required -from functools import wraps -import jwt from . import helper_functions from app.api.v1.models import products, sale_orders, users @@ -22,6 +23,7 @@ class ProductsManagement(Resource): def post(self): """POST /products endpoint""" + # Token verification and admin user determination logged_user = verify.verify_tokens() helper_functions.abort_if_user_is_not_admin(logged_user) @@ -35,40 +37,9 @@ def post(self): # If product is missing required parameter helper_functions.missing_a_required_parameter() - if product_price < 1: - abort(make_response(jsonify( - message="Bad request. Price of the product should be a positive integer above 0." - ), 400)) - - if not isinstance(product_name, str): - abort(make_response(jsonify( - message="Bad request. Product name should be a string" - ), 400)) - - if not isinstance(category, str): - abort(make_response(jsonify( - message="Bad request. The Category should be a string" - ), 400)) - - if products.PRODUCTS: - # If products are in the store already - try: - # Check if a product with a similar name exists - existing_product = [ - product for product in products.PRODUCTS if product['product_name'] == product_name][0] - - abort(make_response(jsonify({ - "message": "Product with a similar name already exists", - "product": existing_product}), 400)) + verify.verify_post_product_fields(product_price, product_name, category) - except IndexError: - # If there is no product with the same name - added_product = products.Products(product_name, product_price, category) - response = added_product.save() - else: - # If there are no products in the store - added_product = products.Products(product_name, product_price, category) - response = added_product.save() + response = helper_functions.add_product_to_store(product_name, product_price, category) return make_response(jsonify({ "message": "Product added successfully", diff --git a/app/api/v1/resources/general_users_endpoints.py b/app/api/v1/resources/general_users_endpoints.py index 8a47821..55957bf 100644 --- a/app/api/v1/resources/general_users_endpoints.py +++ b/app/api/v1/resources/general_users_endpoints.py @@ -62,8 +62,8 @@ def get(self, sale_order_id): for sale_order in sale_orders.SALE_ORDERS: if sale_order["sale_order_id"] == sale_order_id: return make_response(jsonify({ - "message": "{} retrieved successfully".format(sale_order["product_name"]), - "product": sale_order["product_name"] + "message": "Sale Order with Id {} retrieved successfully".format(sale_order["sale_order_id"]), + "product": sale_order } ), 200) diff --git a/app/api/v1/resources/helper_functions.py b/app/api/v1/resources/helper_functions.py index c79ce4b..b39e175 100644 --- a/app/api/v1/resources/helper_functions.py +++ b/app/api/v1/resources/helper_functions.py @@ -30,4 +30,28 @@ def abort_if_user_is_not_admin(user): if user_role!= "Admin": abort(make_response(jsonify( message="Unauthorized. This action is not for you" - ), 401)) \ No newline at end of file + ), 401)) + + +def add_product_to_store(product_name, product_price, category): + if products.PRODUCTS: + # If products are in the store already + try: + # Check if a product with a similar name exists + existing_product = [ + product for product in products.PRODUCTS if product['product_name'] == product_name][0] + + abort(make_response(jsonify({ + "message": "Product with a similar name already exists", + "product": existing_product}), 400)) + + except IndexError: + # If there is no product with the same name + added_product = products.Products(product_name, product_price, category) + response = added_product.save() + else: + # If there are no products in the store + added_product = products.Products(product_name, product_price, category) + response = added_product.save() + + return response \ No newline at end of file diff --git a/app/api/v1/resources/verify.py b/app/api/v1/resources/verify.py index 8b0e3f6..f9e21c3 100644 --- a/app/api/v1/resources/verify.py +++ b/app/api/v1/resources/verify.py @@ -7,22 +7,39 @@ def verify_tokens(): - token = None - if 'Authorization' in request.headers: - token = request.headers['Authorization'] - if not token: - abort(make_response(jsonify({ - "Message": "You need to login"}), 401)) - try: - data = jwt.decode(token, os.getenv('JWT_SECRET_KEY', default='SdaHv342nx!jknr837bjwd?c,lsajjjhw673hdsbgeh')) - for user in users.USERS: - if user['email'] == data['email']: - logged_user = user - - except: - print(os.getenv('JWT_SECRET_KEY', default='SdaHv342nx!jknr837bjwd?c,lsajjjhw673hdsbgeh')) - abort(make_response(jsonify({ - "Message": "This token is invalid" - }), 403)) - - return logged_user["email"] \ No newline at end of file + token = None + if 'Authorization' in request.headers: + token = request.headers['Authorization'] + if not token: + abort(make_response(jsonify({ + "Message": "You need to login"}), 401)) + try: + data = jwt.decode(token, os.getenv('JWT_SECRET_KEY', default='SdaHv342nx!jknr837bjwd?c,lsajjjhw673hdsbgeh')) + for user in users.USERS: + if user['email'] == data['email']: + logged_user = user + + except: + print(os.getenv('JWT_SECRET_KEY', default='SdaHv342nx!jknr837bjwd?c,lsajjjhw673hdsbgeh')) + abort(make_response(jsonify({ + "Message": "The token is either expired or wrong" + }), 403)) + + return logged_user["email"] + + +def verify_post_product_fields(product_price, product_name, category): + if product_price < 1: + abort(make_response(jsonify( + message="Bad request. Price of the product should be a positive integer above 0." + ), 400)) + + if not isinstance(product_name, str): + abort(make_response(jsonify( + message="Bad request. Product name should be a string" + ), 400)) + + if not isinstance(category, str): + abort(make_response(jsonify( + message="Bad request. The Category should be a string" + ), 400)) \ No newline at end of file diff --git a/app/tests/v1/test_general_user_endpoints.py b/app/tests/v1/test_general_user_endpoints.py index e85082d..1fe9e8e 100644 --- a/app/tests/v1/test_general_user_endpoints.py +++ b/app/tests/v1/test_general_user_endpoints.py @@ -104,4 +104,4 @@ def test_invalid_token(self): self.assertEqual(response.status_code, 403) self.assertEqual(helper_functions.convert_response_to_json( - response)["Message"], "This token is invalid") \ No newline at end of file + response)["Message"], "The token is either expired or wrong") \ No newline at end of file From 7c742a87d3b7445ae3c1b9270bfe70714635f139 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 23 Oct 2018 16:21:30 +0300 Subject: [PATCH 075/100] [Fixes bug #161415191] Varible ref before assignement --- app/api/v1/resources/admin_endpoints.py | 2 +- app/api/v1/resources/verify.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/api/v1/resources/admin_endpoints.py b/app/api/v1/resources/admin_endpoints.py index b551a8a..99560d7 100644 --- a/app/api/v1/resources/admin_endpoints.py +++ b/app/api/v1/resources/admin_endpoints.py @@ -25,7 +25,7 @@ def post(self): # Token verification and admin user determination logged_user = verify.verify_tokens() - helper_functions.abort_if_user_is_not_admin(logged_user) + helper_functions.abort_if_user_is_not_admin(logged_user) data = request.get_json() helper_functions.no_json_in_request(data) diff --git a/app/api/v1/resources/verify.py b/app/api/v1/resources/verify.py index f9e21c3..691e418 100644 --- a/app/api/v1/resources/verify.py +++ b/app/api/v1/resources/verify.py @@ -17,15 +17,13 @@ def verify_tokens(): data = jwt.decode(token, os.getenv('JWT_SECRET_KEY', default='SdaHv342nx!jknr837bjwd?c,lsajjjhw673hdsbgeh')) for user in users.USERS: if user['email'] == data['email']: - logged_user = user + return user["email"] except: - print(os.getenv('JWT_SECRET_KEY', default='SdaHv342nx!jknr837bjwd?c,lsajjjhw673hdsbgeh')) abort(make_response(jsonify({ "Message": "The token is either expired or wrong" - }), 403)) + }), 403)) - return logged_user["email"] def verify_post_product_fields(product_price, product_name, category): From 25b10ee2650bfd41464a450b6a784007bbfb9d39 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 23 Oct 2018 21:56:18 +0300 Subject: [PATCH 076/100] [Chore #161414025] Remove unnecessary code block Unnecessary else block causing inconsistency. --- app/api/v1/resources/general_users_endpoints.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/api/v1/resources/general_users_endpoints.py b/app/api/v1/resources/general_users_endpoints.py index 55957bf..38f63fc 100644 --- a/app/api/v1/resources/general_users_endpoints.py +++ b/app/api/v1/resources/general_users_endpoints.py @@ -67,8 +67,7 @@ def get(self, sale_order_id): } ), 200) - else: - return make_response(jsonify({ - "message": "Sale Order with id {} not found".format(sale_order_id) - } - ), 404) \ No newline at end of file + return make_response(jsonify({ + "message": "Sale Order with id {} not found".format(sale_order_id) + } + ), 404) \ No newline at end of file From 9644b84544825457f0f2367fd96a0468d3618117 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 23 Oct 2018 22:55:22 +0300 Subject: [PATCH 077/100] [Chore #161414025] Strip spaces from request data Remove leading and trailing spaces from the responses --- app/api/v1/utils/validator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/api/v1/utils/validator.py b/app/api/v1/utils/validator.py index cd0b486..af8acaf 100644 --- a/app/api/v1/utils/validator.py +++ b/app/api/v1/utils/validator.py @@ -8,9 +8,9 @@ class Validator: def validate_credentials(self, data): - self.email = data["email"] - self.password = data["password"] - self.role = data["role"] + self.email = data["email"].strip() + self.password = data["password"].strip() + self.role = data["role"].strip() valid_email = validate_email(self.email) for user in users.USERS: if self.email == user["email"]: From e3c8174ff5862ef8d7dad3c33f3adb8d466e4368 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Tue, 23 Oct 2018 23:48:49 +0300 Subject: [PATCH 078/100] [Chore #161414025] Remove unnecessary else block causing inconsistency --- app/api/v1/resources/general_users_endpoints.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/api/v1/resources/general_users_endpoints.py b/app/api/v1/resources/general_users_endpoints.py index 55957bf..38f63fc 100644 --- a/app/api/v1/resources/general_users_endpoints.py +++ b/app/api/v1/resources/general_users_endpoints.py @@ -67,8 +67,7 @@ def get(self, sale_order_id): } ), 200) - else: - return make_response(jsonify({ - "message": "Sale Order with id {} not found".format(sale_order_id) - } - ), 404) \ No newline at end of file + return make_response(jsonify({ + "message": "Sale Order with id {} not found".format(sale_order_id) + } + ), 404) \ No newline at end of file From 97f6a483e9ad8bbfac64e88550910a049592d186 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Wed, 24 Oct 2018 00:29:58 +0300 Subject: [PATCH 079/100] [Chore #161414025] Strip spaces around request data --- app/api/v1/resources/auth.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/api/v1/resources/auth.py b/app/api/v1/resources/auth.py index dd19ecd..5979bed 100644 --- a/app/api/v1/resources/auth.py +++ b/app/api/v1/resources/auth.py @@ -22,9 +22,9 @@ def post(self): return make_response(jsonify({ "message": "Missing required credentials" }), 400) - email = data["email"] - password = generate_password_hash(data["password"], method='sha256') - role = data["role"] + email = data["email"].strip() + password = generate_password_hash(data["password"].strip(), method='sha256') + role = data["role"].strip() Validator.validate_credentials(self, data) user = users.User_Model(email, password, role) res = user.save() From 46c3b8440195d3998a330193c1ebc16e510a545b Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Wed, 24 Oct 2018 00:58:31 +0300 Subject: [PATCH 080/100] [Chore #161414025] Prevent the response from returning password Prevent the hashed password from being returned after signup --- app/api/v1/models/users.py | 9 +++++++-- app/api/v1/resources/auth.py | 5 ++++- app/tests/v1/test_auth_endpoints.py | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/api/v1/models/users.py b/app/api/v1/models/users.py index c8e1fcb..138d06d 100644 --- a/app/api/v1/models/users.py +++ b/app/api/v1/models/users.py @@ -16,7 +16,12 @@ def save(self): "password": self.password, "role": self.role } + + new_added_user = { + "id": self.id, + "email": self.email, + "role": self.role + } USERS.append(new_user) - response = jsonify(new_user) - return response \ No newline at end of file + return new_added_user \ No newline at end of file diff --git a/app/api/v1/resources/auth.py b/app/api/v1/resources/auth.py index 5979bed..ab6d48c 100644 --- a/app/api/v1/resources/auth.py +++ b/app/api/v1/resources/auth.py @@ -28,7 +28,10 @@ def post(self): Validator.validate_credentials(self, data) user = users.User_Model(email, password, role) res = user.save() - return make_response(res, 202) + return make_response(jsonify({ + "message": "Account created successfully", + "user": res + }), 202) class Login(Resource): diff --git a/app/tests/v1/test_auth_endpoints.py b/app/tests/v1/test_auth_endpoints.py index 18bdf5f..9f2c3af 100644 --- a/app/tests/v1/test_auth_endpoints.py +++ b/app/tests/v1/test_auth_endpoints.py @@ -25,7 +25,7 @@ def test_add_new_user(self): data = json.loads(res.data.decode()) print(data) - self.assertEqual(data['email'], "test_add_new_user@gmail.com") + self.assertEqual(data['user']['email'], "test_add_new_user@gmail.com") self.assertEqual(res.status_code, 202) def test_add_new_user_no_data(self): From f8398e4f693328a37803808fec39817b70211182 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Wed, 24 Oct 2018 01:09:34 +0300 Subject: [PATCH 081/100] [Feature #161362891] Increase token expiry time from 5 to 30 minutes --- app/api/v1/resources/auth.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/api/v1/resources/auth.py b/app/api/v1/resources/auth.py index ab6d48c..530f516 100644 --- a/app/api/v1/resources/auth.py +++ b/app/api/v1/resources/auth.py @@ -49,11 +49,10 @@ def post(self): if email == user["email"] and check_password_hash(user["password"], password): token = jwt.encode({ "email": email, - "exp": datetime.datetime.utcnow() + datetime.timedelta - (minutes=5) + "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30) }, os.getenv('JWT_SECRET_KEY', default='SdaHv342nx!jknr837bjwd?c,lsajjjhw673hdsbgeh')) return make_response(jsonify({ - "message": "You are successfully logged in", + "message": "Login successful", "token": token.decode("UTF-8")}), 200) return make_response(jsonify({ "message": "Wrong credentials provided" From 8abebcb7ac3a7759d7e5c443191f4c8f429eefe3 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Wed, 24 Oct 2018 07:57:38 +0300 Subject: [PATCH 082/100] [Chore #161201912] Return all product details on query All the details of the product are returned rather than just the name of the product --- app/api/v1/resources/admin_endpoints.py | 5 ++--- app/api/v1/resources/general_users_endpoints.py | 2 +- app/api/v1/resources/helper_functions.py | 2 +- app/tests/v1/base_test.py | 2 +- app/tests/v1/helper_functions.py | 6 ++---- app/tests/v1/test_general_user_endpoints.py | 2 +- 6 files changed, 8 insertions(+), 11 deletions(-) diff --git a/app/api/v1/resources/admin_endpoints.py b/app/api/v1/resources/admin_endpoints.py index 99560d7..94441db 100644 --- a/app/api/v1/resources/admin_endpoints.py +++ b/app/api/v1/resources/admin_endpoints.py @@ -16,10 +16,9 @@ class ProductsManagement(Resource): - """Class contains the tests for admin + """Class contains admin specific endpoints""" + - specific endpoints - """ def post(self): """POST /products endpoint""" diff --git a/app/api/v1/resources/general_users_endpoints.py b/app/api/v1/resources/general_users_endpoints.py index 38f63fc..fe212ee 100644 --- a/app/api/v1/resources/general_users_endpoints.py +++ b/app/api/v1/resources/general_users_endpoints.py @@ -41,7 +41,7 @@ def get(self, product_id): if product["product_id"] == product_id: return make_response(jsonify({ "message": "{} retrieved successfully".format(product["product_name"]), - "product": product["product_name"] + "product": product } ), 200) diff --git a/app/api/v1/resources/helper_functions.py b/app/api/v1/resources/helper_functions.py index b39e175..a0aeac7 100644 --- a/app/api/v1/resources/helper_functions.py +++ b/app/api/v1/resources/helper_functions.py @@ -27,7 +27,7 @@ def missing_a_required_parameter(): def abort_if_user_is_not_admin(user): user_role = [users['role'] for users in users.USERS if users['email'] == user][0] - if user_role!= "Admin": + if user_role!= "admin": abort(make_response(jsonify( message="Unauthorized. This action is not for you" ), 401)) diff --git a/app/tests/v1/base_test.py b/app/tests/v1/base_test.py index 029ac7d..6a528d8 100644 --- a/app/tests/v1/base_test.py +++ b/app/tests/v1/base_test.py @@ -54,7 +54,7 @@ def register_test_admin_account(self): res = self.app_test_client.post("api/v1/auth/signup", json={ "email": "user@gmail.com", - "role": "Admin", + "role": "admin", "password": "Password12#" }, headers={ diff --git a/app/tests/v1/helper_functions.py b/app/tests/v1/helper_functions.py index cc285bc..36e4717 100644 --- a/app/tests/v1/helper_functions.py +++ b/app/tests/v1/helper_functions.py @@ -1,9 +1,7 @@ import json def convert_response_to_json(response): - """Helper function - - Converts the response to a json type - """ + """Converts the response to a json type""" + json_response = json.loads(response.data.decode('utf-8')) return json_response \ No newline at end of file diff --git a/app/tests/v1/test_general_user_endpoints.py b/app/tests/v1/test_general_user_endpoints.py index 1fe9e8e..d2b2fd6 100644 --- a/app/tests/v1/test_general_user_endpoints.py +++ b/app/tests/v1/test_general_user_endpoints.py @@ -45,7 +45,7 @@ def test_retrieve_specific_product(self): self.assertEqual(response.status_code, 200) self.assertEqual(helper_functions.convert_response_to_json( - response)['product'], self.PRODUCT['product_name']) + response)['product']['product_name'], self.PRODUCT['product_name']) def test_retrieve_specific_sale_order(self): """Test GET /saleorder/id - when saleorder exists""" From e806de03c1761adf93b57454b9ef0fd2c1ebfca5 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Wed, 24 Oct 2018 15:07:04 +0300 Subject: [PATCH 083/100] [Chore #161414025] Strip spaces from login request data --- app/api/v1/resources/auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/api/v1/resources/auth.py b/app/api/v1/resources/auth.py index 530f516..6d9bdce 100644 --- a/app/api/v1/resources/auth.py +++ b/app/api/v1/resources/auth.py @@ -42,8 +42,8 @@ def post(self): "message": "Kindly enter your credentials" } ), 400) - email = data["email"] - password = data["password"] + email = data["email"].strip() + password = data["password"].strip() for user in users.USERS: if email == user["email"] and check_password_hash(user["password"], password): From bbe56033497caa12ccaedaee9488d3740ee78f3b Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Thu, 25 Oct 2018 22:07:39 +0300 Subject: [PATCH 084/100] [Chore #161457608] Create blueprints for version two Separate version 2 of the application into its blueprint --- app/__init__.py | 6 ++++++ app/api/v2/__init__.py | 9 +++++++++ 2 files changed, 15 insertions(+) create mode 100644 app/api/v2/__init__.py diff --git a/app/__init__.py b/app/__init__.py index acbc658..fa53e87 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -20,4 +20,10 @@ def create_app(config_name): from .api.v1 import auth_v1_blueprint as v1_blueprint app.register_blueprint(v1_blueprint, url_prefix='/api/v1/auth') + from .api.v2 import endpoint_v2_blueprint as v2_blueprint + app.register_blueprint(v2_blueprint, url_prefix='/api/v2') + + from .api.v2 import auth_v2_blueprint as v2_blueprint + app.register_blueprint(v2_blueprint, url_prefix='/api/v2/auth') + return app diff --git a/app/api/v2/__init__.py b/app/api/v2/__init__.py new file mode 100644 index 0000000..230ffcb --- /dev/null +++ b/app/api/v2/__init__.py @@ -0,0 +1,9 @@ +from flask import Blueprint + + +endpoint_v2_blueprint = Blueprint('endpoint_v2_blueprint', __name__) +auth_v2_blueprint = Blueprint('auth_v2_blueprint', __name__) + + + +from . import views \ No newline at end of file From ddf06706f13d790cc2ac0741aec1816f01a5b4f7 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Thu, 25 Oct 2018 22:23:08 +0300 Subject: [PATCH 085/100] [Chore #161462204] Create database configuration file Module creates a initialization function which sets up the database, creates queries to the database and executes the queries --- app/api/v2/database.py | 129 +++++++++++++++++++++++++++++++++++++++++ instance/config.py | 2 + 2 files changed, 131 insertions(+) create mode 100644 app/api/v2/database.py diff --git a/app/api/v2/database.py b/app/api/v2/database.py new file mode 100644 index 0000000..4e298b6 --- /dev/null +++ b/app/api/v2/database.py @@ -0,0 +1,129 @@ +"""Module creates a connection to the database + +Creates tables for the application +""" + +import sys + +import psycopg2 +from instance.config import config + +def init_db(db_url=None): + """Initialize db connection + + Run queries that set up tables + """ + try: + conn, cursor = query_database() + queries = drop_table_if_exists() + create_tables() + i = 0 + while i != len(queries): + query = queries[i] + cursor.execute(query) + conn.commit() + i += 1 + conn.close() + + except Exception as error: + print("\nQuery not executed : {} \n".format(error)) + + +def create_tables(): + """Queries for setting up the database tables""" + + users_table_query = """ + CREATE TABLE users ( + user_id SERIAL PRIMARY KEY, + email VARCHAR (30) NOT NULL UNIQUE, + password VARCHAR (128) NOT NULL, + role VARCHAR (10) NOT NULL + )""" + + products_table_query = """ + CREATE TABLE products ( + product_id SERIAL PRIMARY KEY, + product_name VARCHAR (24) NOT NULL, + product_price INTEGER NOT NULL, + category VARCHAR (50) NOT NULL + )""" + + sales_order_query = """ + CREATE TABLE saleorders ( + sale_order_id SERIAL PRIMARY KEY, + date_ordered TIMESTAMP DEFAULT NOW(), + product_name VARCHAR (24) NOT NULL, + product_price INTEGER NOT NULL, + quantity INTEGER NOT NULL, + amount INTEGER NOT NULL + )""" + + return [users_table_query, products_table_query, sales_order_query] + + +def drop_table_if_exists(): + """Drop tables before recreating them""" + + drop_products_table = """ + DROP TABLE IF EXISTS products""" + + drop_sales_table = """ + DROP TABLE IF EXISTS saleorders""" + + drop_users_table = """ + DROP TABLE IF EXISTS users""" + + return [drop_products_table, drop_sales_table, drop_users_table] + + +def query_database(query=None, db_url=None): + """Creates a connection to the db + + Executes a query + """ + conn = None + if db_url is None: + db_url = config['db_url'] + try: + # connect to db + conn = psycopg2.connect(db_url) + print("\n\nConnected {}\n".format(conn.get_dsn_parameters())) + cursor = conn.cursor() + + if query: + cursor.execute(query) + conn.commit() + + except(Exception, + psycopg2.DatabaseError, + psycopg2.ProgrammingError) as error: + print("DB ERROR: {}".format(error)) + + return conn, cursor + + +def insert_to_db(query): + """Handles INSERT queries""" + + try: + conn = query_database(query)[0] + conn.close() + except psycopg2.Error as error: + print("Insertion error: {}".format(error)) + sys.exit(1) + + +def select_from_db(query): + """Handles SELECT queries""" + + fetched_content = None + conn, cursor = query_database(query) + if conn: + fetched_content = cursor.fetchall() + conn.close() + + return fetched_content + + +if __name__ == '__main__': + init_db() + query_database() diff --git a/instance/config.py b/instance/config.py index 3cb456a..a1eef27 100644 --- a/instance/config.py +++ b/instance/config.py @@ -32,4 +32,6 @@ class Production(Config): 'testing': Testing, 'staging': Staging, 'production': Production, + 'db_url': "dbname='storemanager' host='localhost' port='5432' user='postgres' password='Password12#'", + 'test_db_url': "dbname='storemanagertest' host='localhost' port='5432' user='postgres' password='Password12#'" } \ No newline at end of file From 2bffeecacfceffff949626f4c2df73e7a443417a Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Thu, 25 Oct 2018 22:28:34 +0300 Subject: [PATCH 086/100] [Chore #161248446] Model products Models products into a resource with a connection to the database --- app/api/v2/models/__init__.py | 0 app/api/v2/models/products.py | 37 +++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 app/api/v2/models/__init__.py create mode 100644 app/api/v2/models/products.py diff --git a/app/api/v2/models/__init__.py b/app/api/v2/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/v2/models/products.py b/app/api/v2/models/products.py new file mode 100644 index 0000000..4e06e97 --- /dev/null +++ b/app/api/v2/models/products.py @@ -0,0 +1,37 @@ +"""This module contains the data store + +and data logic of the store's products +""" +from .. import database + +class Products(): + def __init__(self, product_name, product_price, category): + self.product_name = product_name + self.product_price = product_price + self.category = category + + + def save(self): + query = """ + INSERT INTO products(product_name, product_price, category) VALUES( + '{}', '{}', '{}' + )""".format(self.product_name, self.product_price, self.category) + + database.insert_to_db(query) + + @staticmethod + def fetch_product(product_name): + """ + Queries db for user with given username + Returns user object + """ + # Query db for user with those params + query = """ + SELECT * FROM products + WHERE product_name = '{}'""".format(product_name) + + return database.select_from_db(query) + + @staticmethod + def fetch_products(query): + return database.select_from_db(query) From 284664c9f16db41a433d60af0a6fecc724fddc09 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Thu, 25 Oct 2018 22:34:17 +0300 Subject: [PATCH 087/100] [Chore #161248446] Modify users model to integrate database --- app/api/v2/models/users.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 app/api/v2/models/users.py diff --git a/app/api/v2/models/users.py b/app/api/v2/models/users.py new file mode 100644 index 0000000..7b47417 --- /dev/null +++ b/app/api/v2/models/users.py @@ -0,0 +1,30 @@ +from flask import make_response, jsonify + +from .. import database + +class User_Model(): + def __init__(self, email, password, role): + self.email = email + self.password = password + self.role = role + + def save(self): + query = """ + INSERT INTO users(email, role, password) VALUES( + '{}', '{}', '{}' + )""".format(self.email, self.role, self.password) + + database.insert_to_db(query) + + @staticmethod + def fetch_user(email): + """ + Queries db for user with given username + Returns user object + """ + # Query db for user with those params + query = """ + SELECT * FROM users + WHERE email = '{}'""".format(email) + + return database.select_from_db(query) \ No newline at end of file From 91936a9d7d576b24946f7035b5261f77de5e25b1 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Thu, 25 Oct 2018 22:34:39 +0300 Subject: [PATCH 088/100] [Chore #161248446] Modify sale orders model to integrate database --- app/api/v2/models/sale_orders.py | 38 ++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 app/api/v2/models/sale_orders.py diff --git a/app/api/v2/models/sale_orders.py b/app/api/v2/models/sale_orders.py new file mode 100644 index 0000000..31133f6 --- /dev/null +++ b/app/api/v2/models/sale_orders.py @@ -0,0 +1,38 @@ +"""This module contains the data store + +and data logic of the store attendant's sale orders +""" +import datetime +from flask import jsonify + +from .. import database + +class SaleOrder(): + def __init__(self, product_name, product_price, quantity): + self.product_name = product_name + self.product_price = product_price + self.quantity = quantity + self.amount = (self.quantity * self.product_price) + self.date = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + def save(self): + query = """ + INSERT INTO saleorders(product_name, product_price, quantity, amount, date_ordered) VALUES( + '{}', '{}', '{}', '{}', '{}' + )""".format(self.product_name, self.product_price, self.quantity, self.amount, self.date) + + database.insert_to_db(query) + + + @staticmethod + def fetch_saleorder(product_name): + """ + Queries db for user with given username + Returns user object + """ + # Query db for user with those params + query = """ + SELECT * FROM saleorders + WHERE product_name = '{}'""".format(product_name) + + return database.select_from_db(query) \ No newline at end of file From a25059f352c3900fff9dddc10b1ffb4ec7416506 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Thu, 25 Oct 2018 22:41:35 +0300 Subject: [PATCH 089/100] [Chore #161491333] Create test for store attendant endpoint --- .../v2/test_store_attendant_endpoints.py | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 app/tests/v2/test_store_attendant_endpoints.py diff --git a/app/tests/v2/test_store_attendant_endpoints.py b/app/tests/v2/test_store_attendant_endpoints.py new file mode 100644 index 0000000..c78d9b1 --- /dev/null +++ b/app/tests/v2/test_store_attendant_endpoints.py @@ -0,0 +1,124 @@ +"""Module contains tests for store attendant + +specific endpoints +""" +from flask import current_app + +from . import base_test +from . import helper_functions + +class TestAdminEndpoints(base_test.TestBaseClass): + """ Class contains tests for store attendant specific endpoints """ + + + def test_create_sale_order(self): + """Test POST /saleorder""" + self.register_test_admin_account() + token = self.login_test_admin() + + # send a dummy data response for testing + response = self.app_test_client.post('{}/saleorder'.format( + self.BASE_URL), json=self.SALE_ORDERS, headers=dict(Authorization=token), + content_type='application/json') + + self.assertEqual(response.status_code, 201) + self.assertEqual(helper_functions.convert_response_to_json( + response)['saleorder']['product_name'], self.SALE_ORDERS['product_name']) + self.assertEqual(helper_functions.convert_response_to_json( + response)['saleorder']['product_price'], self.SALE_ORDERS['product_price']) + self.assertEqual(helper_functions.convert_response_to_json( + response)['saleorder']['quantity'], self.SALE_ORDERS['quantity']) + self.assertEqual(helper_functions.convert_response_to_json( + response)['saleorder']['amount'], self.SALE_ORDERS['amount']) + self.assertEqual(helper_functions.convert_response_to_json( + response)['message'], 'Checkout complete') + + + def test_create_sale_order_parameter_missing(self): + """Test POST /saleorder + + with one of the required parameters missing + """ + self.register_test_admin_account() + token = self.login_test_admin() + + response = self.app_test_client.post('{}/saleorder'.format( + self.BASE_URL), json={'product_name': 'Nyundo'}, headers=dict(Authorization=token), + content_type='application/json') + + self.assertEqual(response.status_code, 400) + self.assertEqual(helper_functions.convert_response_to_json( + response)['message'], 'Bad request. Request missing a required argument') + + + def test_create_sale_order_price_below_one(self): + """Test POST /saleorder + + with the price not a valid integer + """ + self.register_test_admin_account() + token = self.login_test_admin() + + response = self.app_test_client.post('{}/saleorder'.format( + self.BASE_URL), json={'product_name': 'Nyundo', 'product_price': -1, 'quantity': 1}, + headers=dict(Authorization=token), + content_type='application/json') + + self.assertEqual(response.status_code, 400) + self.assertEqual(helper_functions.convert_response_to_json( + response)['message'], 'Bad request. Price of the product should be a positive integer above 0.') + + + def test_create_sale_order_invalid_product_name(self): + """Test POST /saleorder + + with the product name not a string + """ + self.register_test_admin_account() + token = self.login_test_admin() + + response = self.app_test_client.post('{}/saleorder'.format( + self.BASE_URL), json={'product_name': 3, 'product_price': 300, 'quantity': 1}, + headers=dict(Authorization=token), + content_type='application/json') + + self.assertEqual(response.status_code, 400) + self.assertEqual(helper_functions.convert_response_to_json( + response)['message'], 'Bad request. Product name should be a string') + + + def test_create_sale_order_price_not_digits(self): + """Test POST /saleorder + + with the price not a valid integer + """ + + self.register_test_admin_account() + token = self.login_test_admin() + + response = self.app_test_client.post('{}/saleorder'.format( + self.BASE_URL), json={'product_name': "Nyundo", 'product_price': "300", 'quantity': 1}, + headers=dict(Authorization=token), + content_type='application/json') + + self.assertEqual(response.status_code, 400) + self.assertEqual(helper_functions.convert_response_to_json( + response)['message'], 'Bad request. The product price should be digits') + + + def test_create_sale_order_invalid_quantity(self): + """Test POST /saleorder + + with the quantity not a valid integer + """ + self.register_test_admin_account() + token = self.login_test_admin() + + response = self.app_test_client.post('{}/saleorder'.format( + self.BASE_URL), json={'product_name': "Nyundo", 'product_price': 300, 'quantity': "1"}, + headers=dict(Authorization=token), + content_type='application/json') + + self.assertEqual(response.status_code, 400) + self.assertEqual(helper_functions.convert_response_to_json( + response)['message'], 'Bad request. The quantity should be specified in digits') \ No newline at end of file From ba68659a65c82aecb4e20f59f9ff5215ea83eb08 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Thu, 25 Oct 2018 22:41:59 +0300 Subject: [PATCH 090/100] [Chore #161491333] Create test for general user endpoint --- app/tests/v2/test_general_user_endpoints.py | 117 ++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 app/tests/v2/test_general_user_endpoints.py diff --git a/app/tests/v2/test_general_user_endpoints.py b/app/tests/v2/test_general_user_endpoints.py new file mode 100644 index 0000000..c783a9c --- /dev/null +++ b/app/tests/v2/test_general_user_endpoints.py @@ -0,0 +1,117 @@ +"""Module contains tests to endpoints that + +are general to both the admin and the normal user +""" + +import json + +from . import base_test +from . import helper_functions + +class TestGeneralUsersEndpoints(base_test.TestBaseClass): + """Class contains the general user, i.e. both admin + + and normal user, endpoints' tests + """ + + + def test_retrieve_all_products(self): + """Test GET /products - when products exist""" + self.register_test_admin_account() + token = self.login_test_admin() + + # send a dummy data response for testing + self.app_test_client.post('{}/products'.format( + self.BASE_URL), json=self.PRODUCT, headers=dict(Authorization=token), + content_type='application/json') + + + response = self.app_test_client.get( + '{}/products'.format(self.BASE_URL), + headers=dict(Authorization=token), + content_type='application/json' + ) + + self.assertEqual(response.status_code, 200) + self.assertEqual(helper_functions.convert_response_to_json( + response)['products'][0][1], self.PRODUCT['product_name']) + + def test_retrieve_specific_product(self): + """Test GET /products/id - when product exist""" + + self.register_test_admin_account() + token = self.login_test_admin() + + # send a dummy data response for testing + self.app_test_client.post('{}/products'.format( + self.BASE_URL), json=self.PRODUCT, headers=dict(Authorization=token), + content_type='application/json') + + response = self.app_test_client.get( + '{}/products/1'.format(self.BASE_URL), + headers=dict(Authorization=token), + content_type='application/json' + ) + + self.assertEqual(response.status_code, 200) + self.assertEqual(helper_functions.convert_response_to_json( + response)['product'][0][1], self.PRODUCT['product_name']) + + def test_retrieve_specific_sale_order(self): + """Test GET /saleorder/id - when saleorder exists""" + + self.register_test_admin_account() + token = self.login_test_admin() + + self.app_test_client.post( + '{}/saleorder'.format(self.BASE_URL), json={ + 'sale_order_id': 1, + 'product_name': "Test Product", + 'product_price': 20, + 'quantity': 1, + 'amount': 20 + }, + headers=dict(Authorization=token), + content_type='application/json') + + response = self.app_test_client.get( + '{}/saleorder/1'.format(self.BASE_URL), + headers=dict(Authorization=token), + content_type='application/json' + ) + + self.assertEqual(response.status_code, 200) + + + def test_missing_token(self): + """Test GET /products - when token is missing""" + + self.register_test_admin_account() + token = "" + + response = self.app_test_client.get( + '{}/products'.format(self.BASE_URL), + headers=dict(Authorization=token), + content_type='application/json' + ) + + self.assertEqual(response.status_code, 401) + self.assertEqual(helper_functions.convert_response_to_json( + response)["Message"], "You need to login") + + + def test_invalid_token(self): + """Test GET /products - when token is missing""" + + self.register_test_admin_account() + token = "sample_invalid-token-afskdghkfhwkedaf-ksfakjfwey" + + response = self.app_test_client.get( + '{}/products'.format(self.BASE_URL), + headers=dict(Authorization=token), + content_type='application/json' + ) + + self.assertEqual(response.status_code, 403) + self.assertEqual(helper_functions.convert_response_to_json( + response)["Message"], "The token is either expired or wrong") \ No newline at end of file From 09527c4fe2f7ff27b089bdb88d19ee39b5986de6 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Thu, 25 Oct 2018 22:42:27 +0300 Subject: [PATCH 091/100] [Chore #161491333] Create test for auth endpoint --- app/tests/v2/test_auth_endpoints.py | 234 ++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 app/tests/v2/test_auth_endpoints.py diff --git a/app/tests/v2/test_auth_endpoints.py b/app/tests/v2/test_auth_endpoints.py new file mode 100644 index 0000000..0a4b1b7 --- /dev/null +++ b/app/tests/v2/test_auth_endpoints.py @@ -0,0 +1,234 @@ +"""Module contains tests to endpoints that + +are used for user registration and authentication +""" + +import json + +from . import base_test +from . import helper_functions + +class TestAuthEndpoints(base_test.TestBaseClass): + """ Class contains tests for auth endpoints """ + + def test_add_new_user(self): + res = self.app_test_client.post("api/v2/auth/signup", + json={ + "email": "test_add_new_user@gmail.com", + "role": "Admin", + "password": "Password12#" + }, + headers={ + "Content-Type": "application/json" + }) + + data = json.loads(res.data.decode()) + print(data) + + self.assertEqual(data['user']['email'], "test_add_new_user@gmail.com") + self.assertEqual(res.status_code, 202) + + def test_add_new_user_no_data(self): + res = self.app_test_client.post("api/v2/auth/signup", + json={ + + }, + headers={ + "Content-Type": "application/json" + }) + + data = json.loads(res.data.decode()) + print(res) + + self.assertEqual(data['message'], "Missing required credentials") + self.assertEqual(res.status_code, 400) + + def test_add_new_user_missing_params(self): + res = self.app_test_client.post("api/v2/auth/signup", + json={ + "email": "", + "role": "Admin", + "password": "Password12#" + }, + headers={ + "Content-Type": "application/json" + }) + + data = json.loads(res.data.decode()) + print(data) + + self.assertEqual(data['message'], "You are missing a required credential") + self.assertEqual(res.status_code, 400) + + def test_add_new_user_invalid_email(self): + res = self.app_test_client.post("api/v2/auth/signup", + json={ + "email": "test_add_new_user", + "role": "Admin", + "password": "Password12#" + }, + headers={ + "Content-Type": "application/json" + }) + + data = json.loads(res.data.decode()) + print(data) + + self.assertEqual(data['message'], "Invalid email") + self.assertEqual(res.status_code, 400) + + def test_add_new_user_no_digit_password(self): + res = self.app_test_client.post("api/v2/auth/signup", + json={ + "email": "test_add_new_user_invalid_email@gmail.com", + "role": "Admin", + "password": "No#digit" + }, + headers={ + "Content-Type": "application/json" + }) + + data = json.loads(res.data.decode()) + print(data) + + self.assertEqual(data['message'], "Password must have a digit") + self.assertEqual(res.status_code, 400) + + def test_add_new_user_short_password(self): + res = self.app_test_client.post("api/v2/auth/signup", + json={ + "email": "test_add_new_user_invalid_email@gmail.com", + "role": "Admin", + "password": "Shor#" + }, + headers={ + "Content-Type": "application/json" + }) + + data = json.loads(res.data.decode()) + print(data) + + self.assertEqual(data['message'], "Password must be long than 6 characters or less than 12") + self.assertEqual(res.status_code, 400) + + def test_add_new_user_no_special_ch_password(self): + res = self.app_test_client.post("api/v2/auth/signup", + json={ + "email": "test_add_new_user_invalid_email@gmail.com", + "role": "Admin", + "password": "NoSplCh12" + }, + headers={ + "Content-Type": "application/json" + }) + + data = json.loads(res.data.decode()) + print(data) + + self.assertEqual(data['message'], "Password must have a special charater") + self.assertEqual(res.status_code, 400) + + def test_add_new_user_no_upper_case_password(self): + res = self.app_test_client.post("api/v2/auth/signup", + json={ + "email": "test_add_new_user_invalid_email@gmail.com", + "role": "Admin", + "password": "noupper12#" + }, + headers={ + "Content-Type": "application/json" + }) + + data = json.loads(res.data.decode()) + print(data) + + self.assertEqual(data['message'], "Password must have an upper case character") + self.assertEqual(res.status_code, 400) + + def test_add_new_user_no_lower_case_password(self): + res = self.app_test_client.post("api/v2/auth/signup", + json={ + "email": "test_add_new_user_invalid_email@gmail.com", + "role": "Admin", + "password": "NOLOWER12#" + }, + headers={ + "Content-Type": "application/json" + }) + + data = json.loads(res.data.decode()) + print(data) + + self.assertEqual(data['message'], "Password must have a lower case character") + self.assertEqual(res.status_code, 400) + + def test_add_new_user_existing(self): + """Test POST /auth/signup""" + self.register_test_admin_account() + response = self.register_test_admin_account() + data = json.loads(response.data.decode()) + + self.assertEqual(data['message'], "Record already exists in the database") + self.assertEqual(response.status_code, 400) + + def test_login_existing_user(self): + self.register_test_admin_account() + resp = self.app_test_client.post("api/v2/auth/login", + json={ + "email": "user@gmail.com", + "password": "Password12#" + }, + headers={ + "Content-Type": "application/json" + }) + + self.assertTrue(helper_functions.convert_response_to_json( + resp)['token']) + self.assertTrue(helper_functions.convert_response_to_json( + resp)['message'], "You are successfully logged in!") + self.assertEqual(resp.status_code, 200) + + def test_login_no_credentials(self): + self.register_test_admin_account() + resp = self.app_test_client.post("api/v2/auth/login", + json={ + + }, + headers={ + "Content-Type": "application/json" + }) + + self.assertTrue(helper_functions.convert_response_to_json( + resp)['message'], "Kindly enter your credentials") + self.assertEqual(resp.status_code, 400) + + def test_login_wrong_password(self): + self.register_test_admin_account() + resp = self.app_test_client.post("api/v2/auth/login", + json={ + "email": "user@gmail.com", + "password": "neverexpecteduser" + }, + headers={ + "Content-Type": "application/json" + }) + + self.assertTrue(helper_functions.convert_response_to_json( + resp)['message'], "Wrong credentials provided") + self.assertEqual(resp.status_code, 403) + + def test_login_non_existant_user(self): + self.register_test_admin_account() + resp = self.app_test_client.post("api/v2/auth/login", + json={ + "email": "non_matching_credentials_user_1018@gmail.com", + "password": "neverexpecteduser" + }, + headers={ + "Content-Type": "application/json" + }) + + self.assertTrue(helper_functions.convert_response_to_json( + resp)['message'], "User not found.") + self.assertEqual(resp.status_code, 404) + \ No newline at end of file From 74ed66d0b775073f4c36c0ca777fe042295e64af Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Thu, 25 Oct 2018 22:43:10 +0300 Subject: [PATCH 092/100] [Chore #161491333] Create test to check app working --- app/tests/v2/test_app_working.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 app/tests/v2/test_app_working.py diff --git a/app/tests/v2/test_app_working.py b/app/tests/v2/test_app_working.py new file mode 100644 index 0000000..4574c93 --- /dev/null +++ b/app/tests/v2/test_app_working.py @@ -0,0 +1,18 @@ +""" + Module contains tests for the working + of the application +""" +from flask import current_app + +#local imports +from . import base_test + +class TestConfigCase(base_test.TestBaseClass): + + + def test_app_exists(self): + self.assertFalse(current_app is None) + + + def test_app_is_testing(self): + self.assertTrue(current_app.config['TESTING']) \ No newline at end of file From 0a2a6f10cd94eab1c461243cdbea5c3d01991ae1 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Thu, 25 Oct 2018 22:44:14 +0300 Subject: [PATCH 093/100] [Chore #161491333] Create tests for admin endpoints Tests are failing at this point --- app/tests/v2/base_test.py | 93 +++++++++++++++ app/tests/v2/helper_functions.py | 7 ++ app/tests/v2/test_admin_endpoints.py | 162 +++++++++++++++++++++++++++ 3 files changed, 262 insertions(+) create mode 100644 app/tests/v2/base_test.py create mode 100644 app/tests/v2/helper_functions.py create mode 100644 app/tests/v2/test_admin_endpoints.py diff --git a/app/tests/v2/base_test.py b/app/tests/v2/base_test.py new file mode 100644 index 0000000..82e28d4 --- /dev/null +++ b/app/tests/v2/base_test.py @@ -0,0 +1,93 @@ +""" + Contains the base test class for the + other test classes +""" +import unittest + +# local imports +from app import create_app +from instance.config import config +from . import helper_functions +from ...api.v2.database import init_db +from instance.config import config + + +class TestBaseClass(unittest.TestCase): + """Base test class""" + + + def setUp(self): + """Create and setup the application + + for testing purposes + """ + self.app = create_app('testing') + self.BASE_URL = 'api/v2' + self.app_context = self.app.app_context() + self.app_context.push() + self.app_test_client = self.app.test_client() + self.app.testing = True + + with self.app.app_context(): + self.db_url = config['test_db_url'] + init_db(self.db_url) + + self.PRODUCT = { + 'product_name': 'Phone Model 1', + 'product_price': 55000, + 'category': 'Phones' + } + + self.SALE_ORDERS = { + 'product_name': 'Phone Model 1', + 'product_price': 55000, + 'quantity': 6, + 'amount': (55000 * 6) + } + + + def tearDown(self): + """Destroy the application that + + is created for testing + """ + with self.app.app_context(): + init_db(self.db_url) + self.app_context.pop() + + def register_test_admin_account(self): + #Register attendant + """Registers an admin test user account""" + + res = self.app_test_client.post("api/v2/auth/signup", + json={ + "email": "user@gmail.com", + "role": "admin", + "password": "Password12#" + }, + headers={ + "Content-Type": "application/json" + }) + + return res + + def login_test_admin(self): + """Validates the test account for the admin""" + + # Login the test account for the admin + resp = self.app_test_client.post("api/v2/auth/login", + json={ + "email": "user@gmail.com", + "password": "Password12#" + }, + headers={ + "Content-Type": "application/json" + }) + + auth_token = helper_functions.convert_response_to_json( + resp)['token'] + + return auth_token + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/app/tests/v2/helper_functions.py b/app/tests/v2/helper_functions.py new file mode 100644 index 0000000..36e4717 --- /dev/null +++ b/app/tests/v2/helper_functions.py @@ -0,0 +1,7 @@ +import json + +def convert_response_to_json(response): + """Converts the response to a json type""" + + json_response = json.loads(response.data.decode('utf-8')) + return json_response \ No newline at end of file diff --git a/app/tests/v2/test_admin_endpoints.py b/app/tests/v2/test_admin_endpoints.py new file mode 100644 index 0000000..4018489 --- /dev/null +++ b/app/tests/v2/test_admin_endpoints.py @@ -0,0 +1,162 @@ +"""Module contains tests for admin + +specific endpoints +""" + +import json + +from flask import current_app + +from . import base_test +from . import helper_functions + +class TestAdminEndpoints(base_test.TestBaseClass): + """ Class contains tests for admin specific endpoints """ + + def test_add_new_product(self): + """Test POST /products""" + self.register_test_admin_account() + token = self.login_test_admin() + + # send a dummy data response for testing + response = self.app_test_client.post('{}/products'.format( + self.BASE_URL), json=self.PRODUCT, headers=dict(Authorization=token), + content_type='application/json') + + self.assertEqual(response.status_code, 201) + self.assertEqual(helper_functions.convert_response_to_json( + response)['product']['product_name'], self.PRODUCT['product_name']) + self.assertEqual(helper_functions.convert_response_to_json( + response)['product']['product_price'], self.PRODUCT['product_price']) + self.assertEqual(helper_functions.convert_response_to_json( + response)['product']['category'], self.PRODUCT['category']) + self.assertEqual(helper_functions.convert_response_to_json( + response)['message'], 'Product added successfully') + + def test_add_new_product_parameter_missing(self): + """Test POST /products + + with one of the required parameters missing + """ + self.register_test_admin_account() + token = self.login_test_admin() + + response = self.app_test_client.post('{}/products'.format( + self.BASE_URL), json={'product_name': 'Nyundo'}, headers=dict(Authorization=token), + content_type='application/json') + + self.assertEqual(response.status_code, 400) + self.assertEqual(helper_functions.convert_response_to_json( + response)['message'], 'Bad request. Request missing a required argument') + + def test_add_new_product_price_under_one(self): + """Test POST /products + + with the price of the product below minimum + """ + self.register_test_admin_account() + token = self.login_test_admin() + + response = self.app_test_client.post('{}/products'.format( + self.BASE_URL), json={ + 'product_id': 1, 'product_name': "Hammer", 'product_price': 0, 'category':'Tools' + }, headers=dict(Authorization=token), + content_type='application/json') + + self.assertEqual(response.status_code, 400) + self.assertEqual(helper_functions.convert_response_to_json( + response)['message'], + 'Price of the product should be a positive integer above 0.') + + + def test_add_new_product_with_product_name_not_string(self): + """Test POST /products + + with the product name not a string + """ + self.register_test_admin_account() + token = self.login_test_admin() + + response = self.app_test_client.post('{}/products'.format( + self.BASE_URL), json={ + 'product_id': 1, 'product_name': 200, 'product_price': 200, 'category':'Tools' + }, headers=dict(Authorization=token), + content_type='application/json') + + self.assertEqual(response.status_code, 400) + self.assertEqual(helper_functions.convert_response_to_json( + response)['message'], + 'Product name should be a string') + + def test_add_new_product_with_category_not_string(self): + """Test POST /products + + with the category not a string + """ + self.register_test_admin_account() + token = self.login_test_admin() + + response = self.app_test_client.post('{}/products'.format( + self.BASE_URL), json={ + 'product_id': 1, 'product_name': "Hammer", 'product_price': 200, 'category': 200 + }, headers=dict(Authorization=token), + content_type='application/json') + + self.assertEqual(response.status_code, 400) + self.assertEqual(helper_functions.convert_response_to_json( + response)['message'], + 'Bad request. The Category should be a string') + + def test_add_new_product_with_product_name_already_existing(self): + """Test POST /products + + with the product name already existing + """ + self.register_test_admin_account() + token = self.login_test_admin() + + self.app_test_client.post('{}/products'.format( + self.BASE_URL), json={ + 'product_id': 1, 'product_name': "Hammer", 'product_price': 200, 'category': "Tools" + }, headers=dict(Authorization=token), + content_type='application/json') + + response = self.app_test_client.post('{}/products'.format( + self.BASE_URL), json={ + 'product_id': 1, 'product_name': "Hammer", 'product_price': 200, 'category': "Tools" + }, headers=dict(Authorization=token), + content_type='application/json') + + self.assertEqual(response.status_code, 400) + self.assertEqual(helper_functions.convert_response_to_json( + response)['message'], + 'Record already exists in the database') + + + def test_fetch_sale_orders(self): + """Test GET /saleorder - when sale order exists""" + self.register_test_admin_account() + token = self.login_test_admin() + + self.app_test_client.post( + '{}/saleorder'.format(self.BASE_URL), json={ + 'product_name': "Test Product", + 'product_price': 20, + 'quantity': 1, + 'amount': 20 + }, + headers=dict(Authorization=token), + content_type='application/json') + + response = self.app_test_client.get( + '{}/saleorder'.format(self.BASE_URL), + + headers=dict(Authorization=token), + content_type='application/json' + ) + + self.assertEqual(response.status_code, 200) + self.assertEqual(helper_functions.convert_response_to_json( + response)['sale_orders'][0][2], "Test Product") + self.assertEqual(helper_functions.convert_response_to_json( + response)['sale_orders'][0][3], 20) From fc495709e7f6d1810ea0a1163cdcdb260af8ed10 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Thu, 25 Oct 2018 23:04:16 +0300 Subject: [PATCH 094/100] [Chore #161491969] Enable endpoints to connect to database --- app/api/v2/resources/__init__.py | 0 app/api/v2/resources/admin_endpoints.py | 80 +++++++++++++++++++ app/api/v2/resources/auth.py | 79 ++++++++++++++++++ .../v2/resources/general_users_endpoints.py | 75 +++++++++++++++++ app/api/v2/resources/helper_functions.py | 40 ++++++++++ .../v2/resources/store_attendant_endpoints.py | 66 +++++++++++++++ app/api/v2/resources/verify.py | 46 +++++++++++ 7 files changed, 386 insertions(+) create mode 100644 app/api/v2/resources/__init__.py create mode 100644 app/api/v2/resources/admin_endpoints.py create mode 100644 app/api/v2/resources/auth.py create mode 100644 app/api/v2/resources/general_users_endpoints.py create mode 100644 app/api/v2/resources/helper_functions.py create mode 100644 app/api/v2/resources/store_attendant_endpoints.py create mode 100644 app/api/v2/resources/verify.py diff --git a/app/api/v2/resources/__init__.py b/app/api/v2/resources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/v2/resources/admin_endpoints.py b/app/api/v2/resources/admin_endpoints.py new file mode 100644 index 0000000..15cbfc8 --- /dev/null +++ b/app/api/v2/resources/admin_endpoints.py @@ -0,0 +1,80 @@ +"""This module contains endpoints + +that are specific to the admin +""" +import os +import jwt +from functools import wraps + +from flask import Flask, jsonify, request, abort, make_response +from flask_restful import Resource +from flask_jwt_extended import get_jwt_identity, jwt_required + +from . import helper_functions +from ..models import products, sale_orders, users +from . import verify +from .. import database +from ..utils import validator + + +class ProductsManagement(Resource): + """Class contains admin specific endpoints""" + + + def post(self): + """POST /products endpoint""" + + # Token verification and admin user determination + # logged_user = verify.verify_tokens() + # helper_functions.abort_if_user_is_not_admin(logged_user) + + data = request.get_json() + helper_functions.no_json_in_request(data) + try: + product_name = data['product_name'] + product_price = data['product_price'] + category = data['category'] + except KeyError: + # If product is missing required parameter + helper_functions.missing_a_required_parameter() + + validator.check_duplication("product_name", "products", product_name) + verify.verify_post_product_fields(product_price, product_name, category) + + added_product = products.Products(product_name, product_price, category) + added_product.save() + + return make_response(jsonify({ + "message": "Product added successfully", + "product": { + "product_name": product_name, + "product_price": product_price, + "category": category + } + }), 201) + + +class SaleAttendantsManagement(Resource): + """Class contains the tests managing + + sale orders + """ + + def get(self): + """GET /saleorder endpoint""" + + # verify.verify_tokens() + + query = """SELECT * FROM saleorders""" + saleorders = database.select_from_db(query) + if not saleorders: + return jsonify({ + 'message': "No sale orders created yet" + }) + + response = jsonify({ + 'message': "Successfully fetched all the sale orders", + 'sale_orders': saleorders + }) + response.status_code = 200 + return response diff --git a/app/api/v2/resources/auth.py b/app/api/v2/resources/auth.py new file mode 100644 index 0000000..29d9e80 --- /dev/null +++ b/app/api/v2/resources/auth.py @@ -0,0 +1,79 @@ +import os +import datetime +import jwt +from werkzeug.security import generate_password_hash, check_password_hash +from functools import wraps + +from flask import Flask, jsonify, request, make_response, abort +from flask_restful import Resource +from flask_jwt_extended import ( + jwt_required, create_access_token, + get_jwt_identity +) + +from instance import config +from ..utils.validator import Validator, check_duplication +from ..models import users + +class SignUp(Resource): + def post(self): + data = request.get_json() + if not data: + return make_response(jsonify({ + "message": "Missing required credentials" + }), 400) + try: + email = data["email"].strip() + request_password = data["password"].strip() + request_role = data["role"].strip() + except KeyError: + return make_response(jsonify({ + "message": "Missing required credentials" + }), 400) + + Validator.validate_credentials(self, data) + check_duplication("email", "users", email) + hashed_password = generate_password_hash(request_password, method='sha256') + user = users.User_Model(email, hashed_password, request_role) + user.save() + return make_response(jsonify({ + "message": "Account created successfully", + "user": { + "email": email, + "role": request_role + } + }), 202) + + +class Login(Resource): + def post(self): + data = request.get_json() + if not data: + return make_response(jsonify({ + "message": "Kindly enter your credentials" + } + ), 400) + request_email = data["email"].strip() + request_password = data["password"].strip() + + user = users.User_Model.fetch_user(request_email) + if not user: + abort(make_response(jsonify( + message="User not found."), 404)) + + user_email = user[0][1] + user_password = user[0][2] + + if request_email == user_email and check_password_hash(user_password, request_password): + token = jwt.encode({ + "email": request_email, + "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30) + }, os.getenv('JWT_SECRET_KEY', default='SdaHv342nx!jknr837bjwd?c,lsajjjhw673hdsbgeh')) + return make_response(jsonify({ + "message": "Login successful", + "token": token.decode("UTF-8")}), 200) + + return make_response(jsonify({ + "message": "Wrong credentials provided" + } + ), 403) diff --git a/app/api/v2/resources/general_users_endpoints.py b/app/api/v2/resources/general_users_endpoints.py new file mode 100644 index 0000000..b29fef8 --- /dev/null +++ b/app/api/v2/resources/general_users_endpoints.py @@ -0,0 +1,75 @@ +"""This module contains endpoints + +for both the admin and the normal user +""" +from flask import jsonify, abort, make_response +from flask_restful import Resource +from flask_jwt_extended import get_jwt_identity, jwt_required +from . import verify + +from . import helper_functions +from ..models import products, sale_orders +from .. import database + +class AllProducts(Resource): + """Class contains the tests for both + + the admin and the normal user endpoints + """ + + def get(self): + """GET /products endpoint""" + verify.verify_tokens() + query = """SELECT * FROM products""" + fetched_products = products.Products.fetch_products(query) + if not fetched_products: + return make_response(jsonify({ + "message": "There are no products in the store yet", + }), 404) + + response = jsonify({ + 'message': "Successfully fetched all the products", + 'products': fetched_products + }) + + response.status_code = 200 + return response + +class SpecificProduct(Resource): + + def get(self, product_id): + + # verify.verify_tokens() + query = """SELECT * FROM products WHERE product_id = '{}'""".format(product_id) + + fetched_product = database.select_from_db(query) + if not fetched_product: + return make_response(jsonify({ + "message": "Product with id {} is not available".format(product_id), + }), 400) + + return make_response(jsonify({ + "message": "{} retrieved successfully".format(fetched_product[0][1]), + "product": fetched_product + }), 200) + + +class SpecificSaleOrder(Resource): + + def get(self, sale_order_id): + """GET /saleorder/""" + + # verify.verify_tokens() + query = """SELECT * FROM saleorders WHERE sale_order_id = '{}'""".format(sale_order_id) + sale_order = database.select_from_db(query) + if not sale_order: + return make_response(jsonify({ + "message": "Sale Order with id {} not found".format(sale_order_id) + } + ), 404) + + return make_response(jsonify({ + "message": "Sale order fetched successfully", + "saleorder": sale_order + } + ), 200) \ No newline at end of file diff --git a/app/api/v2/resources/helper_functions.py b/app/api/v2/resources/helper_functions.py new file mode 100644 index 0000000..99645ac --- /dev/null +++ b/app/api/v2/resources/helper_functions.py @@ -0,0 +1,40 @@ +from datetime import datetime + +from flask import abort, jsonify, make_response + +from ..models import products, sale_orders, users +from .. import database + +def no_json_in_request(data): + """Aborts if the data does + + not contain a json object + """ + + if data is None: + # If a json was not obtained from the request + abort(make_response(jsonify( + message="Bad request. Request data must be in json format"), 400)) + + +def missing_a_required_parameter(): + """Aborts if request data is missing a + + required argument + """ + abort(make_response(jsonify( + message="Bad request. Request missing a required argument"), 400)) + + +def abort_if_user_is_not_admin(user): + user_role = [users['role'] for users in users.USERS if users['email'] == user][0] + if user_role!= "admin": + abort(make_response(jsonify( + message="Unauthorized. This action is not for you" + ), 401)) + + +def add_product_to_store(product_name, product_price, category): + + added_product = products.Products(product_name, product_price, category) + added_product.save() \ No newline at end of file diff --git a/app/api/v2/resources/store_attendant_endpoints.py b/app/api/v2/resources/store_attendant_endpoints.py new file mode 100644 index 0000000..96597b5 --- /dev/null +++ b/app/api/v2/resources/store_attendant_endpoints.py @@ -0,0 +1,66 @@ +"""This module contains endpoints + +that are specific to the store attendant +""" +from flask import Flask, jsonify, request, abort, make_response +from flask_restful import Resource +from flask_jwt_extended import get_jwt_identity, jwt_required + +from . import helper_functions +from ..models import products, sale_orders +from . import verify + +class SaleRecords(Resource): + """Class contains the tests for store attendant + + specific endpoints + """ + + def post(self): + """POST /saleorder endpoint""" + + # verify.verify_tokens() + + data = request.get_json() + helper_functions.no_json_in_request(data) + try: + product_name = data['product_name'] + product_price = data['product_price'] + quantity = data['quantity'] + except KeyError: + # If product is missing required parameter + helper_functions.missing_a_required_parameter() + + if not isinstance(product_price, int): + abort(make_response(jsonify( + message="Bad request. The product price should be digits" + ), 400)) + + if product_price < 1: + abort(make_response(jsonify( + message="Bad request. Price of the product should be a positive integer above 0." + ), 400)) + + if not isinstance(product_name, str): + abort(make_response(jsonify( + message="Bad request. Product name should be a string" + ), 400)) + + + if not isinstance(quantity, int): + abort(make_response(jsonify( + message="Bad request. The quantity should be specified in digits" + ), 400)) + strip_product_name = product_name.strip() + sale_order = sale_orders.SaleOrder(strip_product_name, product_price, quantity) + sale_order.save() + + return make_response(jsonify({ + "message": "Checkout complete", + "saleorder": { + "product_name": product_name, + "product_price": product_price, + "quantity": quantity, + "amount": (product_price * quantity) + } + }), 201) diff --git a/app/api/v2/resources/verify.py b/app/api/v2/resources/verify.py new file mode 100644 index 0000000..31e3f64 --- /dev/null +++ b/app/api/v2/resources/verify.py @@ -0,0 +1,46 @@ +import os +import jwt +from functools import wraps + +from flask import request, make_response, jsonify, abort +from ..models import users + + +def verify_tokens(): + token = None + if 'Authorization' in request.headers: + token = request.headers['Authorization'] + if not token: + abort(make_response(jsonify({ + "Message": "You need to login"}), 401)) + try: + data = jwt.decode(token, os.getenv('JWT_SECRET_KEY', default='SdaHv342nx!jknr837bjwd?c,lsajjjhw673hdsbgeh')) + return data["email"] + + except: + abort(make_response(jsonify({ + "Message": "The token is either expired or wrong" + }), 403)) + + + +def verify_post_product_fields(product_price, product_name, category): + if not isinstance(product_price, int): + abort(make_response(jsonify( + message="Product price should be an integer" + ), 400)) + + if product_price < 1: + abort(make_response(jsonify( + message="Price of the product should be a positive integer above 0." + ), 400)) + + if not isinstance(product_name, str): + abort(make_response(jsonify( + message="Product name should be a string" + ), 400)) + + if not isinstance(category, str): + abort(make_response(jsonify( + message="Bad request. The Category should be a string" + ), 400)) \ No newline at end of file From 93051be437be72e959b0668a7f5b6fa18167fd08 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Thu, 25 Oct 2018 23:12:02 +0300 Subject: [PATCH 095/100] [Chore #161491969] Add data validation and routing --- app/api/v2/utils/__init__.py | 0 app/api/v2/utils/validator.py | 52 +++++++++++++++++++++++++++++++++++ app/api/v2/views.py | 20 ++++++++++++++ app/tests/v2/__init__.py | 0 4 files changed, 72 insertions(+) create mode 100644 app/api/v2/utils/__init__.py create mode 100644 app/api/v2/utils/validator.py create mode 100644 app/api/v2/views.py create mode 100644 app/tests/v2/__init__.py diff --git a/app/api/v2/utils/__init__.py b/app/api/v2/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/v2/utils/validator.py b/app/api/v2/utils/validator.py new file mode 100644 index 0000000..33d2f43 --- /dev/null +++ b/app/api/v2/utils/validator.py @@ -0,0 +1,52 @@ +import re + +from flask import make_response, jsonify, abort +from validate_email import validate_email + +from .. import database + +class Validator: + def validate_credentials(self, data): + self.email = data["email"].strip() + self.password = data["password"].strip() + self.role = data["role"].strip() + valid_email = validate_email(self.email) + + if self.email == "" or self.password == "" or self.role == "": + Message = "You are missing a required credential" + abort(400, Message) + if not valid_email: + Message = "Invalid email" + abort(400, Message) + elif len(self.password) < 6 or len(self.password) > 12: + Message = "Password must be long than 6 characters or less than 12" + abort(400, Message) + elif not any(char.isdigit() for char in self.password): + Message = "Password must have a digit" + abort(400, Message) + elif not any(char.isupper() for char in self.password): + Message = "Password must have an upper case character" + abort(400, Message) + elif not any(char.islower() for char in self.password): + Message = "Password must have a lower case character" + abort(400, Message) + elif not re.search("^.*(?=.*[@#$%^&+=]).*$", self.password): + Message = "Password must have a special charater" + abort(400, Message) + +def check_duplication(column, table, value): + """ + Check if a param is already in use, abort if in use + """ + query = """ + SELECT {} FROM {} WHERE {}.{} = '{}' + """.format(column, table, table, column, value) + + duplicated = database.select_from_db(query) + if duplicated: + print(duplicated) + + abort(make_response(jsonify( + message="Record already exists in the database"), 400)) + + diff --git a/app/api/v2/views.py b/app/api/v2/views.py new file mode 100644 index 0000000..4c2f85b --- /dev/null +++ b/app/api/v2/views.py @@ -0,0 +1,20 @@ +"""Define API endpoints as routes""" + +from flask_restful import Api, Resource + +from . import endpoint_v2_blueprint, auth_v2_blueprint +from .resources import admin_endpoints, general_users_endpoints, store_attendant_endpoints, auth + +API = Api(endpoint_v2_blueprint) +AUTH_API = Api(auth_v2_blueprint) + + +API.add_resource(admin_endpoints.ProductsManagement, '/products') +API.add_resource(general_users_endpoints.AllProducts, '/products') +API.add_resource(general_users_endpoints.SpecificProduct, '/products/') +API.add_resource(store_attendant_endpoints.SaleRecords, '/saleorder') +API.add_resource(admin_endpoints.SaleAttendantsManagement, '/saleorder') +API.add_resource(general_users_endpoints.SpecificSaleOrder, '/saleorder/') + +AUTH_API.add_resource(auth.SignUp, '/signup') +AUTH_API.add_resource(auth.Login, '/login') \ No newline at end of file diff --git a/app/tests/v2/__init__.py b/app/tests/v2/__init__.py new file mode 100644 index 0000000..e69de29 From 051d8960baa4fbaef6f9aa969e9fc7a269f9d18f Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Thu, 25 Oct 2018 23:17:52 +0300 Subject: [PATCH 096/100] [Chore #161209925] Add psycopg2 package for databases --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 5af5f19..22ab13e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,6 +24,7 @@ MarkupSafe==1.0 mccabe==0.6.1 more-itertools==4.3.0 pluggy==0.7.1 +psycopg2-binary==2.7.5 py==1.7.0 pycparser==2.19 PyJWT==1.6.4 From 802b7749feaef62d05db5c2afb7d0dbf52fdd598 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Thu, 25 Oct 2018 23:19:14 +0300 Subject: [PATCH 097/100] [Chore #161216309] Change travis command to cover version 2 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9f2fc29..9df8503 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ before_script: # command to run tests script: - - coverage run --source=app.api.v1 -m pytest app/tests/v1 -v -W error::UserWarning && coverage report + - coverage run --source=app.api -m pytest app/tests -v -W error::UserWarning && coverage report after_script: - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT From 49fe8767ede5db44489ee27695bf7b4f3a49a259 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Thu, 25 Oct 2018 23:55:22 +0300 Subject: [PATCH 098/100] [Chore #161216309] Add database creation commands --- .travis.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.travis.yml b/.travis.yml index 9df8503..2e0cd6c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,10 @@ env: global: - CC_TEST_REPORTER_ID=18007e474cdfcf70da58a682805aff9f88216339b0dce34454dc0bfcc988ea91 + - APP_SETTINGS="development" + - DATABASE_URL="dbname='storemanager' host='localhost' port='5432' user='postgres' password='Password12#'" + - DATABASE_TEST_URL="dbname='storemanagertest' host='localhost' port='5432' user='postgres' password='Password12#'" + - SECRET_KEY="mvangiffj38kncliu3yt7gvLWDNTDISFJWrk'\flQHJsdnlQI2ROH" language: python @@ -16,6 +20,8 @@ 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 + - psql -c 'CREATE DATABASE storemanager;' -U postgres + - psql -c 'CREATE DATABASE storemanagertest;' -U postgres # command to run tests script: From 88417179aeb0f0e582b5d2ead81de7087051150a Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Fri, 26 Oct 2018 21:12:59 +0300 Subject: [PATCH 099/100] [Chore #161209988] Remove version 2 of the application from version 1 Transfer version 2 of the application to the repository store-manager-api-v2 --- app/__init__.py | 6 - app/api/v2/__init__.py | 9 - app/api/v2/database.py | 129 ---------- app/api/v2/models/__init__.py | 0 app/api/v2/models/products.py | 37 --- app/api/v2/models/sale_orders.py | 38 --- app/api/v2/models/users.py | 30 --- app/api/v2/resources/__init__.py | 0 app/api/v2/resources/admin_endpoints.py | 80 ------ app/api/v2/resources/auth.py | 79 ------ .../v2/resources/general_users_endpoints.py | 75 ------ app/api/v2/resources/helper_functions.py | 40 --- .../v2/resources/store_attendant_endpoints.py | 66 ----- app/api/v2/resources/verify.py | 46 ---- app/api/v2/utils/__init__.py | 0 app/api/v2/utils/validator.py | 52 ---- app/api/v2/views.py | 20 -- app/tests/v2/__init__.py | 0 app/tests/v2/base_test.py | 93 ------- app/tests/v2/helper_functions.py | 7 - app/tests/v2/test_admin_endpoints.py | 162 ------------ app/tests/v2/test_app_working.py | 18 -- app/tests/v2/test_auth_endpoints.py | 234 ------------------ app/tests/v2/test_general_user_endpoints.py | 117 --------- .../v2/test_store_attendant_endpoints.py | 124 ---------- instance/config.py | 4 +- 26 files changed, 1 insertion(+), 1465 deletions(-) delete mode 100644 app/api/v2/__init__.py delete mode 100644 app/api/v2/database.py delete mode 100644 app/api/v2/models/__init__.py delete mode 100644 app/api/v2/models/products.py delete mode 100644 app/api/v2/models/sale_orders.py delete mode 100644 app/api/v2/models/users.py delete mode 100644 app/api/v2/resources/__init__.py delete mode 100644 app/api/v2/resources/admin_endpoints.py delete mode 100644 app/api/v2/resources/auth.py delete mode 100644 app/api/v2/resources/general_users_endpoints.py delete mode 100644 app/api/v2/resources/helper_functions.py delete mode 100644 app/api/v2/resources/store_attendant_endpoints.py delete mode 100644 app/api/v2/resources/verify.py delete mode 100644 app/api/v2/utils/__init__.py delete mode 100644 app/api/v2/utils/validator.py delete mode 100644 app/api/v2/views.py delete mode 100644 app/tests/v2/__init__.py delete mode 100644 app/tests/v2/base_test.py delete mode 100644 app/tests/v2/helper_functions.py delete mode 100644 app/tests/v2/test_admin_endpoints.py delete mode 100644 app/tests/v2/test_app_working.py delete mode 100644 app/tests/v2/test_auth_endpoints.py delete mode 100644 app/tests/v2/test_general_user_endpoints.py delete mode 100644 app/tests/v2/test_store_attendant_endpoints.py diff --git a/app/__init__.py b/app/__init__.py index fa53e87..acbc658 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -20,10 +20,4 @@ def create_app(config_name): from .api.v1 import auth_v1_blueprint as v1_blueprint app.register_blueprint(v1_blueprint, url_prefix='/api/v1/auth') - from .api.v2 import endpoint_v2_blueprint as v2_blueprint - app.register_blueprint(v2_blueprint, url_prefix='/api/v2') - - from .api.v2 import auth_v2_blueprint as v2_blueprint - app.register_blueprint(v2_blueprint, url_prefix='/api/v2/auth') - return app diff --git a/app/api/v2/__init__.py b/app/api/v2/__init__.py deleted file mode 100644 index 230ffcb..0000000 --- a/app/api/v2/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from flask import Blueprint - - -endpoint_v2_blueprint = Blueprint('endpoint_v2_blueprint', __name__) -auth_v2_blueprint = Blueprint('auth_v2_blueprint', __name__) - - - -from . import views \ No newline at end of file diff --git a/app/api/v2/database.py b/app/api/v2/database.py deleted file mode 100644 index 4e298b6..0000000 --- a/app/api/v2/database.py +++ /dev/null @@ -1,129 +0,0 @@ -"""Module creates a connection to the database - -Creates tables for the application -""" - -import sys - -import psycopg2 -from instance.config import config - -def init_db(db_url=None): - """Initialize db connection - - Run queries that set up tables - """ - try: - conn, cursor = query_database() - queries = drop_table_if_exists() + create_tables() - i = 0 - while i != len(queries): - query = queries[i] - cursor.execute(query) - conn.commit() - i += 1 - conn.close() - - except Exception as error: - print("\nQuery not executed : {} \n".format(error)) - - -def create_tables(): - """Queries for setting up the database tables""" - - users_table_query = """ - CREATE TABLE users ( - user_id SERIAL PRIMARY KEY, - email VARCHAR (30) NOT NULL UNIQUE, - password VARCHAR (128) NOT NULL, - role VARCHAR (10) NOT NULL - )""" - - products_table_query = """ - CREATE TABLE products ( - product_id SERIAL PRIMARY KEY, - product_name VARCHAR (24) NOT NULL, - product_price INTEGER NOT NULL, - category VARCHAR (50) NOT NULL - )""" - - sales_order_query = """ - CREATE TABLE saleorders ( - sale_order_id SERIAL PRIMARY KEY, - date_ordered TIMESTAMP DEFAULT NOW(), - product_name VARCHAR (24) NOT NULL, - product_price INTEGER NOT NULL, - quantity INTEGER NOT NULL, - amount INTEGER NOT NULL - )""" - - return [users_table_query, products_table_query, sales_order_query] - - -def drop_table_if_exists(): - """Drop tables before recreating them""" - - drop_products_table = """ - DROP TABLE IF EXISTS products""" - - drop_sales_table = """ - DROP TABLE IF EXISTS saleorders""" - - drop_users_table = """ - DROP TABLE IF EXISTS users""" - - return [drop_products_table, drop_sales_table, drop_users_table] - - -def query_database(query=None, db_url=None): - """Creates a connection to the db - - Executes a query - """ - conn = None - if db_url is None: - db_url = config['db_url'] - try: - # connect to db - conn = psycopg2.connect(db_url) - print("\n\nConnected {}\n".format(conn.get_dsn_parameters())) - cursor = conn.cursor() - - if query: - cursor.execute(query) - conn.commit() - - except(Exception, - psycopg2.DatabaseError, - psycopg2.ProgrammingError) as error: - print("DB ERROR: {}".format(error)) - - return conn, cursor - - -def insert_to_db(query): - """Handles INSERT queries""" - - try: - conn = query_database(query)[0] - conn.close() - except psycopg2.Error as error: - print("Insertion error: {}".format(error)) - sys.exit(1) - - -def select_from_db(query): - """Handles SELECT queries""" - - fetched_content = None - conn, cursor = query_database(query) - if conn: - fetched_content = cursor.fetchall() - conn.close() - - return fetched_content - - -if __name__ == '__main__': - init_db() - query_database() diff --git a/app/api/v2/models/__init__.py b/app/api/v2/models/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/api/v2/models/products.py b/app/api/v2/models/products.py deleted file mode 100644 index 4e06e97..0000000 --- a/app/api/v2/models/products.py +++ /dev/null @@ -1,37 +0,0 @@ -"""This module contains the data store - -and data logic of the store's products -""" -from .. import database - -class Products(): - def __init__(self, product_name, product_price, category): - self.product_name = product_name - self.product_price = product_price - self.category = category - - - def save(self): - query = """ - INSERT INTO products(product_name, product_price, category) VALUES( - '{}', '{}', '{}' - )""".format(self.product_name, self.product_price, self.category) - - database.insert_to_db(query) - - @staticmethod - def fetch_product(product_name): - """ - Queries db for user with given username - Returns user object - """ - # Query db for user with those params - query = """ - SELECT * FROM products - WHERE product_name = '{}'""".format(product_name) - - return database.select_from_db(query) - - @staticmethod - def fetch_products(query): - return database.select_from_db(query) diff --git a/app/api/v2/models/sale_orders.py b/app/api/v2/models/sale_orders.py deleted file mode 100644 index 31133f6..0000000 --- a/app/api/v2/models/sale_orders.py +++ /dev/null @@ -1,38 +0,0 @@ -"""This module contains the data store - -and data logic of the store attendant's sale orders -""" -import datetime -from flask import jsonify - -from .. import database - -class SaleOrder(): - def __init__(self, product_name, product_price, quantity): - self.product_name = product_name - self.product_price = product_price - self.quantity = quantity - self.amount = (self.quantity * self.product_price) - self.date = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - def save(self): - query = """ - INSERT INTO saleorders(product_name, product_price, quantity, amount, date_ordered) VALUES( - '{}', '{}', '{}', '{}', '{}' - )""".format(self.product_name, self.product_price, self.quantity, self.amount, self.date) - - database.insert_to_db(query) - - - @staticmethod - def fetch_saleorder(product_name): - """ - Queries db for user with given username - Returns user object - """ - # Query db for user with those params - query = """ - SELECT * FROM saleorders - WHERE product_name = '{}'""".format(product_name) - - return database.select_from_db(query) \ No newline at end of file diff --git a/app/api/v2/models/users.py b/app/api/v2/models/users.py deleted file mode 100644 index 7b47417..0000000 --- a/app/api/v2/models/users.py +++ /dev/null @@ -1,30 +0,0 @@ -from flask import make_response, jsonify - -from .. import database - -class User_Model(): - def __init__(self, email, password, role): - self.email = email - self.password = password - self.role = role - - def save(self): - query = """ - INSERT INTO users(email, role, password) VALUES( - '{}', '{}', '{}' - )""".format(self.email, self.role, self.password) - - database.insert_to_db(query) - - @staticmethod - def fetch_user(email): - """ - Queries db for user with given username - Returns user object - """ - # Query db for user with those params - query = """ - SELECT * FROM users - WHERE email = '{}'""".format(email) - - return database.select_from_db(query) \ No newline at end of file diff --git a/app/api/v2/resources/__init__.py b/app/api/v2/resources/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/api/v2/resources/admin_endpoints.py b/app/api/v2/resources/admin_endpoints.py deleted file mode 100644 index 15cbfc8..0000000 --- a/app/api/v2/resources/admin_endpoints.py +++ /dev/null @@ -1,80 +0,0 @@ -"""This module contains endpoints - -that are specific to the admin -""" -import os -import jwt -from functools import wraps - -from flask import Flask, jsonify, request, abort, make_response -from flask_restful import Resource -from flask_jwt_extended import get_jwt_identity, jwt_required - -from . import helper_functions -from ..models import products, sale_orders, users -from . import verify -from .. import database -from ..utils import validator - - -class ProductsManagement(Resource): - """Class contains admin specific endpoints""" - - - def post(self): - """POST /products endpoint""" - - # Token verification and admin user determination - # logged_user = verify.verify_tokens() - # helper_functions.abort_if_user_is_not_admin(logged_user) - - data = request.get_json() - helper_functions.no_json_in_request(data) - try: - product_name = data['product_name'] - product_price = data['product_price'] - category = data['category'] - except KeyError: - # If product is missing required parameter - helper_functions.missing_a_required_parameter() - - validator.check_duplication("product_name", "products", product_name) - verify.verify_post_product_fields(product_price, product_name, category) - - added_product = products.Products(product_name, product_price, category) - added_product.save() - - return make_response(jsonify({ - "message": "Product added successfully", - "product": { - "product_name": product_name, - "product_price": product_price, - "category": category - } - }), 201) - - -class SaleAttendantsManagement(Resource): - """Class contains the tests managing - - sale orders - """ - - def get(self): - """GET /saleorder endpoint""" - - # verify.verify_tokens() - - query = """SELECT * FROM saleorders""" - saleorders = database.select_from_db(query) - if not saleorders: - return jsonify({ - 'message': "No sale orders created yet" - }) - - response = jsonify({ - 'message': "Successfully fetched all the sale orders", - 'sale_orders': saleorders - }) - response.status_code = 200 - return response diff --git a/app/api/v2/resources/auth.py b/app/api/v2/resources/auth.py deleted file mode 100644 index 29d9e80..0000000 --- a/app/api/v2/resources/auth.py +++ /dev/null @@ -1,79 +0,0 @@ -import os -import datetime -import jwt -from werkzeug.security import generate_password_hash, check_password_hash -from functools import wraps - -from flask import Flask, jsonify, request, make_response, abort -from flask_restful import Resource -from flask_jwt_extended import ( - jwt_required, create_access_token, - get_jwt_identity -) - -from instance import config -from ..utils.validator import Validator, check_duplication -from ..models import users - -class SignUp(Resource): - def post(self): - data = request.get_json() - if not data: - return make_response(jsonify({ - "message": "Missing required credentials" - }), 400) - try: - email = data["email"].strip() - request_password = data["password"].strip() - request_role = data["role"].strip() - except KeyError: - return make_response(jsonify({ - "message": "Missing required credentials" - }), 400) - - Validator.validate_credentials(self, data) - check_duplication("email", "users", email) - hashed_password = generate_password_hash(request_password, method='sha256') - user = users.User_Model(email, hashed_password, request_role) - user.save() - return make_response(jsonify({ - "message": "Account created successfully", - "user": { - "email": email, - "role": request_role - } - }), 202) - - -class Login(Resource): - def post(self): - data = request.get_json() - if not data: - return make_response(jsonify({ - "message": "Kindly enter your credentials" - } - ), 400) - request_email = data["email"].strip() - request_password = data["password"].strip() - - user = users.User_Model.fetch_user(request_email) - if not user: - abort(make_response(jsonify( - message="User not found."), 404)) - - user_email = user[0][1] - user_password = user[0][2] - - if request_email == user_email and check_password_hash(user_password, request_password): - token = jwt.encode({ - "email": request_email, - "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30) - }, os.getenv('JWT_SECRET_KEY', default='SdaHv342nx!jknr837bjwd?c,lsajjjhw673hdsbgeh')) - return make_response(jsonify({ - "message": "Login successful", - "token": token.decode("UTF-8")}), 200) - - return make_response(jsonify({ - "message": "Wrong credentials provided" - } - ), 403) diff --git a/app/api/v2/resources/general_users_endpoints.py b/app/api/v2/resources/general_users_endpoints.py deleted file mode 100644 index b29fef8..0000000 --- a/app/api/v2/resources/general_users_endpoints.py +++ /dev/null @@ -1,75 +0,0 @@ -"""This module contains endpoints - -for both the admin and the normal user -""" -from flask import jsonify, abort, make_response -from flask_restful import Resource -from flask_jwt_extended import get_jwt_identity, jwt_required -from . import verify - -from . import helper_functions -from ..models import products, sale_orders -from .. import database - -class AllProducts(Resource): - """Class contains the tests for both - - the admin and the normal user endpoints - """ - - def get(self): - """GET /products endpoint""" - verify.verify_tokens() - query = """SELECT * FROM products""" - fetched_products = products.Products.fetch_products(query) - if not fetched_products: - return make_response(jsonify({ - "message": "There are no products in the store yet", - }), 404) - - response = jsonify({ - 'message': "Successfully fetched all the products", - 'products': fetched_products - }) - - response.status_code = 200 - return response - -class SpecificProduct(Resource): - - def get(self, product_id): - - # verify.verify_tokens() - query = """SELECT * FROM products WHERE product_id = '{}'""".format(product_id) - - fetched_product = database.select_from_db(query) - if not fetched_product: - return make_response(jsonify({ - "message": "Product with id {} is not available".format(product_id), - }), 400) - - return make_response(jsonify({ - "message": "{} retrieved successfully".format(fetched_product[0][1]), - "product": fetched_product - }), 200) - - -class SpecificSaleOrder(Resource): - - def get(self, sale_order_id): - """GET /saleorder/""" - - # verify.verify_tokens() - query = """SELECT * FROM saleorders WHERE sale_order_id = '{}'""".format(sale_order_id) - sale_order = database.select_from_db(query) - if not sale_order: - return make_response(jsonify({ - "message": "Sale Order with id {} not found".format(sale_order_id) - } - ), 404) - - return make_response(jsonify({ - "message": "Sale order fetched successfully", - "saleorder": sale_order - } - ), 200) \ No newline at end of file diff --git a/app/api/v2/resources/helper_functions.py b/app/api/v2/resources/helper_functions.py deleted file mode 100644 index 99645ac..0000000 --- a/app/api/v2/resources/helper_functions.py +++ /dev/null @@ -1,40 +0,0 @@ -from datetime import datetime - -from flask import abort, jsonify, make_response - -from ..models import products, sale_orders, users -from .. import database - -def no_json_in_request(data): - """Aborts if the data does - - not contain a json object - """ - - if data is None: - # If a json was not obtained from the request - abort(make_response(jsonify( - message="Bad request. Request data must be in json format"), 400)) - - -def missing_a_required_parameter(): - """Aborts if request data is missing a - - required argument - """ - abort(make_response(jsonify( - message="Bad request. Request missing a required argument"), 400)) - - -def abort_if_user_is_not_admin(user): - user_role = [users['role'] for users in users.USERS if users['email'] == user][0] - if user_role!= "admin": - abort(make_response(jsonify( - message="Unauthorized. This action is not for you" - ), 401)) - - -def add_product_to_store(product_name, product_price, category): - - added_product = products.Products(product_name, product_price, category) - added_product.save() \ No newline at end of file diff --git a/app/api/v2/resources/store_attendant_endpoints.py b/app/api/v2/resources/store_attendant_endpoints.py deleted file mode 100644 index 96597b5..0000000 --- a/app/api/v2/resources/store_attendant_endpoints.py +++ /dev/null @@ -1,66 +0,0 @@ -"""This module contains endpoints - -that are specific to the store attendant -""" -from flask import Flask, jsonify, request, abort, make_response -from flask_restful import Resource -from flask_jwt_extended import get_jwt_identity, jwt_required - -from . import helper_functions -from ..models import products, sale_orders -from . import verify - -class SaleRecords(Resource): - """Class contains the tests for store attendant - - specific endpoints - """ - - def post(self): - """POST /saleorder endpoint""" - - # verify.verify_tokens() - - data = request.get_json() - helper_functions.no_json_in_request(data) - try: - product_name = data['product_name'] - product_price = data['product_price'] - quantity = data['quantity'] - except KeyError: - # If product is missing required parameter - helper_functions.missing_a_required_parameter() - - if not isinstance(product_price, int): - abort(make_response(jsonify( - message="Bad request. The product price should be digits" - ), 400)) - - if product_price < 1: - abort(make_response(jsonify( - message="Bad request. Price of the product should be a positive integer above 0." - ), 400)) - - if not isinstance(product_name, str): - abort(make_response(jsonify( - message="Bad request. Product name should be a string" - ), 400)) - - - if not isinstance(quantity, int): - abort(make_response(jsonify( - message="Bad request. The quantity should be specified in digits" - ), 400)) - strip_product_name = product_name.strip() - sale_order = sale_orders.SaleOrder(strip_product_name, product_price, quantity) - sale_order.save() - - return make_response(jsonify({ - "message": "Checkout complete", - "saleorder": { - "product_name": product_name, - "product_price": product_price, - "quantity": quantity, - "amount": (product_price * quantity) - } - }), 201) diff --git a/app/api/v2/resources/verify.py b/app/api/v2/resources/verify.py deleted file mode 100644 index 31e3f64..0000000 --- a/app/api/v2/resources/verify.py +++ /dev/null @@ -1,46 +0,0 @@ -import os -import jwt -from functools import wraps - -from flask import request, make_response, jsonify, abort -from ..models import users - - -def verify_tokens(): - token = None - if 'Authorization' in request.headers: - token = request.headers['Authorization'] - if not token: - abort(make_response(jsonify({ - "Message": "You need to login"}), 401)) - try: - data = jwt.decode(token, os.getenv('JWT_SECRET_KEY', default='SdaHv342nx!jknr837bjwd?c,lsajjjhw673hdsbgeh')) - return data["email"] - - except: - abort(make_response(jsonify({ - "Message": "The token is either expired or wrong" - }), 403)) - - - -def verify_post_product_fields(product_price, product_name, category): - if not isinstance(product_price, int): - abort(make_response(jsonify( - message="Product price should be an integer" - ), 400)) - - if product_price < 1: - abort(make_response(jsonify( - message="Price of the product should be a positive integer above 0." - ), 400)) - - if not isinstance(product_name, str): - abort(make_response(jsonify( - message="Product name should be a string" - ), 400)) - - if not isinstance(category, str): - abort(make_response(jsonify( - message="Bad request. The Category should be a string" - ), 400)) \ No newline at end of file diff --git a/app/api/v2/utils/__init__.py b/app/api/v2/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/api/v2/utils/validator.py b/app/api/v2/utils/validator.py deleted file mode 100644 index 33d2f43..0000000 --- a/app/api/v2/utils/validator.py +++ /dev/null @@ -1,52 +0,0 @@ -import re - -from flask import make_response, jsonify, abort -from validate_email import validate_email - -from .. import database - -class Validator: - def validate_credentials(self, data): - self.email = data["email"].strip() - self.password = data["password"].strip() - self.role = data["role"].strip() - valid_email = validate_email(self.email) - - if self.email == "" or self.password == "" or self.role == "": - Message = "You are missing a required credential" - abort(400, Message) - if not valid_email: - Message = "Invalid email" - abort(400, Message) - elif len(self.password) < 6 or len(self.password) > 12: - Message = "Password must be long than 6 characters or less than 12" - abort(400, Message) - elif not any(char.isdigit() for char in self.password): - Message = "Password must have a digit" - abort(400, Message) - elif not any(char.isupper() for char in self.password): - Message = "Password must have an upper case character" - abort(400, Message) - elif not any(char.islower() for char in self.password): - Message = "Password must have a lower case character" - abort(400, Message) - elif not re.search("^.*(?=.*[@#$%^&+=]).*$", self.password): - Message = "Password must have a special charater" - abort(400, Message) - -def check_duplication(column, table, value): - """ - Check if a param is already in use, abort if in use - """ - query = """ - SELECT {} FROM {} WHERE {}.{} = '{}' - """.format(column, table, table, column, value) - - duplicated = database.select_from_db(query) - if duplicated: - print(duplicated) - - abort(make_response(jsonify( - message="Record already exists in the database"), 400)) - - diff --git a/app/api/v2/views.py b/app/api/v2/views.py deleted file mode 100644 index 4c2f85b..0000000 --- a/app/api/v2/views.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Define API endpoints as routes""" - -from flask_restful import Api, Resource - -from . import endpoint_v2_blueprint, auth_v2_blueprint -from .resources import admin_endpoints, general_users_endpoints, store_attendant_endpoints, auth - -API = Api(endpoint_v2_blueprint) -AUTH_API = Api(auth_v2_blueprint) - - -API.add_resource(admin_endpoints.ProductsManagement, '/products') -API.add_resource(general_users_endpoints.AllProducts, '/products') -API.add_resource(general_users_endpoints.SpecificProduct, '/products/') -API.add_resource(store_attendant_endpoints.SaleRecords, '/saleorder') -API.add_resource(admin_endpoints.SaleAttendantsManagement, '/saleorder') -API.add_resource(general_users_endpoints.SpecificSaleOrder, '/saleorder/') - -AUTH_API.add_resource(auth.SignUp, '/signup') -AUTH_API.add_resource(auth.Login, '/login') \ No newline at end of file diff --git a/app/tests/v2/__init__.py b/app/tests/v2/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/tests/v2/base_test.py b/app/tests/v2/base_test.py deleted file mode 100644 index 82e28d4..0000000 --- a/app/tests/v2/base_test.py +++ /dev/null @@ -1,93 +0,0 @@ -""" - Contains the base test class for the - other test classes -""" -import unittest - -# local imports -from app import create_app -from instance.config import config -from . import helper_functions -from ...api.v2.database import init_db -from instance.config import config - - -class TestBaseClass(unittest.TestCase): - """Base test class""" - - - def setUp(self): - """Create and setup the application - - for testing purposes - """ - self.app = create_app('testing') - self.BASE_URL = 'api/v2' - self.app_context = self.app.app_context() - self.app_context.push() - self.app_test_client = self.app.test_client() - self.app.testing = True - - with self.app.app_context(): - self.db_url = config['test_db_url'] - init_db(self.db_url) - - self.PRODUCT = { - 'product_name': 'Phone Model 1', - 'product_price': 55000, - 'category': 'Phones' - } - - self.SALE_ORDERS = { - 'product_name': 'Phone Model 1', - 'product_price': 55000, - 'quantity': 6, - 'amount': (55000 * 6) - } - - - def tearDown(self): - """Destroy the application that - - is created for testing - """ - with self.app.app_context(): - init_db(self.db_url) - self.app_context.pop() - - def register_test_admin_account(self): - #Register attendant - """Registers an admin test user account""" - - res = self.app_test_client.post("api/v2/auth/signup", - json={ - "email": "user@gmail.com", - "role": "admin", - "password": "Password12#" - }, - headers={ - "Content-Type": "application/json" - }) - - return res - - def login_test_admin(self): - """Validates the test account for the admin""" - - # Login the test account for the admin - resp = self.app_test_client.post("api/v2/auth/login", - json={ - "email": "user@gmail.com", - "password": "Password12#" - }, - headers={ - "Content-Type": "application/json" - }) - - auth_token = helper_functions.convert_response_to_json( - resp)['token'] - - return auth_token - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/app/tests/v2/helper_functions.py b/app/tests/v2/helper_functions.py deleted file mode 100644 index 36e4717..0000000 --- a/app/tests/v2/helper_functions.py +++ /dev/null @@ -1,7 +0,0 @@ -import json - -def convert_response_to_json(response): - """Converts the response to a json type""" - - json_response = json.loads(response.data.decode('utf-8')) - return json_response \ No newline at end of file diff --git a/app/tests/v2/test_admin_endpoints.py b/app/tests/v2/test_admin_endpoints.py deleted file mode 100644 index 4018489..0000000 --- a/app/tests/v2/test_admin_endpoints.py +++ /dev/null @@ -1,162 +0,0 @@ -"""Module contains tests for admin - -specific endpoints -""" - -import json - -from flask import current_app - -from . import base_test -from . import helper_functions - -class TestAdminEndpoints(base_test.TestBaseClass): - """ Class contains tests for admin specific endpoints """ - - def test_add_new_product(self): - """Test POST /products""" - self.register_test_admin_account() - token = self.login_test_admin() - - # send a dummy data response for testing - response = self.app_test_client.post('{}/products'.format( - self.BASE_URL), json=self.PRODUCT, headers=dict(Authorization=token), - content_type='application/json') - - self.assertEqual(response.status_code, 201) - self.assertEqual(helper_functions.convert_response_to_json( - response)['product']['product_name'], self.PRODUCT['product_name']) - self.assertEqual(helper_functions.convert_response_to_json( - response)['product']['product_price'], self.PRODUCT['product_price']) - self.assertEqual(helper_functions.convert_response_to_json( - response)['product']['category'], self.PRODUCT['category']) - self.assertEqual(helper_functions.convert_response_to_json( - response)['message'], 'Product added successfully') - - def test_add_new_product_parameter_missing(self): - """Test POST /products - - with one of the required parameters missing - """ - self.register_test_admin_account() - token = self.login_test_admin() - - response = self.app_test_client.post('{}/products'.format( - self.BASE_URL), json={'product_name': 'Nyundo'}, headers=dict(Authorization=token), - content_type='application/json') - - self.assertEqual(response.status_code, 400) - self.assertEqual(helper_functions.convert_response_to_json( - response)['message'], 'Bad request. Request missing a required argument') - - def test_add_new_product_price_under_one(self): - """Test POST /products - - with the price of the product below minimum - """ - self.register_test_admin_account() - token = self.login_test_admin() - - response = self.app_test_client.post('{}/products'.format( - self.BASE_URL), json={ - 'product_id': 1, 'product_name': "Hammer", 'product_price': 0, 'category':'Tools' - }, headers=dict(Authorization=token), - content_type='application/json') - - self.assertEqual(response.status_code, 400) - self.assertEqual(helper_functions.convert_response_to_json( - response)['message'], - 'Price of the product should be a positive integer above 0.') - - - def test_add_new_product_with_product_name_not_string(self): - """Test POST /products - - with the product name not a string - """ - self.register_test_admin_account() - token = self.login_test_admin() - - response = self.app_test_client.post('{}/products'.format( - self.BASE_URL), json={ - 'product_id': 1, 'product_name': 200, 'product_price': 200, 'category':'Tools' - }, headers=dict(Authorization=token), - content_type='application/json') - - self.assertEqual(response.status_code, 400) - self.assertEqual(helper_functions.convert_response_to_json( - response)['message'], - 'Product name should be a string') - - def test_add_new_product_with_category_not_string(self): - """Test POST /products - - with the category not a string - """ - self.register_test_admin_account() - token = self.login_test_admin() - - response = self.app_test_client.post('{}/products'.format( - self.BASE_URL), json={ - 'product_id': 1, 'product_name': "Hammer", 'product_price': 200, 'category': 200 - }, headers=dict(Authorization=token), - content_type='application/json') - - self.assertEqual(response.status_code, 400) - self.assertEqual(helper_functions.convert_response_to_json( - response)['message'], - 'Bad request. The Category should be a string') - - def test_add_new_product_with_product_name_already_existing(self): - """Test POST /products - - with the product name already existing - """ - self.register_test_admin_account() - token = self.login_test_admin() - - self.app_test_client.post('{}/products'.format( - self.BASE_URL), json={ - 'product_id': 1, 'product_name': "Hammer", 'product_price': 200, 'category': "Tools" - }, headers=dict(Authorization=token), - content_type='application/json') - - response = self.app_test_client.post('{}/products'.format( - self.BASE_URL), json={ - 'product_id': 1, 'product_name': "Hammer", 'product_price': 200, 'category': "Tools" - }, headers=dict(Authorization=token), - content_type='application/json') - - self.assertEqual(response.status_code, 400) - self.assertEqual(helper_functions.convert_response_to_json( - response)['message'], - 'Record already exists in the database') - - - def test_fetch_sale_orders(self): - """Test GET /saleorder - when sale order exists""" - self.register_test_admin_account() - token = self.login_test_admin() - - self.app_test_client.post( - '{}/saleorder'.format(self.BASE_URL), json={ - 'product_name': "Test Product", - 'product_price': 20, - 'quantity': 1, - 'amount': 20 - }, - headers=dict(Authorization=token), - content_type='application/json') - - response = self.app_test_client.get( - '{}/saleorder'.format(self.BASE_URL), - - headers=dict(Authorization=token), - content_type='application/json' - ) - - self.assertEqual(response.status_code, 200) - self.assertEqual(helper_functions.convert_response_to_json( - response)['sale_orders'][0][2], "Test Product") - self.assertEqual(helper_functions.convert_response_to_json( - response)['sale_orders'][0][3], 20) diff --git a/app/tests/v2/test_app_working.py b/app/tests/v2/test_app_working.py deleted file mode 100644 index 4574c93..0000000 --- a/app/tests/v2/test_app_working.py +++ /dev/null @@ -1,18 +0,0 @@ -""" - Module contains tests for the working - of the application -""" -from flask import current_app - -#local imports -from . import base_test - -class TestConfigCase(base_test.TestBaseClass): - - - def test_app_exists(self): - self.assertFalse(current_app is None) - - - def test_app_is_testing(self): - self.assertTrue(current_app.config['TESTING']) \ No newline at end of file diff --git a/app/tests/v2/test_auth_endpoints.py b/app/tests/v2/test_auth_endpoints.py deleted file mode 100644 index 0a4b1b7..0000000 --- a/app/tests/v2/test_auth_endpoints.py +++ /dev/null @@ -1,234 +0,0 @@ -"""Module contains tests to endpoints that - -are used for user registration and authentication -""" - -import json - -from . import base_test -from . import helper_functions - -class TestAuthEndpoints(base_test.TestBaseClass): - """ Class contains tests for auth endpoints """ - - def test_add_new_user(self): - res = self.app_test_client.post("api/v2/auth/signup", - json={ - "email": "test_add_new_user@gmail.com", - "role": "Admin", - "password": "Password12#" - }, - headers={ - "Content-Type": "application/json" - }) - - data = json.loads(res.data.decode()) - print(data) - - self.assertEqual(data['user']['email'], "test_add_new_user@gmail.com") - self.assertEqual(res.status_code, 202) - - def test_add_new_user_no_data(self): - res = self.app_test_client.post("api/v2/auth/signup", - json={ - - }, - headers={ - "Content-Type": "application/json" - }) - - data = json.loads(res.data.decode()) - print(res) - - self.assertEqual(data['message'], "Missing required credentials") - self.assertEqual(res.status_code, 400) - - def test_add_new_user_missing_params(self): - res = self.app_test_client.post("api/v2/auth/signup", - json={ - "email": "", - "role": "Admin", - "password": "Password12#" - }, - headers={ - "Content-Type": "application/json" - }) - - data = json.loads(res.data.decode()) - print(data) - - self.assertEqual(data['message'], "You are missing a required credential") - self.assertEqual(res.status_code, 400) - - def test_add_new_user_invalid_email(self): - res = self.app_test_client.post("api/v2/auth/signup", - json={ - "email": "test_add_new_user", - "role": "Admin", - "password": "Password12#" - }, - headers={ - "Content-Type": "application/json" - }) - - data = json.loads(res.data.decode()) - print(data) - - self.assertEqual(data['message'], "Invalid email") - self.assertEqual(res.status_code, 400) - - def test_add_new_user_no_digit_password(self): - res = self.app_test_client.post("api/v2/auth/signup", - json={ - "email": "test_add_new_user_invalid_email@gmail.com", - "role": "Admin", - "password": "No#digit" - }, - headers={ - "Content-Type": "application/json" - }) - - data = json.loads(res.data.decode()) - print(data) - - self.assertEqual(data['message'], "Password must have a digit") - self.assertEqual(res.status_code, 400) - - def test_add_new_user_short_password(self): - res = self.app_test_client.post("api/v2/auth/signup", - json={ - "email": "test_add_new_user_invalid_email@gmail.com", - "role": "Admin", - "password": "Shor#" - }, - headers={ - "Content-Type": "application/json" - }) - - data = json.loads(res.data.decode()) - print(data) - - self.assertEqual(data['message'], "Password must be long than 6 characters or less than 12") - self.assertEqual(res.status_code, 400) - - def test_add_new_user_no_special_ch_password(self): - res = self.app_test_client.post("api/v2/auth/signup", - json={ - "email": "test_add_new_user_invalid_email@gmail.com", - "role": "Admin", - "password": "NoSplCh12" - }, - headers={ - "Content-Type": "application/json" - }) - - data = json.loads(res.data.decode()) - print(data) - - self.assertEqual(data['message'], "Password must have a special charater") - self.assertEqual(res.status_code, 400) - - def test_add_new_user_no_upper_case_password(self): - res = self.app_test_client.post("api/v2/auth/signup", - json={ - "email": "test_add_new_user_invalid_email@gmail.com", - "role": "Admin", - "password": "noupper12#" - }, - headers={ - "Content-Type": "application/json" - }) - - data = json.loads(res.data.decode()) - print(data) - - self.assertEqual(data['message'], "Password must have an upper case character") - self.assertEqual(res.status_code, 400) - - def test_add_new_user_no_lower_case_password(self): - res = self.app_test_client.post("api/v2/auth/signup", - json={ - "email": "test_add_new_user_invalid_email@gmail.com", - "role": "Admin", - "password": "NOLOWER12#" - }, - headers={ - "Content-Type": "application/json" - }) - - data = json.loads(res.data.decode()) - print(data) - - self.assertEqual(data['message'], "Password must have a lower case character") - self.assertEqual(res.status_code, 400) - - def test_add_new_user_existing(self): - """Test POST /auth/signup""" - self.register_test_admin_account() - response = self.register_test_admin_account() - data = json.loads(response.data.decode()) - - self.assertEqual(data['message'], "Record already exists in the database") - self.assertEqual(response.status_code, 400) - - def test_login_existing_user(self): - self.register_test_admin_account() - resp = self.app_test_client.post("api/v2/auth/login", - json={ - "email": "user@gmail.com", - "password": "Password12#" - }, - headers={ - "Content-Type": "application/json" - }) - - self.assertTrue(helper_functions.convert_response_to_json( - resp)['token']) - self.assertTrue(helper_functions.convert_response_to_json( - resp)['message'], "You are successfully logged in!") - self.assertEqual(resp.status_code, 200) - - def test_login_no_credentials(self): - self.register_test_admin_account() - resp = self.app_test_client.post("api/v2/auth/login", - json={ - - }, - headers={ - "Content-Type": "application/json" - }) - - self.assertTrue(helper_functions.convert_response_to_json( - resp)['message'], "Kindly enter your credentials") - self.assertEqual(resp.status_code, 400) - - def test_login_wrong_password(self): - self.register_test_admin_account() - resp = self.app_test_client.post("api/v2/auth/login", - json={ - "email": "user@gmail.com", - "password": "neverexpecteduser" - }, - headers={ - "Content-Type": "application/json" - }) - - self.assertTrue(helper_functions.convert_response_to_json( - resp)['message'], "Wrong credentials provided") - self.assertEqual(resp.status_code, 403) - - def test_login_non_existant_user(self): - self.register_test_admin_account() - resp = self.app_test_client.post("api/v2/auth/login", - json={ - "email": "non_matching_credentials_user_1018@gmail.com", - "password": "neverexpecteduser" - }, - headers={ - "Content-Type": "application/json" - }) - - self.assertTrue(helper_functions.convert_response_to_json( - resp)['message'], "User not found.") - self.assertEqual(resp.status_code, 404) - \ No newline at end of file diff --git a/app/tests/v2/test_general_user_endpoints.py b/app/tests/v2/test_general_user_endpoints.py deleted file mode 100644 index c783a9c..0000000 --- a/app/tests/v2/test_general_user_endpoints.py +++ /dev/null @@ -1,117 +0,0 @@ -"""Module contains tests to endpoints that - -are general to both the admin and the normal user -""" - -import json - -from . import base_test -from . import helper_functions - -class TestGeneralUsersEndpoints(base_test.TestBaseClass): - """Class contains the general user, i.e. both admin - - and normal user, endpoints' tests - """ - - - def test_retrieve_all_products(self): - """Test GET /products - when products exist""" - self.register_test_admin_account() - token = self.login_test_admin() - - # send a dummy data response for testing - self.app_test_client.post('{}/products'.format( - self.BASE_URL), json=self.PRODUCT, headers=dict(Authorization=token), - content_type='application/json') - - - response = self.app_test_client.get( - '{}/products'.format(self.BASE_URL), - headers=dict(Authorization=token), - content_type='application/json' - ) - - self.assertEqual(response.status_code, 200) - self.assertEqual(helper_functions.convert_response_to_json( - response)['products'][0][1], self.PRODUCT['product_name']) - - def test_retrieve_specific_product(self): - """Test GET /products/id - when product exist""" - - self.register_test_admin_account() - token = self.login_test_admin() - - # send a dummy data response for testing - self.app_test_client.post('{}/products'.format( - self.BASE_URL), json=self.PRODUCT, headers=dict(Authorization=token), - content_type='application/json') - - response = self.app_test_client.get( - '{}/products/1'.format(self.BASE_URL), - headers=dict(Authorization=token), - content_type='application/json' - ) - - self.assertEqual(response.status_code, 200) - self.assertEqual(helper_functions.convert_response_to_json( - response)['product'][0][1], self.PRODUCT['product_name']) - - def test_retrieve_specific_sale_order(self): - """Test GET /saleorder/id - when saleorder exists""" - - self.register_test_admin_account() - token = self.login_test_admin() - - self.app_test_client.post( - '{}/saleorder'.format(self.BASE_URL), json={ - 'sale_order_id': 1, - 'product_name': "Test Product", - 'product_price': 20, - 'quantity': 1, - 'amount': 20 - }, - headers=dict(Authorization=token), - content_type='application/json') - - response = self.app_test_client.get( - '{}/saleorder/1'.format(self.BASE_URL), - headers=dict(Authorization=token), - content_type='application/json' - ) - - self.assertEqual(response.status_code, 200) - - - def test_missing_token(self): - """Test GET /products - when token is missing""" - - self.register_test_admin_account() - token = "" - - response = self.app_test_client.get( - '{}/products'.format(self.BASE_URL), - headers=dict(Authorization=token), - content_type='application/json' - ) - - self.assertEqual(response.status_code, 401) - self.assertEqual(helper_functions.convert_response_to_json( - response)["Message"], "You need to login") - - - def test_invalid_token(self): - """Test GET /products - when token is missing""" - - self.register_test_admin_account() - token = "sample_invalid-token-afskdghkfhwkedaf-ksfakjfwey" - - response = self.app_test_client.get( - '{}/products'.format(self.BASE_URL), - headers=dict(Authorization=token), - content_type='application/json' - ) - - self.assertEqual(response.status_code, 403) - self.assertEqual(helper_functions.convert_response_to_json( - response)["Message"], "The token is either expired or wrong") \ No newline at end of file diff --git a/app/tests/v2/test_store_attendant_endpoints.py b/app/tests/v2/test_store_attendant_endpoints.py deleted file mode 100644 index c78d9b1..0000000 --- a/app/tests/v2/test_store_attendant_endpoints.py +++ /dev/null @@ -1,124 +0,0 @@ -"""Module contains tests for store attendant - -specific endpoints -""" -from flask import current_app - -from . import base_test -from . import helper_functions - -class TestAdminEndpoints(base_test.TestBaseClass): - """ Class contains tests for store attendant specific endpoints """ - - - def test_create_sale_order(self): - """Test POST /saleorder""" - self.register_test_admin_account() - token = self.login_test_admin() - - # send a dummy data response for testing - response = self.app_test_client.post('{}/saleorder'.format( - self.BASE_URL), json=self.SALE_ORDERS, headers=dict(Authorization=token), - content_type='application/json') - - self.assertEqual(response.status_code, 201) - self.assertEqual(helper_functions.convert_response_to_json( - response)['saleorder']['product_name'], self.SALE_ORDERS['product_name']) - self.assertEqual(helper_functions.convert_response_to_json( - response)['saleorder']['product_price'], self.SALE_ORDERS['product_price']) - self.assertEqual(helper_functions.convert_response_to_json( - response)['saleorder']['quantity'], self.SALE_ORDERS['quantity']) - self.assertEqual(helper_functions.convert_response_to_json( - response)['saleorder']['amount'], self.SALE_ORDERS['amount']) - self.assertEqual(helper_functions.convert_response_to_json( - response)['message'], 'Checkout complete') - - - def test_create_sale_order_parameter_missing(self): - """Test POST /saleorder - - with one of the required parameters missing - """ - self.register_test_admin_account() - token = self.login_test_admin() - - response = self.app_test_client.post('{}/saleorder'.format( - self.BASE_URL), json={'product_name': 'Nyundo'}, headers=dict(Authorization=token), - content_type='application/json') - - self.assertEqual(response.status_code, 400) - self.assertEqual(helper_functions.convert_response_to_json( - response)['message'], 'Bad request. Request missing a required argument') - - - def test_create_sale_order_price_below_one(self): - """Test POST /saleorder - - with the price not a valid integer - """ - self.register_test_admin_account() - token = self.login_test_admin() - - response = self.app_test_client.post('{}/saleorder'.format( - self.BASE_URL), json={'product_name': 'Nyundo', 'product_price': -1, 'quantity': 1}, - headers=dict(Authorization=token), - content_type='application/json') - - self.assertEqual(response.status_code, 400) - self.assertEqual(helper_functions.convert_response_to_json( - response)['message'], 'Bad request. Price of the product should be a positive integer above 0.') - - - def test_create_sale_order_invalid_product_name(self): - """Test POST /saleorder - - with the product name not a string - """ - self.register_test_admin_account() - token = self.login_test_admin() - - response = self.app_test_client.post('{}/saleorder'.format( - self.BASE_URL), json={'product_name': 3, 'product_price': 300, 'quantity': 1}, - headers=dict(Authorization=token), - content_type='application/json') - - self.assertEqual(response.status_code, 400) - self.assertEqual(helper_functions.convert_response_to_json( - response)['message'], 'Bad request. Product name should be a string') - - - def test_create_sale_order_price_not_digits(self): - """Test POST /saleorder - - with the price not a valid integer - """ - - self.register_test_admin_account() - token = self.login_test_admin() - - response = self.app_test_client.post('{}/saleorder'.format( - self.BASE_URL), json={'product_name': "Nyundo", 'product_price': "300", 'quantity': 1}, - headers=dict(Authorization=token), - content_type='application/json') - - self.assertEqual(response.status_code, 400) - self.assertEqual(helper_functions.convert_response_to_json( - response)['message'], 'Bad request. The product price should be digits') - - - def test_create_sale_order_invalid_quantity(self): - """Test POST /saleorder - - with the quantity not a valid integer - """ - self.register_test_admin_account() - token = self.login_test_admin() - - response = self.app_test_client.post('{}/saleorder'.format( - self.BASE_URL), json={'product_name': "Nyundo", 'product_price': 300, 'quantity': "1"}, - headers=dict(Authorization=token), - content_type='application/json') - - self.assertEqual(response.status_code, 400) - self.assertEqual(helper_functions.convert_response_to_json( - response)['message'], 'Bad request. The quantity should be specified in digits') \ No newline at end of file diff --git a/instance/config.py b/instance/config.py index a1eef27..7ef9c3e 100644 --- a/instance/config.py +++ b/instance/config.py @@ -31,7 +31,5 @@ class Production(Config): 'development': Development, 'testing': Testing, 'staging': Staging, - 'production': Production, - 'db_url': "dbname='storemanager' host='localhost' port='5432' user='postgres' password='Password12#'", - 'test_db_url': "dbname='storemanagertest' host='localhost' port='5432' user='postgres' password='Password12#'" + 'production': Production } \ No newline at end of file From 60b255110bdf72416f543c0729040bc43ecf6578 Mon Sep 17 00:00:00 2001 From: Caleb Rotich Date: Fri, 26 Oct 2018 21:19:46 +0300 Subject: [PATCH 100/100] [Chore #161216309] Remove database environment variables from .travis --- .travis.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2e0cd6c..c6d5171 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,6 @@ env: global: - CC_TEST_REPORTER_ID=18007e474cdfcf70da58a682805aff9f88216339b0dce34454dc0bfcc988ea91 - APP_SETTINGS="development" - - DATABASE_URL="dbname='storemanager' host='localhost' port='5432' user='postgres' password='Password12#'" - - DATABASE_TEST_URL="dbname='storemanagertest' host='localhost' port='5432' user='postgres' password='Password12#'" - SECRET_KEY="mvangiffj38kncliu3yt7gvLWDNTDISFJWrk'\flQHJsdnlQI2ROH" language: python @@ -20,12 +18,10 @@ 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 - - psql -c 'CREATE DATABASE storemanager;' -U postgres - - psql -c 'CREATE DATABASE storemanagertest;' -U postgres # command to run tests script: - - coverage run --source=app.api -m pytest app/tests -v -W error::UserWarning && coverage report + - coverage run --source=app.api.v1 -m pytest app/tests/v1 -v -W error::UserWarning && coverage report after_script: - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT