From e72b24246570728e95e91f4c3cad90e9e73e5934 Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Wed, 7 Dec 2016 11:02:44 -0500 Subject: [PATCH 01/24] Update dev/commands.py slightly --- dev/commands.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/dev/commands.py b/dev/commands.py index 5d9b041..52c8763 100755 --- a/dev/commands.py +++ b/dev/commands.py @@ -9,18 +9,19 @@ sys.path.insert(1, os.path.join(sys.path[0], '..')) -def createdb(): +def createdb(ensure=True): """Create the schema and tables and return a Session.""" from minigrid import models engine = models.create_engine() - models.Base.metadata.create_all(engine) + if ensure: + models.Base.metadata.create_all(engine) return sessionmaker(bind=engine, autocommit=True)() def create_user(*, email): """Create a user with the given e-mail.""" from minigrid import models - session = createdb() + session = createdb(ensure=False) with session.begin(): session.add(models.User(email=email)) print('Created user with e-mail ' + email) @@ -47,7 +48,7 @@ def main(): from minigrid.options import parse_command_line parse_command_line([None] + others) from minigrid.options import options - if 'db_schema' not in others: + if not any(other.startswith('--db_schema=') for other in others): options.db_schema = 'minigrid_dev' try: command = globals()[args.command_name] From 438e8e9832792d6f99019fe083ae1b732e2fcef6 Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Wed, 7 Dec 2016 13:19:31 -0500 Subject: [PATCH 02/24] Travis test runner fix --- tests/python/coverage_run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/python/coverage_run.sh b/tests/python/coverage_run.sh index 18fcb4d..3313031 100755 --- a/tests/python/coverage_run.sh +++ b/tests/python/coverage_run.sh @@ -1,6 +1,6 @@ #!/usr/bin/env sh set -e coverage erase -coverage run --source=minigrid,server.py --branch -m unittest ${@-discover tests.python} +coverage run --source=minigrid,server.py --branch -m unittest ${@:-discover tests.python} coverage html coverage report -m From 38b3aece089fca9632cebf3fb9c52fde9d4eb031 Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Wed, 7 Dec 2016 13:37:14 -0500 Subject: [PATCH 03/24] Test database connection attempts --- tests/python/test_server.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/python/test_server.py b/tests/python/test_server.py index e67028b..1663eb8 100644 --- a/tests/python/test_server.py +++ b/tests/python/test_server.py @@ -1,5 +1,7 @@ from unittest.mock import patch +from sqlalchemy.exc import OperationalError + from tests.python.util import Test from server import Application, main @@ -13,15 +15,23 @@ def test_b64dec_incorrect_padding(self, mock_logging): Application(debug=True) self.assertTrue(mock_logging.info.called) + @patch('server.models.Base.metadata.create_all') + @patch('server.sleep') + def test_wait_for_db(self, sleep, create_all): + create_all.side_effect = OperationalError(None, None, None) + with self.assertLogs(level='ERROR'): + self.assertRaises(OperationalError, Application) + self.assertEqual(len(create_all.mock_calls), 2) + self.assertTrue(sleep.called) + class TestServer(Test): @patch('server.AsyncIOMainLoop') @patch('server.print') - @patch('server.logging') @patch('server.get_event_loop') - def test_main(self, get_event_loop, mock_logging, mock_print, main_loop): - main() + def test_main(self, get_event_loop, mock_print, main_loop): + with self.assertLogs(level='INFO'): + main() self.assertTrue(main_loop().install.called) self.assertTrue(mock_print.called) - self.assertTrue(mock_logging.info.called) self.assertTrue(get_event_loop().run_forever.called) From 851d22e888bf53513b1091de18b76911d6258e1c Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Fri, 9 Dec 2016 12:04:43 -0500 Subject: [PATCH 04/24] Replace coveralls with codecov --- .travis.yml | 2 +- dev/requirements.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index fe2ec78..14a751d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ script: - xvfb-run --server-args="-screen 0, 1280x1280x16" tests/python/coverage_run.sh after_success: - - coveralls + - codecov notifications: email: diff --git a/dev/requirements.txt b/dev/requirements.txt index 0e4d80f..f562de2 100644 --- a/dev/requirements.txt +++ b/dev/requirements.txt @@ -1,3 +1,4 @@ +codecov fakeredis==0.8.1 flake8==3.2.1 pydocstyle==1.1.1 From f28d8e26e137cb340619328ab1c955eb6fc0c28d Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Fri, 9 Dec 2016 14:24:09 -0500 Subject: [PATCH 05/24] Add script to create initial user --- dev/commands.py | 1 + prod/create_initial_user.py | 36 ++++++++++++++++++++++++++++++++++++ prod/docker-compose.yml | 2 +- prod/install.sh | 19 ++++++++++++++----- 4 files changed, 52 insertions(+), 6 deletions(-) create mode 100755 prod/create_initial_user.py diff --git a/dev/commands.py b/dev/commands.py index 52c8763..6733684 100755 --- a/dev/commands.py +++ b/dev/commands.py @@ -15,6 +15,7 @@ def createdb(ensure=True): engine = models.create_engine() if ensure: models.Base.metadata.create_all(engine) + print('Created schema {}'.format(models.Base.metadata.schema)) return sessionmaker(bind=engine, autocommit=True)() diff --git a/prod/create_initial_user.py b/prod/create_initial_user.py new file mode 100755 index 0000000..d0035c7 --- /dev/null +++ b/prod/create_initial_user.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +"""Create the initial administrator for the application.""" +import argparse +import os +import sys + +from sqlalchemy import func +from sqlalchemy.orm import sessionmaker + +sys.path.insert(1, os.path.join(sys.path[0], '..')) + + +def main(): + """Supply the administrator's e-mail""" + parser = argparse.ArgumentParser() + parser.add_argument('email') + args, others = parser.parse_known_args() + from minigrid.options import parse_command_line + parse_command_line([None] + others) + from minigrid.options import options + if not any(other.startswith('--db_schema=') for other in others): + options.db_schema = 'minigrid_dev' + from minigrid import models + engine = models.create_engine() + session = sessionmaker(bind=engine, autocommit=True)() + users = session.query(func.count(models.User.user_id)).scalar() + if users: + print('At least one user already exists. Log in as that user.') + sys.exit(1) + with session.begin(): + session.add(models.User(email=args.email)) + print('Created initial user with e-mail', args.email) + + +if __name__ == '__main__': + main() diff --git a/prod/docker-compose.yml b/prod/docker-compose.yml index fb848f8..277c979 100644 --- a/prod/docker-compose.yml +++ b/prod/docker-compose.yml @@ -1,7 +1,7 @@ version: '2' services: minigrid: - image: selcolumbia/minigrid-server:0.0.7 + image: selcolumbia/minigrid-server:0.1.0 command: ./prod/run.sh --db_host=db --redis_url=redis://redis:6379/0 --minigrid-website-url=https://www.example.com depends_on: - redis diff --git a/prod/install.sh b/prod/install.sh index d943a21..430266f 100755 --- a/prod/install.sh +++ b/prod/install.sh @@ -1,5 +1,5 @@ #!/usr/bin/env sh -# Minigrid Server installer for version 0.0.7 +# Minigrid Server installer for version 0.1.0 set -e # Do you have docker installed? @@ -106,14 +106,21 @@ $SUDO openssl dhparam -out /etc/letsencrypt/live/$LETSENCRYPT_DIR/dhparam.pem 20 # Download the configuration files printf "========================================\n" -printf " Downloading configuration files \n" +printf " Generating configuration \n" printf "========================================\n" -$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.0.7/prod/docker-compose.yml > docker-compose.yml -$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.0.7/prod/nginx.conf > nginx.conf +$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.0/prod/docker-compose.yml > docker-compose.yml +$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.0/prod/nginx.conf > nginx.conf sed -i s/www.example.com/$LETSENCRYPT_DIR/g docker-compose.yml sed -i s/www.example.com/$LETSENCRYPT_DIR/g nginx.conf +printf "\n" +printf "Please enter an e-mail address for the \n" +printf "administrator. This will be the only \n" +printf "account that can log in at first. \n" +printf "Administrator e-mail address:\n>>> " +read ADMIN_EMAIL + # Bring up the server printf "========================================\n" printf " Starting minigrid server. \n" @@ -126,7 +133,9 @@ if [ -f /etc/redhat-release ] ; then chcon -Rt svirt_sandbox_file_t . fi $DOCKER_COMPOSE up -d -NGINX_CONTAINER_NAME=$($DOCKER_COMPOSE ps | grep nginx | cut -d' ' -f1) +MINIGRID_CONTAINER_NAME=$($DOCKER_COMPOSE ps | grep _minigrid_ | cut -d' ' -f1) +docker exec $MINIGRID_CONTAINER_NAME "prod/create_initial_user.py $ADMIN_EMAIL" +NGINX_CONTAINER_NAME=$($DOCKER_COMPOSE ps | grep _nginx_ | cut -d' ' -f1) # Let's Encrypt auto-renew (for now this is a cron job). printf "========================================\n" From 855772ed69cd47438a682c83d360eb75de47fc24 Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Fri, 9 Dec 2016 14:45:41 -0500 Subject: [PATCH 06/24] Temporarily set version to 0.1.0.0 --- prod/docker-compose.yml | 2 +- prod/install.sh | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/prod/docker-compose.yml b/prod/docker-compose.yml index 277c979..4c63ab4 100644 --- a/prod/docker-compose.yml +++ b/prod/docker-compose.yml @@ -1,7 +1,7 @@ version: '2' services: minigrid: - image: selcolumbia/minigrid-server:0.1.0 + image: selcolumbia/minigrid-server:0.1.0.0 command: ./prod/run.sh --db_host=db --redis_url=redis://redis:6379/0 --minigrid-website-url=https://www.example.com depends_on: - redis diff --git a/prod/install.sh b/prod/install.sh index 430266f..0028131 100755 --- a/prod/install.sh +++ b/prod/install.sh @@ -1,5 +1,5 @@ #!/usr/bin/env sh -# Minigrid Server installer for version 0.1.0 +# Minigrid Server installer for version 0.1.0.0 set -e # Do you have docker installed? @@ -108,8 +108,8 @@ $SUDO openssl dhparam -out /etc/letsencrypt/live/$LETSENCRYPT_DIR/dhparam.pem 20 printf "========================================\n" printf " Generating configuration \n" printf "========================================\n" -$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.0/prod/docker-compose.yml > docker-compose.yml -$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.0/prod/nginx.conf > nginx.conf +$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.0.0/prod/docker-compose.yml > docker-compose.yml +$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.0.0/prod/nginx.conf > nginx.conf sed -i s/www.example.com/$LETSENCRYPT_DIR/g docker-compose.yml sed -i s/www.example.com/$LETSENCRYPT_DIR/g nginx.conf From ce5620eb18b9e71f14d4ff26ed5cb44b8c5af09a Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Fri, 9 Dec 2016 15:06:45 -0500 Subject: [PATCH 07/24] Update install script --- prod/create_initial_user.py | 2 +- prod/docker-compose.yml | 2 +- prod/install.sh | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/prod/create_initial_user.py b/prod/create_initial_user.py index d0035c7..66d36aa 100755 --- a/prod/create_initial_user.py +++ b/prod/create_initial_user.py @@ -19,7 +19,7 @@ def main(): parse_command_line([None] + others) from minigrid.options import options if not any(other.startswith('--db_schema=') for other in others): - options.db_schema = 'minigrid_dev' + options.db_schema = 'minigrid' from minigrid import models engine = models.create_engine() session = sessionmaker(bind=engine, autocommit=True)() diff --git a/prod/docker-compose.yml b/prod/docker-compose.yml index 4c63ab4..ae46464 100644 --- a/prod/docker-compose.yml +++ b/prod/docker-compose.yml @@ -1,7 +1,7 @@ version: '2' services: minigrid: - image: selcolumbia/minigrid-server:0.1.0.0 + image: selcolumbia/minigrid-server:0.1.0.1 command: ./prod/run.sh --db_host=db --redis_url=redis://redis:6379/0 --minigrid-website-url=https://www.example.com depends_on: - redis diff --git a/prod/install.sh b/prod/install.sh index 0028131..ac07f79 100755 --- a/prod/install.sh +++ b/prod/install.sh @@ -1,5 +1,5 @@ #!/usr/bin/env sh -# Minigrid Server installer for version 0.1.0.0 +# Minigrid Server installer for version 0.1.0.1 set -e # Do you have docker installed? @@ -108,8 +108,8 @@ $SUDO openssl dhparam -out /etc/letsencrypt/live/$LETSENCRYPT_DIR/dhparam.pem 20 printf "========================================\n" printf " Generating configuration \n" printf "========================================\n" -$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.0.0/prod/docker-compose.yml > docker-compose.yml -$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.0.0/prod/nginx.conf > nginx.conf +$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.0.1/prod/docker-compose.yml > docker-compose.yml +$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.0.1/prod/nginx.conf > nginx.conf sed -i s/www.example.com/$LETSENCRYPT_DIR/g docker-compose.yml sed -i s/www.example.com/$LETSENCRYPT_DIR/g nginx.conf @@ -134,7 +134,7 @@ if [ -f /etc/redhat-release ] ; then fi $DOCKER_COMPOSE up -d MINIGRID_CONTAINER_NAME=$($DOCKER_COMPOSE ps | grep _minigrid_ | cut -d' ' -f1) -docker exec $MINIGRID_CONTAINER_NAME "prod/create_initial_user.py $ADMIN_EMAIL" +docker exec $MINIGRID_CONTAINER_NAME ""prod/create_initial_user.py --db-host=db $ADMIN_EMAIL"" NGINX_CONTAINER_NAME=$($DOCKER_COMPOSE ps | grep _nginx_ | cut -d' ' -f1) # Let's Encrypt auto-renew (for now this is a cron job). From ecb1dd393f0577756663aa422282746069ff1c3c Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Fri, 9 Dec 2016 15:37:40 -0500 Subject: [PATCH 08/24] Update install script --- prod/create_initial_user.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/prod/create_initial_user.py b/prod/create_initial_user.py index 66d36aa..5a2b8e5 100755 --- a/prod/create_initial_user.py +++ b/prod/create_initial_user.py @@ -3,8 +3,10 @@ import argparse import os import sys +from time import sleep from sqlalchemy import func +from sqlalchemy.exc import OperationalError from sqlalchemy.orm import sessionmaker sys.path.insert(1, os.path.join(sys.path[0], '..')) @@ -23,7 +25,12 @@ def main(): from minigrid import models engine = models.create_engine() session = sessionmaker(bind=engine, autocommit=True)() - users = session.query(func.count(models.User.user_id)).scalar() + try: + users = session.query(func.count(models.User.user_id)).scalar() + except OperationalError: + print('Database connection failed... trying again in 5 seconds.') + sleep(5) + users = session.query(func.count(models.User.user_id)).scalar() if users: print('At least one user already exists. Log in as that user.') sys.exit(1) From 6891830db8d47deb317bf2911e2dea554ed4ece2 Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Fri, 9 Dec 2016 15:38:21 -0500 Subject: [PATCH 09/24] Set version to 0.1.0.2 --- prod/docker-compose.yml | 2 +- prod/install.sh | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/prod/docker-compose.yml b/prod/docker-compose.yml index ae46464..57d4478 100644 --- a/prod/docker-compose.yml +++ b/prod/docker-compose.yml @@ -1,7 +1,7 @@ version: '2' services: minigrid: - image: selcolumbia/minigrid-server:0.1.0.1 + image: selcolumbia/minigrid-server:0.1.0.2 command: ./prod/run.sh --db_host=db --redis_url=redis://redis:6379/0 --minigrid-website-url=https://www.example.com depends_on: - redis diff --git a/prod/install.sh b/prod/install.sh index ac07f79..e1cdf2e 100755 --- a/prod/install.sh +++ b/prod/install.sh @@ -1,5 +1,5 @@ #!/usr/bin/env sh -# Minigrid Server installer for version 0.1.0.1 +# Minigrid Server installer for version 0.1.0.2 set -e # Do you have docker installed? @@ -108,8 +108,8 @@ $SUDO openssl dhparam -out /etc/letsencrypt/live/$LETSENCRYPT_DIR/dhparam.pem 20 printf "========================================\n" printf " Generating configuration \n" printf "========================================\n" -$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.0.1/prod/docker-compose.yml > docker-compose.yml -$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.0.1/prod/nginx.conf > nginx.conf +$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.0.2/prod/docker-compose.yml > docker-compose.yml +$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.0.2/prod/nginx.conf > nginx.conf sed -i s/www.example.com/$LETSENCRYPT_DIR/g docker-compose.yml sed -i s/www.example.com/$LETSENCRYPT_DIR/g nginx.conf From 0c6ef99dd80902b30c08d8f0b95977c3c5b17997 Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Fri, 9 Dec 2016 15:52:44 -0500 Subject: [PATCH 10/24] Improve install script slightly --- prod/install.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/prod/install.sh b/prod/install.sh index e1cdf2e..05ddf33 100755 --- a/prod/install.sh +++ b/prod/install.sh @@ -134,6 +134,7 @@ if [ -f /etc/redhat-release ] ; then fi $DOCKER_COMPOSE up -d MINIGRID_CONTAINER_NAME=$($DOCKER_COMPOSE ps | grep _minigrid_ | cut -d' ' -f1) +sleep 1 docker exec $MINIGRID_CONTAINER_NAME ""prod/create_initial_user.py --db-host=db $ADMIN_EMAIL"" NGINX_CONTAINER_NAME=$($DOCKER_COMPOSE ps | grep _nginx_ | cut -d' ' -f1) From f6fdffdbbab93a882aa75dfea7857d0991eb8425 Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Fri, 9 Dec 2016 16:20:05 -0500 Subject: [PATCH 11/24] First steps to minigrid list --- minigrid/handlers.py | 8 ++++- minigrid/models.py | 40 +++++++++++++++++++++ minigrid/templates/index-logged-out.html | 12 +++++++ minigrid/templates/index-minigrid-list.html | 10 ++++++ minigrid/templates/index.html | 15 -------- tests/python/fixture_minigrids.py | 18 ++++++++++ 6 files changed, 87 insertions(+), 16 deletions(-) create mode 100644 minigrid/templates/index-logged-out.html create mode 100644 minigrid/templates/index-minigrid-list.html delete mode 100644 minigrid/templates/index.html create mode 100755 tests/python/fixture_minigrids.py diff --git a/minigrid/handlers.py b/minigrid/handlers.py index fa665e7..7e33de1 100644 --- a/minigrid/handlers.py +++ b/minigrid/handlers.py @@ -46,7 +46,13 @@ class MainHandler(BaseHandler): def get(self): """Render the homepage.""" - self.render('index.html', reason=None) + if self.current_user: + minigrids = ( + self.session + .query(models.Minigrid).order_by(models.Minigrid.name)) + self.render('index-minigrid-list.html', minigrids=minigrids) + return + self.render('index-logged-out.html', reason=None) def post(self): """Send login information to the portier broker.""" diff --git a/minigrid/models.py b/minigrid/models.py index a30bbf9..91020d8 100644 --- a/minigrid/models.py +++ b/minigrid/models.py @@ -2,6 +2,7 @@ import sqlalchemy as sa from sqlalchemy.dialects import postgresql as pg from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.sql.functions import current_timestamp from sqlalchemy.sql import func from minigrid.options import options @@ -36,6 +37,25 @@ def pk(): pg.UUID, primary_key=True, server_default=func.uuid_generate_v4()) +def update_time(): + """Return a timestamp column set to CURRENT_TIMESTAMP by default.""" + return sa.Column( + pg.TIMESTAMP(timezone=True), + nullable=False, + server_default=current_timestamp(), + ) + + +def json_column(column_name, default=None): + """Return a JSONB column that is a dictionary at the top level.""" + return sa.Column( + pg.json.JSONB, + sa.CheckConstraint("{} @> '{{}}'".format(column_name)), + nullable=False, + server_default=default, + ) + + class User(Base): """The model for a registered user.""" @@ -45,3 +65,23 @@ class User(Base): pg.TEXT, sa.CheckConstraint("email ~ '.*@.*'"), nullable=False, unique=True, ) + + +class Minigrid(Base): + """The model for a minigrid.""" + + __tablename__ = 'minigrid' + minigrid_id = pk() + name = sa.Column( + pg.TEXT, sa.CheckConstraint("name != ''"), + nullable=False, unique=True) + day_tariff = sa.Column( + pg.NUMERIC, + sa.CheckConstraint('day_tariff > 0'), nullable=False) + day_tariff_update_time = update_time() + night_tariff = sa.Column( + pg.NUMERIC, + sa.CheckConstraint('night_tariff > 0'), nullable=False) + night_tariff_update_time = update_time() + error_code = json_column('error_code', default='{}') + status = json_column('error_code', default='{}') diff --git a/minigrid/templates/index-logged-out.html b/minigrid/templates/index-logged-out.html new file mode 100644 index 0000000..2bdd318 --- /dev/null +++ b/minigrid/templates/index-logged-out.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + +{% block body %} + {% if reason is not None %} +

