From f42a87df4ea4227d3b9beb3c9966a8e3705f57a2 Mon Sep 17 00:00:00 2001 From: Pav Sidhu Date: Mon, 1 Jul 2019 00:02:05 +0100 Subject: [PATCH 1/9] Setup easier database management using Docker --- .env | 2 +- README.md | 28 +++++++++++++++++----------- docker-compose.yml | 11 +++++++++++ package.json | 4 +++- 4 files changed, 32 insertions(+), 13 deletions(-) create mode 100644 docker-compose.yml diff --git a/.env b/.env index dfa2439..463e55c 100644 --- a/.env +++ b/.env @@ -1,2 +1,2 @@ -DATABASE_URL = "postgres://localhost:5432/postgres" +DATABASE_URL = "postgres://localhost:5432/feeling" PGUSER = "postgres" diff --git a/README.md b/README.md index fd11cfb..6981763 100644 --- a/README.md +++ b/README.md @@ -6,28 +6,28 @@ ## How To Use -1. Make sure Postgres is running with the uuid-ossp extension. Run the following on your Postgres server to install the extension: +1. Make sure you have Docker running. To start the database: -```sql -CREATE EXTENSION "uuid-ossp"; +```bash +npm run start-database ``` -2. Install Python libraries: +2. Install JavaScript libraries: ```bash -python3 -m venv venv -source venv/bin/activate -pip install -r requirements.txt +npm install -g serverless +npm install ``` -3. Install JavaScript libraries: +3. Install Python libraries: ```bash -npm install -g serverless -npm install +python3 -m venv venv +source venv/bin/activate +pip install -r requirements.txt ``` -4. Generate the tables: +4. Generate the database tables: ```bash python -m src.setup @@ -38,3 +38,9 @@ python -m src.setup ```bash npm run develop ``` + +6. To stop the database when you're finished: + +```bash +npm run stop-database +``` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c33b621 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +version: '3' +services: + database: + image: "fintrace/postgres-uuid" + container_name: "feeling-database" + environment: + - POSTGRES_DB=feeling + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD= + ports: + - "5432:5432" diff --git a/package.json b/package.json index 2ef6ba4..69ce6e2 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,9 @@ "scripts": { "develop": "sls offline start", "migrate": "alembic revision -m", - "upgrade": "alembic upgrade" + "upgrade": "alembic upgrade", + "start-database": "docker-compose up -d", + "stop-database": "docker-compose down" }, "devDependencies": { "serverless-offline-python": "^3.21.4", From 4d1a4e34bb0b703e2b4fafb86e435694c1b25a80 Mon Sep 17 00:00:00 2001 From: Pav Sidhu Date: Wed, 3 Jul 2019 14:53:30 +0100 Subject: [PATCH 2/9] Add pytest --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 135f830..933d097 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ black pylint +pytest SQLAlchemy==1.2.15 psycopg2-binary==2.7.6.1 python-dotenv==0.10.1 From e49e68550c12497213de994eadec8047a3cf5e9f Mon Sep 17 00:00:00 2001 From: Pav Sidhu Date: Wed, 3 Jul 2019 14:58:25 +0100 Subject: [PATCH 3/9] Add test command --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 69ce6e2..59a5064 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "develop": "sls offline start", "migrate": "alembic revision -m", "upgrade": "alembic upgrade", + "test": "pytest", "start-database": "docker-compose up -d", "stop-database": "docker-compose down" }, From 4c58fa43d3605959394829c1cff87720d9f8827d Mon Sep 17 00:00:00 2001 From: Pav Sidhu Date: Thu, 4 Jul 2019 16:46:10 +0100 Subject: [PATCH 4/9] Add tests setup --- .env | 1 + .gitignore | 3 +++ requirements.txt | 3 ++- src/__init__.py | 0 src/setup.py | 2 +- src/utils/decorators.py | 2 +- tests/__init__.py | 0 7 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 src/__init__.py create mode 100644 tests/__init__.py diff --git a/.env b/.env index 463e55c..2b258cf 100644 --- a/.env +++ b/.env @@ -1,2 +1,3 @@ DATABASE_URL = "postgres://localhost:5432/feeling" PGUSER = "postgres" +API_URL = "localhost:3000" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4751b83..19c9417 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ pip-delete-this-directory.txt # Virtual Environment venv +# pytest +.pytest_cache + # JavaScript node_modules diff --git a/requirements.txt b/requirements.txt index 933d097..37afb8c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,5 @@ psycopg2-binary==2.7.6.1 python-dotenv==0.10.1 alembic==1.0.5 argon2-cffi==18.3.0 -jsonschema==2.6.0 \ No newline at end of file +jsonschema==2.6.0 +requests==2.22.0 \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/setup.py b/src/setup.py index acc9b7b..059a89f 100644 --- a/src/setup.py +++ b/src/setup.py @@ -17,7 +17,7 @@ # Create tables if they do not exist Base.metadata.create_all(database) -# create a Session +# Create a Session Session = sessionmaker(bind=database) session = Session() diff --git a/src/utils/decorators.py b/src/utils/decorators.py index b4beabd..d82fc4f 100644 --- a/src/utils/decorators.py +++ b/src/utils/decorators.py @@ -74,7 +74,7 @@ def wrap_function(*args): def token_required(function): - """Decorator handle access tokens""" + """Decorator to handle access tokens""" def wrap_function(*args): event = args[0] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 From 8bf5462d9c50cb52a028e2b2235086f3cc1f71a7 Mon Sep 17 00:00:00 2001 From: Pav Sidhu Date: Thu, 4 Jul 2019 16:46:24 +0100 Subject: [PATCH 5/9] Change settings from put to post --- src/functions/settings.py | 4 ++-- src/models/Settings.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/functions/settings.py b/src/functions/settings.py index 0dd5d38..f3d846d 100644 --- a/src/functions/settings.py +++ b/src/functions/settings.py @@ -31,13 +31,13 @@ def get(event, context, session): @database @token_required @validate(schema) -def put(event, context, session): +def post(event, context, session): body = event["body"] user_id = event["user_id"] # Update settings in database settings = Settings(user_id, body) - session.add(settings) + session.merge(settings) session.commit() return {"statusCode": 200} diff --git a/src/models/Settings.py b/src/models/Settings.py index ba92fbe..14fcfdd 100644 --- a/src/models/Settings.py +++ b/src/models/Settings.py @@ -14,6 +14,9 @@ def __init__(self, user_id, settings): self.user_id = user_id self.settings = settings + def toJson(self): + return {"id": self.id, "user_id": self.user_id, "settings": self.settings} + def __repr__(self): return "".format( self.id, self.user_id, self.settings, self.created_at, self.updated_at From 87d1b377ff3bb2de6381b5b1da81d27f3ea12b31 Mon Sep 17 00:00:00 2001 From: Pav Sidhu Date: Thu, 4 Jul 2019 16:46:42 +0100 Subject: [PATCH 6/9] Update serverless.yml to reflect settings endpoint --- serverless.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/serverless.yml b/serverless.yml index 9113a3b..5e87b36 100644 --- a/serverless.yml +++ b/serverless.yml @@ -47,12 +47,12 @@ functions: method: delete cors: true - put_settings: - handler: src/functions/settings.put + post_settings: + handler: src/functions/settings.post events: - http: path: /settings - method: put + method: post cors: true get_settings: From 641cfdd6c9d5b896e0722393e0db4fad037b575b Mon Sep 17 00:00:00 2001 From: Pav Sidhu Date: Sat, 20 Jul 2019 14:09:10 +0100 Subject: [PATCH 7/9] Add quote tests --- .env | 2 +- tests/test_quote.py | 39 ++++++++++++++++++++++++++++++++++ tests/utils.py | 51 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 tests/test_quote.py create mode 100644 tests/utils.py diff --git a/.env b/.env index 2b258cf..129c1a6 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ DATABASE_URL = "postgres://localhost:5432/feeling" PGUSER = "postgres" -API_URL = "localhost:3000" \ No newline at end of file +API_URL = "http://localhost:3000" \ No newline at end of file diff --git a/tests/test_quote.py b/tests/test_quote.py new file mode 100644 index 0000000..45a6fc7 --- /dev/null +++ b/tests/test_quote.py @@ -0,0 +1,39 @@ +import requests +from dotenv import load_dotenv +from pytest import fixture + +from src.consts import Emotion +from src.models import Quote + +from .utils import base_url, session, token + + +@fixture +def quote(session): + """Adds a temporary quote to the database""" + + quote = Quote( + "To escape criticism: do nothing, say nothing, be nothing.", + "Elbert Hubbard", + Emotion.UPSET, + ) + session.add(quote) + session.commit() + + yield quote + + session.query(Quote).filter(Quote.id == quote.id).delete() + session.commit() + + +def test_get_a_quote(base_url, token, quote): + """Fetch a random quote""" + + emotion = quote.emotion.name.lower() + + url = f"{base_url}/quote/{emotion}" + response = requests.get(url) + json = response.json() + + assert response.status_code == 200 + assert json["emotion"].lower() == emotion diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..e10d9ab --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,51 @@ +import os + +from pytest import fixture +from dotenv import load_dotenv +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from src.models import User + + +@fixture() +def base_url(): + load_dotenv() + return os.getenv("API_URL") + + +@fixture() +def session(): + """Create a SQLAlchemy database session""" + + # Set environment variables from .env + load_dotenv() + + # Connect to database + database_url = os.getenv("DATABASE_URL") + database = create_engine(database_url) + + # Create a session + SessionMaker = sessionmaker(bind=database) + created_session = SessionMaker() + + yield created_session + + created_session.close() + + +@fixture +def user(session): + user = User("Michael", "Lee", "michael_lee@gmail.com", "password") + session.add(user) + session.commit() + + yield user + + session.query(User).filter(User.id == user.id).delete() + session.commit() + + +@fixture +def token(session): + return None From 6dace898fcee1b1fc84c307b577f822c4629f4a2 Mon Sep 17 00:00:00 2001 From: Pav Sidhu Date: Sat, 20 Jul 2019 14:11:45 +0100 Subject: [PATCH 8/9] Add known quote emotion test --- tests/test_quote.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_quote.py b/tests/test_quote.py index 45a6fc7..fdd62b4 100644 --- a/tests/test_quote.py +++ b/tests/test_quote.py @@ -37,3 +37,14 @@ def test_get_a_quote(base_url, token, quote): assert response.status_code == 200 assert json["emotion"].lower() == emotion + + +def test_get_a_quote_with_unknown_emotion(base_url, token, quote): + """Attempt to fetch a quote but emotion is not valid""" + + emotion = "meh" + + url = f"{base_url}/quote/{emotion}" + response = requests.get(url) + + assert response.status_code == 400 From ebdfe3aff806b7971dd65e0efd3320ebec97a9fb Mon Sep 17 00:00:00 2001 From: Pav Sidhu Date: Sat, 20 Jul 2019 14:30:56 +0100 Subject: [PATCH 9/9] Add settings test --- tests/test_settings.py | 50 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/test_settings.py diff --git a/tests/test_settings.py b/tests/test_settings.py new file mode 100644 index 0000000..1cd2acf --- /dev/null +++ b/tests/test_settings.py @@ -0,0 +1,50 @@ +import requests +from dotenv import load_dotenv +from pytest import fixture + +from src.models import Settings + +from .utils import base_url, session, token + + +@fixture +def settings(session, user): + settings = Settings( + user.id, + {"daily_reminder": {"enabled": True, "time": {"hour": 20, "minute": 00}}}, + ) + session.add(settings) + session.commit() + + yield settings + + session.query(Settings).filter(Settings.id == settings.id).delete() + session.commit() + + +def test_get_settings(base_url, token, settings): + """Fetch a user's settings""" + + url = f"{base_url}/settings" + response = requests.get(url, headers={"Authorization": f"Bearer {token}"}) + json = response.json() + + assert response.status_code == 200 + assert json.settings == settings.toJson() + + +def test_update_settings(base_url, token, settings): + """Update a user's settings""" + + new_settings = { + "daily_reminder": {"enabled": False, "time": {"hour": 21, "minute": 30}} + } + + url = f"{base_url}/settings" + response = requests.post( + url, headers={"Authorization": f"Bearer {token}"}, data=new_settings + ) + json = response.json() + + assert response.status_code == 200 + assert json.settings == new_settings