From e6fe10cbdcb6276c333ffb36f24a0dfa2b3153ce Mon Sep 17 00:00:00 2001 From: Adam Shimali Date: Wed, 24 Feb 2016 14:01:19 +0000 Subject: [PATCH] [WIP] added endpoint and dao to create invites for users. Droped token as later code to send email invite can generate timebased url to send to user. That can then be checked against configurable time threshold for expiry. Therefore no need to store a token. --- app/__init__.py | 2 + app/dao/invited_user_dao.py | 6 +++ app/invite/__init__.py | 0 app/invite/rest.py | 24 +++++++++ app/models.py | 14 +----- app/schemas.py | 12 +++++ migrations/versions/0023_drop_token.py | 26 ++++++++++ tests/app/dao/test_invited_user_dao.py | 23 +++++++++ tests/app/invite/__init__.py | 0 tests/app/invite/test_invite_rest.py | 70 ++++++++++++++++++++++++++ 10 files changed, 164 insertions(+), 13 deletions(-) create mode 100644 app/dao/invited_user_dao.py create mode 100644 app/invite/__init__.py create mode 100644 app/invite/rest.py create mode 100644 migrations/versions/0023_drop_token.py create mode 100644 tests/app/dao/test_invited_user_dao.py create mode 100644 tests/app/invite/__init__.py create mode 100644 tests/app/invite/test_invite_rest.py diff --git a/app/__init__.py b/app/__init__.py index 87863e235..6ca701570 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -45,6 +45,7 @@ def create_app(): from app.status.healthcheck import status as status_blueprint from app.job.rest import job as job_blueprint from app.notifications.rest import notifications as notifications_blueprint + from app.invite.rest import invite as invite_blueprint application.register_blueprint(service_blueprint, url_prefix='/service') application.register_blueprint(user_blueprint, url_prefix='/user') @@ -52,6 +53,7 @@ def create_app(): application.register_blueprint(status_blueprint, url_prefix='/status') application.register_blueprint(notifications_blueprint, url_prefix='/notifications') application.register_blueprint(job_blueprint) + application.register_blueprint(invite_blueprint) return application diff --git a/app/dao/invited_user_dao.py b/app/dao/invited_user_dao.py new file mode 100644 index 000000000..b17faa2f2 --- /dev/null +++ b/app/dao/invited_user_dao.py @@ -0,0 +1,6 @@ +from app import db + + +def save_invited_user(invited_user): + db.session.add(invited_user) + db.session.commit() diff --git a/app/invite/__init__.py b/app/invite/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/app/invite/rest.py b/app/invite/rest.py new file mode 100644 index 000000000..25ed891eb --- /dev/null +++ b/app/invite/rest.py @@ -0,0 +1,24 @@ +from flask import ( + Blueprint, + request, + jsonify +) + +from app.dao.invited_user_dao import save_invited_user +from app.schemas import invited_user_schema + +invite = Blueprint('invite', __name__, url_prefix='/service//invite') + +from app.errors import register_errors +register_errors(invite) + + +@invite.route('', methods=['POST']) +def create_invite_user(service_id): + invited_user, errors = invited_user_schema.load(request.get_json()) + if errors: + return jsonify(result="error", message=errors), 400 + + save_invited_user(invited_user) + + return jsonify(data=invited_user_schema.dump(invited_user).data), 201 diff --git a/app/models.py b/app/models.py index e0065e3e3..170a8949e 100644 --- a/app/models.py +++ b/app/models.py @@ -239,7 +239,6 @@ class InvitedUser(db.Model): from_user = db.relationship('User') service_id = db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'), index=True, unique=False) service = db.relationship('Service') - _token = db.Column(db.String, nullable=False) created_at = db.Column( db.DateTime, index=False, @@ -247,15 +246,4 @@ class InvitedUser(db.Model): nullable=False, default=datetime.datetime.now) status = db.Column( - db.Enum(*INVITED_USER_STATUS_TYPES, name='invited_users_status_types'), nullable=False, default='invited') - - @property - def token(self): - raise AttributeError("Token not readable") - - @token.setter - def token(self, token): - self._token = hashpw(token) - - def check_token(self, token): - return check_hash(token, self._token) + db.Enum(*INVITED_USER_STATUS_TYPES, name='invited_users_status_types'), nullable=False, default='pending') diff --git a/app/schemas.py b/app/schemas.py index c024c2be1..9851fc3d1 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -118,6 +118,16 @@ class NotificationStatusSchema(BaseSchema): model = models.Notification +class InvitedUserSchema(BaseSchema): + class Meta: + model = models.InvitedUser + + @validates('email_address') + def validate_to(self, value): + if not email_regex.match(value): + raise ValidationError('Invalid email') + + user_schema = UserSchema() user_schema_load_json = UserSchema(load_json=True) users_schema = UserSchema(many=True) @@ -142,3 +152,5 @@ email_notification_schema = EmailNotificationSchema() notification_status_schema = NotificationStatusSchema() notifications_status_schema = NotificationStatusSchema(many=True) notification_status_schema_load_json = NotificationStatusSchema(load_json=True) +invited_user_schema = InvitedUserSchema() +invited_users_schema = InvitedUserSchema(many=True) diff --git a/migrations/versions/0023_drop_token.py b/migrations/versions/0023_drop_token.py new file mode 100644 index 000000000..bf6f705d7 --- /dev/null +++ b/migrations/versions/0023_drop_token.py @@ -0,0 +1,26 @@ +"""empty message + +Revision ID: 0023_drop_token +Revises: 0022_add_invite_users +Create Date: 2016-02-24 13:58:04.440296 + +""" + +# revision identifiers, used by Alembic. +revision = '0023_drop_token' +down_revision = '0022_add_invite_users' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_column('invited_users', '_token') + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.add_column('invited_users', sa.Column('_token', sa.VARCHAR(), autoincrement=False, nullable=False)) + ### end Alembic commands ### diff --git a/tests/app/dao/test_invited_user_dao.py b/tests/app/dao/test_invited_user_dao.py new file mode 100644 index 000000000..5d81ad488 --- /dev/null +++ b/tests/app/dao/test_invited_user_dao.py @@ -0,0 +1,23 @@ + +from app.models import InvitedUser + +from app.dao.invited_user_dao import save_invited_user + + +def test_create_invited_user(notify_db, notify_db_session, sample_service): + assert InvitedUser.query.count() == 0 + email_address = 'invited_user@service.gov.uk' + invite_from = sample_service.users[0] + + data = { + 'service': sample_service, + 'email_address': email_address, + 'from_user': invite_from + } + + invited_user = InvitedUser(**data) + save_invited_user(invited_user) + + assert InvitedUser.query.count() == 1 + assert invited_user.email_address == email_address + assert invited_user.from_user == invite_from diff --git a/tests/app/invite/__init__.py b/tests/app/invite/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/app/invite/test_invite_rest.py b/tests/app/invite/test_invite_rest.py new file mode 100644 index 000000000..1ba4a69ee --- /dev/null +++ b/tests/app/invite/test_invite_rest.py @@ -0,0 +1,70 @@ +import json + +from tests import create_authorization_header + + +def test_create_invited_user(notify_api, sample_service): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + + email_address = 'invited_user@service.gov.uk' + invite_from = sample_service.users[0] + + data = { + 'service': str(sample_service.id), + 'email_address': email_address, + 'from_user': invite_from.id + } + + data = json.dumps(data) + + auth_header = create_authorization_header( + path='/service/{}/invite'.format(sample_service.id), + method='POST', + request_body=data + ) + + response = client.post( + '/service/{}/invite'.format(sample_service.id), + headers=[('Content-Type', 'application/json'), auth_header], + data=data + ) + assert response.status_code == 201 + json_resp = json.loads(response.get_data(as_text=True)) + + assert json_resp['data']['service'] == str(sample_service.id) + assert json_resp['data']['email_address'] == email_address + assert json_resp['data']['from_user'] == invite_from.id + assert json_resp['data']['id'] + + +def test_create_invited_user_invalid_email(notify_api, sample_service): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + + email_address = 'notanemail' + invite_from = sample_service.users[0] + + data = { + 'service': str(sample_service.id), + 'email_address': email_address, + 'from_user': invite_from.id + } + + data = json.dumps(data) + + auth_header = create_authorization_header( + path='/service/{}/invite'.format(sample_service.id), + method='POST', + request_body=data + ) + + response = client.post( + '/service/{}/invite'.format(sample_service.id), + headers=[('Content-Type', 'application/json'), auth_header], + data=data + ) + assert response.status_code == 400 + json_resp = json.loads(response.get_data(as_text=True)) + assert json_resp['result'] == 'error' + assert json_resp['message'] == {'email_address': ['Invalid email']}