Login unsuccessful: {{ reason }}

+ {% end %} +
+ {% module xsrf_form_html() %} + E-mail: + +
+{% end %} diff --git a/minigrid/templates/index-minigrid-list.html b/minigrid/templates/index-minigrid-list.html new file mode 100644 index 0000000..45a07bb --- /dev/null +++ b/minigrid/templates/index-minigrid-list.html @@ -0,0 +1,10 @@ +{% extends 'base.html' %} + +{% block body %} +

Minigrids:

+
    + {% for grid in minigrids %} +
  • {{ grid.name }}
  • + {% end %} +
+{% end %} diff --git a/minigrid/templates/index.html b/minigrid/templates/index.html deleted file mode 100644 index 3c90996..0000000 --- a/minigrid/templates/index.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends 'base.html' %} - -{% block body %} -

This is a page.

- {% if reason is not None %} -

Login unsuccessful: {{ reason }}

- {% end %} - {% if current_user is None %} -
- {% module xsrf_form_html() %} - E-mail: - -
- {% end %} -{% end %} diff --git a/tests/python/fixture_minigrids.py b/tests/python/fixture_minigrids.py new file mode 100755 index 0000000..7bd18e7 --- /dev/null +++ b/tests/python/fixture_minigrids.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +"""Minigrids fixture""" +import os +import sys + +sys.path.insert(1, os.path.join(sys.path[0], '../..')) + + +def main(): + from minigrid.options import parse_command_line + parse_command_line(sys.argv) + from minigrid import models + # TODO: create minigrid models + print(models) + + +if __name__ == '__main__': + main() From d43f535654c1600b5f3e94000b40d0d3677d735d Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Mon, 12 Dec 2016 16:56:09 -0500 Subject: [PATCH 12/24] Fix cron job sleep --- prod/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prod/install.sh b/prod/install.sh index d943a21..f307e13 100755 --- a/prod/install.sh +++ b/prod/install.sh @@ -146,6 +146,6 @@ CRON_CMD="mkdir -p /tmp/letsencrypt && "\ "if [ -f /tmp/renewed ] ; then docker restart $NGINX_CONTAINER_NAME ; fi ; "\ "rm -f /tmp/renewed" # https://certbot.eff.org/#ubuntuxenial-nginx recommends running this twice a day on random minute within the hour -CRON_JOB="00 01,13 * * * sleep \$(expr \$RANDOM \% 59); $CRON_CMD" +CRON_JOB="00 01,13 * * * sleep \$(expr \$RANDOM \% 59 \* 60); $CRON_CMD" crontab -l | fgrep -i -v "$CRON_CMD" | { cat; echo "$CRON_JOB"; } | crontab - crontab -l From 93588d5bc3033ee4b2ffc6b693959f50aa99f749 Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Tue, 13 Dec 2016 15:41:53 -0500 Subject: [PATCH 13/24] Fix login error page load --- minigrid/error.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minigrid/error.py b/minigrid/error.py index 33f9054..d17201a 100644 --- a/minigrid/error.py +++ b/minigrid/error.py @@ -21,7 +21,7 @@ class LoginError(MinigridHTTPError): def __init__(self, status_code=400, - template_name='index.html', + template_name='index-logged-out.html', *, reason): """Create a login error (400 by default).""" super().__init__(reason, status_code, template_name) From 398a115591237311931857c5af24f6b5d37181c9 Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Wed, 14 Dec 2016 12:56:52 -0500 Subject: [PATCH 14/24] Add minigrid display view and some tests --- minigrid/handlers.py | 20 +++++++++++++++++++ minigrid/models.py | 2 +- minigrid/templates/index-minigrid-list.html | 2 +- minigrid/templates/minigrid.html | 13 ++++++++++++ tests/python/test_handlers.py | 22 ++++++++++++++++++++- tests/python/util.py | 1 + 6 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 minigrid/templates/minigrid.html diff --git a/minigrid/handlers.py b/minigrid/handlers.py index 7e33de1..13e3aa8 100644 --- a/minigrid/handlers.py +++ b/minigrid/handlers.py @@ -3,6 +3,7 @@ from urllib.parse import urlencode from uuid import uuid4 +from sqlalchemy.exc import DataError from sqlalchemy.orm.exc import NoResultFound import tornado.web @@ -70,6 +71,24 @@ def post(self): self.redirect('https://broker.portier.io/auth?' + query_args) +class MinigridHandler(BaseHandler): + """Handlers for a minigrid view.""" + + @tornado.web.authenticated + def get(self, minigrid_id): + """Render the view for a minigrid record.""" + try: + minigrid = ( + self.session + .query(models.Minigrid) + .filter_by(minigrid_id=minigrid_id) + .one() + ) + except (NoResultFound, DataError): + raise tornado.web.HTTPError(404) + self.render('minigrid.html', minigrid=minigrid) + + class VerifyLoginHandler(BaseHandler): """Handlers for portier verification.""" @@ -121,6 +140,7 @@ def post(self): application_urls = [ (r'/', MainHandler), + (r'/minigrid/(.+)?', MinigridHandler), (r'/verify/?', VerifyLoginHandler), (r'/logout/?', LogoutHandler), ] diff --git a/minigrid/models.py b/minigrid/models.py index 91020d8..15fb13b 100644 --- a/minigrid/models.py +++ b/minigrid/models.py @@ -84,4 +84,4 @@ class Minigrid(Base): sa.CheckConstraint('night_tariff > 0'), nullable=False) night_tariff_update_time = update_time() error_code = json_column('error_code', default='{}') - status = json_column('error_code', default='{}') + status = json_column('status', default='{}') diff --git a/minigrid/templates/index-minigrid-list.html b/minigrid/templates/index-minigrid-list.html index 45a07bb..42e55b6 100644 --- a/minigrid/templates/index-minigrid-list.html +++ b/minigrid/templates/index-minigrid-list.html @@ -4,7 +4,7 @@

Minigrids:

    {% for grid in minigrids %} -
  • {{ grid.name }}
  • +
  • {{ grid.name }}
  • {% end %}
{% end %} diff --git a/minigrid/templates/minigrid.html b/minigrid/templates/minigrid.html new file mode 100644 index 0000000..f259402 --- /dev/null +++ b/minigrid/templates/minigrid.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} + +{% block body %} +

Minigrid Name: {{ minigrid.name }}

+

ID: {{ minigrid.minigrid_id }}

+

Day tariff: {{ minigrid.day_tariff }}

+

Last update of day tariff: {{ minigrid.day_tariff_update_time }}

+

Night tariff: {{ minigrid.night_tariff }}

+

Last update of night tariff: {{ minigrid.night_tariff_update_time }}

+

Error code: {{ minigrid.error_code }}

+

Status: {{ minigrid.status }}

+ +{% end %} diff --git a/tests/python/test_handlers.py b/tests/python/test_handlers.py index 91eae23..832c219 100644 --- a/tests/python/test_handlers.py +++ b/tests/python/test_handlers.py @@ -11,10 +11,30 @@ class TestIndex(HTTPTest): - def test_get(self): + def setUp(self): + super().setUp() + with self.session.begin(): + self.user = models.User(email='a@a.com') + self.session.add(self.user) + self.session.add_all(( + models.Minigrid(name='a', day_tariff=1, night_tariff=2), + models.Minigrid(name='b', day_tariff=10, night_tariff=20), + )) + + def test_get_not_logged_in(self): response = self.fetch('/') self.assertResponseCode(response, 200) self.assertNotIn('user', response.headers['Set-Cookie']) + self.assertIn('Log In', response.body.decode()) + self.assertNotIn('Log Out', response.body.decode()) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_get_logged_in(self, get_current_user): + get_current_user.return_value = self.user + response = self.fetch('/') + self.assertResponseCode(response, 200) + self.assertNotIn('Log In', response.body.decode()) + self.assertIn('Log Out', response.body.decode()) class TestXSRF(HTTPTest): diff --git a/tests/python/util.py b/tests/python/util.py index 32b7337..082e935 100644 --- a/tests/python/util.py +++ b/tests/python/util.py @@ -37,6 +37,7 @@ class Dummy: class Test(unittest.TestCase): def setUp(self): + shutil.rmtree(path('../tests/python/tmp'), ignore_errors=True) os.mkdir(path('../tests/python/tmp')) models.Base.metadata.create_all(engine) self.connection = engine.connect() From 8af97911306a53ca78d4c7c60d7c6f34aafff84b Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Wed, 14 Dec 2016 17:02:00 -0500 Subject: [PATCH 15/24] More tests --- tests/python/test_handlers.py | 62 +++++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/tests/python/test_handlers.py b/tests/python/test_handlers.py index 832c219..597006b 100644 --- a/tests/python/test_handlers.py +++ b/tests/python/test_handlers.py @@ -1,5 +1,8 @@ from unittest.mock import patch from urllib.parse import urlparse, parse_qs +import uuid + +from bs4 import BeautifulSoup as inconvenient_soup from tornado.testing import ExpectLog @@ -10,16 +13,20 @@ from server import Application +BeautifulSoup = lambda x: inconvenient_soup(x, 'html.parser') + + class TestIndex(HTTPTest): def setUp(self): super().setUp() with self.session.begin(): self.user = models.User(email='a@a.com') self.session.add(self.user) - self.session.add_all(( + self.minigrids = ( models.Minigrid(name='a', day_tariff=1, night_tariff=2), models.Minigrid(name='b', day_tariff=10, night_tariff=20), - )) + ) + self.session.add_all(self.minigrids) def test_get_not_logged_in(self): response = self.fetch('/') @@ -35,6 +42,57 @@ def test_get_logged_in(self, get_current_user): self.assertResponseCode(response, 200) self.assertNotIn('Log In', response.body.decode()) self.assertIn('Log Out', response.body.decode()) + body = BeautifulSoup(response.body) + minigrids = body.ul.findAll('li') + self.assertEqual(len(minigrids), 2) + self.assertEqual( + minigrids[0].a['href'], + '/minigrid/' + self.minigrids[0].minigrid_id, + ) + self.assertEqual(minigrids[0].a.text, self.minigrids[0].name) + + +class TestMinigridView(HTTPTest): + def setUp(self): + super().setUp() + with self.session.begin(): + self.user = models.User(email='a@a.com') + self.session.add(self.user) + self.minigrids = ( + models.Minigrid(name='a', day_tariff=1, night_tariff=2), + models.Minigrid(name='b', day_tariff=10, night_tariff=20), + ) + self.session.add_all(self.minigrids) + + def test_get_not_logged_in(self): + response = self.fetch( + '/minigrid/' + self.minigrids[0].minigrid_id, + follow_redirects=False, + ) + self.assertEqual(response.code, 302) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_get_malformed_id(self, get_current_user): + get_current_user.return_value = self.user + with ExpectLog('tornado.access', '404'): + response = self.fetch('/minigrid/' + 'nope') + self.assertEqual(response.code, 404) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_get_nonexistent_id(self, get_current_user): + get_current_user.return_value = self.user + with ExpectLog('tornado.access', '404'): + response = self.fetch('/minigrid/' + str(uuid.uuid4())) + self.assertEqual(response.code, 404) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_get_success(self, get_current_user): + get_current_user.return_value = self.user + response = self.fetch('/minigrid/' + self.minigrids[0].minigrid_id) + self.assertEqual(response.code, 200) + body = BeautifulSoup(response.body) + self.assertIn('Minigrid Name: a', body.h1) + self.assertIn('Day tariff: 1', body.findAll('p')[2]) class TestXSRF(HTTPTest): From fbc8634b4eb25f9e29458c28213c12946fc4e89d Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Wed, 14 Dec 2016 17:17:02 -0500 Subject: [PATCH 16/24] Make flake8 happy --- tests/python/test_handlers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/python/test_handlers.py b/tests/python/test_handlers.py index 597006b..1b0593e 100644 --- a/tests/python/test_handlers.py +++ b/tests/python/test_handlers.py @@ -13,7 +13,8 @@ from server import Application -BeautifulSoup = lambda x: inconvenient_soup(x, 'html.parser') +def BeautifulSoup(page): + return inconvenient_soup(page, 'html.parser') class TestIndex(HTTPTest): From 19a40d237a8c02cfe6f77663cd93886c0f9da530 Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Thu, 15 Dec 2016 17:09:05 -0500 Subject: [PATCH 17/24] Add user management --- minigrid/handlers.py | 28 ++++++++++++++++++++- minigrid/templates/index-logged-out.html | 2 +- minigrid/templates/index-minigrid-list.html | 6 ++++- minigrid/templates/users.html | 22 ++++++++++++++++ 4 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 minigrid/templates/users.html diff --git a/minigrid/handlers.py b/minigrid/handlers.py index 13e3aa8..3527604 100644 --- a/minigrid/handlers.py +++ b/minigrid/handlers.py @@ -3,7 +3,7 @@ from urllib.parse import urlencode from uuid import uuid4 -from sqlalchemy.exc import DataError +from sqlalchemy.exc import DataError, IntegrityError from sqlalchemy.orm.exc import NoResultFound import tornado.web @@ -71,6 +71,31 @@ def post(self): self.redirect('https://broker.portier.io/auth?' + query_args) +class UsersHandler(BaseHandler): + """Handlers for user management.""" + + def _render_users(self, reason=None): + users = self.session.query(models.User).order_by('email') + self.render('users.html', users=users, reason=reason) + + @tornado.web.authenticated + def get(self): + """Render the view for user management.""" + self._render_users() + + @tornado.web.authenticated + def post(self): + """Create a new user model.""" + email = self.get_argument('email') + reason = None + try: + with self.session.begin(): + self.session.add(models.User(email=email)) + except IntegrityError: + reason = 'Account for {} already exists'.format(email) + self._render_users(reason=reason) + + class MinigridHandler(BaseHandler): """Handlers for a minigrid view.""" @@ -141,6 +166,7 @@ def post(self): application_urls = [ (r'/', MainHandler), (r'/minigrid/(.+)?', MinigridHandler), + (r'/users/?', UsersHandler), (r'/verify/?', VerifyLoginHandler), (r'/logout/?', LogoutHandler), ] diff --git a/minigrid/templates/index-logged-out.html b/minigrid/templates/index-logged-out.html index 2bdd318..b09f7ce 100644 --- a/minigrid/templates/index-logged-out.html +++ b/minigrid/templates/index-logged-out.html @@ -2,7 +2,7 @@ {% block body %} {% if reason is not None %} -

Login unsuccessful: {{ reason }}

+

Login unsuccessful: {{ reason }}

{% end %}
{% module xsrf_form_html() %} diff --git a/minigrid/templates/index-minigrid-list.html b/minigrid/templates/index-minigrid-list.html index 42e55b6..739589b 100644 --- a/minigrid/templates/index-minigrid-list.html +++ b/minigrid/templates/index-minigrid-list.html @@ -1,10 +1,14 @@ {% extends 'base.html' %} {% block body %} +

Manage users »

Minigrids:

{% end %} diff --git a/minigrid/templates/users.html b/minigrid/templates/users.html new file mode 100644 index 0000000..a33fec6 --- /dev/null +++ b/minigrid/templates/users.html @@ -0,0 +1,22 @@ +{% extends 'base.html' %} + +{% block body %} +

Users:

+ +

Add user account:

+ + {% module xsrf_form_html() %} + E-mail: + +
+ {% if reason is not None %} +

Could not create user account: {{ reason }}

+ {% end %} + +{% end %} From 5f9f30690bc99669d45ce0a4902a0ceecf30acdb Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Thu, 15 Dec 2016 17:10:01 -0500 Subject: [PATCH 18/24] Fix test --- tests/python/test_handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/python/test_handlers.py b/tests/python/test_handlers.py index 1b0593e..76a9aa6 100644 --- a/tests/python/test_handlers.py +++ b/tests/python/test_handlers.py @@ -50,7 +50,7 @@ def test_get_logged_in(self, get_current_user): minigrids[0].a['href'], '/minigrid/' + self.minigrids[0].minigrid_id, ) - self.assertEqual(minigrids[0].a.text, self.minigrids[0].name) + self.assertEqual(minigrids[0].a.text, self.minigrids[0].name + ' ยป') class TestMinigridView(HTTPTest): From 9413729c993e65465bbf32d3b0a509edd4fbaf5d Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Fri, 16 Dec 2016 14:28:20 -0500 Subject: [PATCH 19/24] Use begin_nested instead of autocommit=True --- dev/commands.py | 4 ++-- minigrid/handlers.py | 4 ++-- prod/create_initial_user.py | 4 ++-- server.py | 2 +- tests/python/test_handlers.py | 6 +++--- tests/python/test_models.py | 2 +- tests/python/util.py | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/dev/commands.py b/dev/commands.py index 6733684..54d8a9d 100755 --- a/dev/commands.py +++ b/dev/commands.py @@ -16,14 +16,14 @@ def createdb(ensure=True): if ensure: models.Base.metadata.create_all(engine) print('Created schema {}'.format(models.Base.metadata.schema)) - return sessionmaker(bind=engine, autocommit=True)() + return sessionmaker(bind=engine)() def create_user(*, email): """Create a user with the given e-mail.""" from minigrid import models session = createdb(ensure=False) - with session.begin(): + with session.begin_nested(): session.add(models.User(email=email)) print('Created user with e-mail ' + email) diff --git a/minigrid/handlers.py b/minigrid/handlers.py index 3527604..ed6f662 100644 --- a/minigrid/handlers.py +++ b/minigrid/handlers.py @@ -19,7 +19,7 @@ class BaseHandler(tornado.web.RequestHandler): @property def session(self): - """The database session. Use session.begin() for transactions.""" + """The db session. Use session.begin_nested() for transactions.""" return self.application.session def get_current_user(self): @@ -89,7 +89,7 @@ def post(self): email = self.get_argument('email') reason = None try: - with self.session.begin(): + with self.session.begin_nested(): self.session.add(models.User(email=email)) except IntegrityError: reason = 'Account for {} already exists'.format(email) diff --git a/prod/create_initial_user.py b/prod/create_initial_user.py index 5a2b8e5..7826e66 100755 --- a/prod/create_initial_user.py +++ b/prod/create_initial_user.py @@ -24,7 +24,7 @@ def main(): options.db_schema = 'minigrid' from minigrid import models engine = models.create_engine() - session = sessionmaker(bind=engine, autocommit=True)() + session = sessionmaker(bind=engine)() try: users = session.query(func.count(models.User.user_id)).scalar() except OperationalError: @@ -34,7 +34,7 @@ def main(): if users: print('At least one user already exists. Log in as that user.') sys.exit(1) - with session.begin(): + with session.begin_nested(): session.add(models.User(email=args.email)) print('Created initial user with e-mail', args.email) diff --git a/server.py b/server.py index e5fa33d..e53a02c 100644 --- a/server.py +++ b/server.py @@ -37,7 +37,7 @@ def __init__(self, session=None, **kwargs): 'Database connection failed... trying again in 5 seconds.') sleep(5) models.Base.metadata.create_all(engine) - Session = sessionmaker(bind=engine, autocommit=True) + Session = sessionmaker(bind=engine) self.session = Session() else: self.session = session diff --git a/tests/python/test_handlers.py b/tests/python/test_handlers.py index 76a9aa6..594ff7b 100644 --- a/tests/python/test_handlers.py +++ b/tests/python/test_handlers.py @@ -20,7 +20,7 @@ def BeautifulSoup(page): class TestIndex(HTTPTest): def setUp(self): super().setUp() - with self.session.begin(): + with self.session.begin_nested(): self.user = models.User(email='a@a.com') self.session.add(self.user) self.minigrids = ( @@ -56,7 +56,7 @@ def test_get_logged_in(self, get_current_user): class TestMinigridView(HTTPTest): def setUp(self): super().setUp() - with self.session.begin(): + with self.session.begin_nested(): self.user = models.User(email='a@a.com') self.session.add(self.user) self.minigrids = ( @@ -118,7 +118,7 @@ def test_verify_no_xsrf(self): class TestAuthentication(HTTPTest): def create_user(self, email='a@a.com'): - with self.session.begin(): + with self.session.begin_nested(): self.session.add(models.User(email=email)) def test_login_missing_email(self): diff --git a/tests/python/test_models.py b/tests/python/test_models.py index 978f4e8..9df29c2 100644 --- a/tests/python/test_models.py +++ b/tests/python/test_models.py @@ -5,7 +5,7 @@ class TestUser(Test): def test_create(self): - with self.session.begin(): + with self.session.begin_nested(): self.session.add(models.User(email='a@b.com')) user = self.session.query(models.User).one() self.assertEqual(user.email, 'a@b.com') diff --git a/tests/python/util.py b/tests/python/util.py index 082e935..4ef5066 100644 --- a/tests/python/util.py +++ b/tests/python/util.py @@ -42,7 +42,7 @@ def setUp(self): models.Base.metadata.create_all(engine) self.connection = engine.connect() self.transaction = self.connection.begin() - self.session = Session(bind=self.connection, autocommit=True) + self.session = Session(bind=self.connection) super().setUp() def tearDown(self): From ac09d6cd7c05113e34c32053451f47dca561e5c0 Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Fri, 16 Dec 2016 14:28:53 -0500 Subject: [PATCH 20/24] Add user view tests --- tests/python/test_handlers.py | 67 ++++++++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/tests/python/test_handlers.py b/tests/python/test_handlers.py index 594ff7b..bb7cfec 100644 --- a/tests/python/test_handlers.py +++ b/tests/python/test_handlers.py @@ -70,32 +70,91 @@ def test_get_not_logged_in(self): '/minigrid/' + self.minigrids[0].minigrid_id, follow_redirects=False, ) - self.assertEqual(response.code, 302) + self.assertResponseCode(response, 302) @patch('minigrid.handlers.BaseHandler.get_current_user') def test_get_malformed_id(self, get_current_user): get_current_user.return_value = self.user with ExpectLog('tornado.access', '404'): response = self.fetch('/minigrid/' + 'nope') - self.assertEqual(response.code, 404) + self.assertResponseCode(response, 404) @patch('minigrid.handlers.BaseHandler.get_current_user') def test_get_nonexistent_id(self, get_current_user): get_current_user.return_value = self.user with ExpectLog('tornado.access', '404'): response = self.fetch('/minigrid/' + str(uuid.uuid4())) - self.assertEqual(response.code, 404) + self.assertResponseCode(response, 404) @patch('minigrid.handlers.BaseHandler.get_current_user') def test_get_success(self, get_current_user): get_current_user.return_value = self.user response = self.fetch('/minigrid/' + self.minigrids[0].minigrid_id) - self.assertEqual(response.code, 200) + self.assertResponseCode(response, 200) body = BeautifulSoup(response.body) self.assertIn('Minigrid Name: a', body.h1) self.assertIn('Day tariff: 1', body.findAll('p')[2]) +class TestUsersView(HTTPTest): + def setUp(self): + super().setUp() + with self.session.begin_nested(): + self.users = ( + models.User(email='a@a.com'), + models.User(email='b@b.com'), + ) + self.session.add_all(self.users) + + def test_get_not_logged_in(self): + response = self.fetch('/users', follow_redirects=False) + self.assertResponseCode(response, 302) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_get_logged_in(self, get_current_user): + get_current_user.return_value = self.users[0] + response = self.fetch('/users') + self.assertResponseCode(response, 200) + body = BeautifulSoup(response.body) + user_ul = body.ul.findAll('li') + self.assertEqual(user_ul[0].a['href'], 'mailto:a@a.com') + self.assertEqual(user_ul[1].a['href'], 'mailto:b@b.com') + + def test_post_not_logged_in(self): + with ExpectLog('tornado.access', '403'): + response = self.fetch('/users', method='POST', body='') + self.assertResponseCode(response, 403) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_post_empty_email(self, get_current_user): + get_current_user.return_value = self.users[0] + response = self.fetch('/users?email=', method='POST', body='') + self.assertIn('Could not create user account', response.body.decode()) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_post_invalid_email(self, get_current_user): + get_current_user.return_value = self.users[0] + response = self.fetch('/users?email=notanemail', method='POST', body='') + self.assertIn('Could not create user account', response.body.decode()) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_post_user_exists(self, get_current_user): + get_current_user.return_value = self.users[0] + response = self.fetch('/users?email=a@a.com', method='POST', body='') + self.assertIn( + 'Account for a@a.com already exists', response.body.decode()) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_post_success(self, get_current_user): + get_current_user.return_value = self.users[0] + response = self.fetch('/users?email=ba@a.com', method='POST', body='') + body = BeautifulSoup(response.body) + user_ul = body.ul.findAll('li') + self.assertEqual(user_ul[1].a['href'], 'mailto:ba@a.com') + self.assertIsNotNone( + self.session.query(models.User).filter_by(email='ba@a.com').one()) + + class TestXSRF(HTTPTest): def get_app(self): self.app = Application(self.session) From 785dd01a201b0fa951b07b0e48164865918942ac Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Fri, 16 Dec 2016 14:29:19 -0500 Subject: [PATCH 21/24] Fix line too long --- tests/python/test_handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/python/test_handlers.py b/tests/python/test_handlers.py index bb7cfec..cc0279a 100644 --- a/tests/python/test_handlers.py +++ b/tests/python/test_handlers.py @@ -134,7 +134,7 @@ def test_post_empty_email(self, get_current_user): @patch('minigrid.handlers.BaseHandler.get_current_user') def test_post_invalid_email(self, get_current_user): get_current_user.return_value = self.users[0] - response = self.fetch('/users?email=notanemail', method='POST', body='') + response = self.fetch('/users?email=notemail', method='POST', body='') self.assertIn('Could not create user account', response.body.decode()) @patch('minigrid.handlers.BaseHandler.get_current_user') From db8da7711abb03456ee87606e7a68d99eec3a8f8 Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Mon, 19 Dec 2016 11:12:22 -0500 Subject: [PATCH 22/24] Allow tests to run while server is running --- tests/python/test_server.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/python/test_server.py b/tests/python/test_server.py index 1663eb8..1902475 100644 --- a/tests/python/test_server.py +++ b/tests/python/test_server.py @@ -27,11 +27,13 @@ def test_wait_for_db(self, sleep, create_all): class TestServer(Test): @patch('server.AsyncIOMainLoop') + @patch('server.Application') @patch('server.print') @patch('server.get_event_loop') - def test_main(self, get_event_loop, mock_print, main_loop): + def test_main(self, get_event_loop, mock_print, application, main_loop): with self.assertLogs(level='INFO'): main() self.assertTrue(main_loop().install.called) + self.assertTrue(application().listen.called) self.assertTrue(mock_print.called) self.assertTrue(get_event_loop().run_forever.called) From e30c5b132e4aa23f618a2c83033ad30cd7d2f1a4 Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Mon, 19 Dec 2016 11:22:51 -0500 Subject: [PATCH 23/24] Improve user creation error messages --- minigrid/handlers.py | 7 +++++-- tests/python/test_handlers.py | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/minigrid/handlers.py b/minigrid/handlers.py index ed6f662..424f854 100644 --- a/minigrid/handlers.py +++ b/minigrid/handlers.py @@ -91,8 +91,11 @@ def post(self): try: with self.session.begin_nested(): self.session.add(models.User(email=email)) - except IntegrityError: - reason = 'Account for {} already exists'.format(email) + except IntegrityError as error: + if 'user_email_check' in error.orig.pgerror: + reason = '{} is not a valid e-mail address'.format(email) + else: + reason = 'Account for {} already exists'.format(email) self._render_users(reason=reason) diff --git a/tests/python/test_handlers.py b/tests/python/test_handlers.py index cc0279a..5d9ef4c 100644 --- a/tests/python/test_handlers.py +++ b/tests/python/test_handlers.py @@ -130,12 +130,14 @@ def test_post_empty_email(self, get_current_user): get_current_user.return_value = self.users[0] response = self.fetch('/users?email=', method='POST', body='') self.assertIn('Could not create user account', response.body.decode()) + self.assertIn('not a valid e-mail address', response.body.decode()) @patch('minigrid.handlers.BaseHandler.get_current_user') def test_post_invalid_email(self, get_current_user): get_current_user.return_value = self.users[0] response = self.fetch('/users?email=notemail', method='POST', body='') self.assertIn('Could not create user account', response.body.decode()) + self.assertIn('not a valid e-mail address', response.body.decode()) @patch('minigrid.handlers.BaseHandler.get_current_user') def test_post_user_exists(self, get_current_user): From 5a6fa67b9607b4e405d9c13c633f814caec98f0e Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Mon, 19 Dec 2016 11:54:28 -0500 Subject: [PATCH 24/24] Update version to 0.1.1 --- prod/docker-compose.yml | 2 +- prod/install.sh | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/prod/docker-compose.yml b/prod/docker-compose.yml index 57d4478..9c4fe41 100644 --- a/prod/docker-compose.yml +++ b/prod/docker-compose.yml @@ -1,7 +1,7 @@ version: '2' services: minigrid: - image: selcolumbia/minigrid-server:0.1.0.2 + image: selcolumbia/minigrid-server:0.1.1 command: ./prod/run.sh --db_host=db --redis_url=redis://redis:6379/0 --minigrid-website-url=https://www.example.com depends_on: - redis diff --git a/prod/install.sh b/prod/install.sh index 9d35c18..0d3ab4e 100755 --- a/prod/install.sh +++ b/prod/install.sh @@ -1,5 +1,5 @@ #!/usr/bin/env sh -# Minigrid Server installer for version 0.1.0.2 +# Minigrid Server installer for version 0.1.1 set -e # Do you have docker installed? @@ -108,8 +108,8 @@ $SUDO openssl dhparam -out /etc/letsencrypt/live/$LETSENCRYPT_DIR/dhparam.pem 20 printf "========================================\n" printf " Generating configuration \n" printf "========================================\n" -$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.0.2/prod/docker-compose.yml > docker-compose.yml -$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.0.2/prod/nginx.conf > nginx.conf +$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.1/prod/docker-compose.yml > docker-compose.yml +$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.1/prod/nginx.conf > nginx.conf sed -i s/www.example.com/$LETSENCRYPT_DIR/g docker-compose.yml sed -i s/www.example.com/$LETSENCRYPT_DIR/g nginx.conf