mirror of
https://github.com/GSA/notifications-api.git
synced 2026-02-01 23:55:58 -05:00
Merge branch 'master' into celery-jobs
This commit is contained in:
@@ -34,6 +34,7 @@ export NOTIFICATION_QUEUE_PREFIX='[unique-to-environment]-notification_developme
|
|||||||
export SECRET_KEY='dev-notify-secret-key'
|
export SECRET_KEY='dev-notify-secret-key'
|
||||||
export SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/notification_api'
|
export SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/notification_api'
|
||||||
export VERIFY_CODE_FROM_EMAIL_ADDRESS='no-reply@notify.works'
|
export VERIFY_CODE_FROM_EMAIL_ADDRESS='no-reply@notify.works'
|
||||||
|
export NOTIFY_EMAIL_DOMAIN='dev.notify.com'
|
||||||
"> environment.sh
|
"> environment.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ def create_app():
|
|||||||
from app.status.healthcheck import status as status_blueprint
|
from app.status.healthcheck import status as status_blueprint
|
||||||
from app.job.rest import job as job_blueprint
|
from app.job.rest import job as job_blueprint
|
||||||
from app.notifications.rest import notifications as notifications_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(service_blueprint, url_prefix='/service')
|
||||||
application.register_blueprint(user_blueprint, url_prefix='/user')
|
application.register_blueprint(user_blueprint, url_prefix='/user')
|
||||||
@@ -53,6 +54,7 @@ def create_app():
|
|||||||
application.register_blueprint(status_blueprint, url_prefix='/status')
|
application.register_blueprint(status_blueprint, url_prefix='/status')
|
||||||
application.register_blueprint(notifications_blueprint)
|
application.register_blueprint(notifications_blueprint)
|
||||||
application.register_blueprint(job_blueprint)
|
application.register_blueprint(job_blueprint)
|
||||||
|
application.register_blueprint(invite_blueprint)
|
||||||
|
|
||||||
return application
|
return application
|
||||||
|
|
||||||
|
|||||||
6
app/dao/invited_user_dao.py
Normal file
6
app/dao/invited_user_dao.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from app import db
|
||||||
|
|
||||||
|
|
||||||
|
def save_invited_user(invited_user):
|
||||||
|
db.session.add(invited_user)
|
||||||
|
db.session.commit()
|
||||||
0
app/invite/__init__.py
Normal file
0
app/invite/__init__.py
Normal file
24
app/invite/rest.py
Normal file
24
app/invite/rest.py
Normal file
@@ -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/<service_id>/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
|
||||||
@@ -239,3 +239,26 @@ class Notification(db.Model):
|
|||||||
onupdate=datetime.datetime.now)
|
onupdate=datetime.datetime.now)
|
||||||
status = db.Column(
|
status = db.Column(
|
||||||
db.Enum(*NOTIFICATION_STATUS_TYPES, name='notification_status_types'), nullable=False, default='sent')
|
db.Enum(*NOTIFICATION_STATUS_TYPES, name='notification_status_types'), nullable=False, default='sent')
|
||||||
|
|
||||||
|
|
||||||
|
INVITED_USER_STATUS_TYPES = ['pending', 'accepted', 'cancelled']
|
||||||
|
|
||||||
|
|
||||||
|
class InvitedUser(db.Model):
|
||||||
|
|
||||||
|
__tablename__ = 'invited_users'
|
||||||
|
|
||||||
|
id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
|
email_address = db.Column(db.String(255), nullable=False)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), index=True, nullable=False)
|
||||||
|
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')
|
||||||
|
created_at = db.Column(
|
||||||
|
db.DateTime,
|
||||||
|
index=False,
|
||||||
|
unique=False,
|
||||||
|
nullable=False,
|
||||||
|
default=datetime.datetime.now)
|
||||||
|
status = db.Column(
|
||||||
|
db.Enum(*INVITED_USER_STATUS_TYPES, name='invited_users_status_types'), nullable=False, default='pending')
|
||||||
|
|||||||
@@ -128,6 +128,16 @@ class NotificationStatusSchema(BaseSchema):
|
|||||||
model = models.Notification
|
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 = UserSchema()
|
||||||
user_schema_load_json = UserSchema(load_json=True)
|
user_schema_load_json = UserSchema(load_json=True)
|
||||||
users_schema = UserSchema(many=True)
|
users_schema = UserSchema(many=True)
|
||||||
@@ -153,3 +163,5 @@ job_email_template_notification_schema = JobEmailTemplateNotificationSchema()
|
|||||||
notification_status_schema = NotificationStatusSchema()
|
notification_status_schema = NotificationStatusSchema()
|
||||||
notifications_status_schema = NotificationStatusSchema(many=True)
|
notifications_status_schema = NotificationStatusSchema(many=True)
|
||||||
notification_status_schema_load_json = NotificationStatusSchema(load_json=True)
|
notification_status_schema_load_json = NotificationStatusSchema(load_json=True)
|
||||||
|
invited_user_schema = InvitedUserSchema()
|
||||||
|
invited_users_schema = InvitedUserSchema(many=True)
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ from app.models import ApiKey
|
|||||||
from app.schemas import (
|
from app.schemas import (
|
||||||
services_schema,
|
services_schema,
|
||||||
service_schema,
|
service_schema,
|
||||||
api_keys_schema
|
api_keys_schema,
|
||||||
)
|
users_schema)
|
||||||
from app import email_safe
|
from app import email_safe
|
||||||
|
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
@@ -155,3 +155,13 @@ def get_api_keys(service_id, key_id=None):
|
|||||||
return jsonify(result="error", message="API key not found"), 404
|
return jsonify(result="error", message="API key not found"), 404
|
||||||
|
|
||||||
return jsonify(apiKeys=api_keys_schema.dump(api_keys).data), 200
|
return jsonify(apiKeys=api_keys_schema.dump(api_keys).data), 200
|
||||||
|
|
||||||
|
|
||||||
|
@service.route('/<service_id>/users', methods=['GET'])
|
||||||
|
def get_users_for_service(service_id):
|
||||||
|
fetched = dao_fetch_service_by_id(service_id)
|
||||||
|
if not fetched:
|
||||||
|
return jsonify(data=[])
|
||||||
|
|
||||||
|
result = users_schema.dump(fetched.users)
|
||||||
|
return jsonify(data=result.data)
|
||||||
|
|||||||
42
migrations/versions/0022_add_invite_users.py
Normal file
42
migrations/versions/0022_add_invite_users.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 0022_add_invite_users
|
||||||
|
Revises: 0021_add_job_metadata
|
||||||
|
Create Date: 2016-02-23 16:41:40.481468
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '0022_add_invite_users'
|
||||||
|
down_revision = '0021_add_job_metadata'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('invited_users',
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('email_address', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('service_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('_token', sa.String(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('status', sa.Enum('pending', 'accepted', 'cancelled', name='invited_users_status_types'), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['service_id'], ['services.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_invited_users_service_id'), 'invited_users', ['service_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_invited_users_user_id'), 'invited_users', ['user_id'], unique=False)
|
||||||
|
### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_index(op.f('ix_invited_users_user_id'), table_name='invited_users')
|
||||||
|
op.drop_index(op.f('ix_invited_users_service_id'), table_name='invited_users')
|
||||||
|
op.drop_table('invited_users')
|
||||||
|
op.execute('DROP TYPE invited_users_status_types')
|
||||||
|
### end Alembic commands ###
|
||||||
26
migrations/versions/0023_drop_token.py
Normal file
26
migrations/versions/0023_drop_token.py
Normal file
@@ -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 ###
|
||||||
23
tests/app/dao/test_invited_user_dao.py
Normal file
23
tests/app/dao/test_invited_user_dao.py
Normal file
@@ -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
|
||||||
0
tests/app/invite/__init__.py
Normal file
0
tests/app/invite/__init__.py
Normal file
70
tests/app/invite/test_invite_rest.py
Normal file
70
tests/app/invite/test_invite_rest.py
Normal file
@@ -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']}
|
||||||
@@ -4,6 +4,7 @@ import uuid
|
|||||||
from app.dao.users_dao import save_model_user
|
from app.dao.users_dao import save_model_user
|
||||||
from app.models import User
|
from app.models import User
|
||||||
from tests import create_authorization_header
|
from tests import create_authorization_header
|
||||||
|
from tests.app.conftest import sample_service as create_service
|
||||||
|
|
||||||
|
|
||||||
def test_get_service_list(notify_api, service_factory):
|
def test_get_service_list(notify_api, service_factory):
|
||||||
@@ -341,3 +342,49 @@ def test_update_service_should_404_if_id_is_invalid(notify_api, notify_db):
|
|||||||
headers=[('Content-Type', 'application/json'), auth_header]
|
headers=[('Content-Type', 'application/json'), auth_header]
|
||||||
)
|
)
|
||||||
assert resp.status_code == 404
|
assert resp.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_users_by_service(notify_api, notify_db, notify_db_session, sample_user):
|
||||||
|
with notify_api.test_request_context():
|
||||||
|
with notify_api.test_client() as client:
|
||||||
|
|
||||||
|
sample_service = create_service(notify_db=notify_db, notify_db_session=notify_db_session,
|
||||||
|
service_name='Sample service', user=sample_user)
|
||||||
|
auth_header = create_authorization_header(
|
||||||
|
path='/service/{}/users'.format(sample_service.id),
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = client.get(
|
||||||
|
'/service/{}/users'.format(sample_service.id),
|
||||||
|
headers=[('Content-Type', 'application/json'), auth_header]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
result = json.loads(resp.get_data(as_text=True))
|
||||||
|
assert len(result['data']) == 1
|
||||||
|
assert result['data'][0]['name'] == sample_user.name
|
||||||
|
assert result['data'][0]['email_address'] == sample_user.email_address
|
||||||
|
assert result['data'][0]['mobile_number'] == sample_user.mobile_number
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_users_for_service_returns_empty_list_if_no_users_associated_with_service(notify_api,
|
||||||
|
notify_db,
|
||||||
|
notify_db_session,
|
||||||
|
sample_service,
|
||||||
|
sample_user):
|
||||||
|
with notify_api.test_request_context():
|
||||||
|
with notify_api.test_client() as client:
|
||||||
|
sample_service.users.remove(sample_user)
|
||||||
|
auth_header = create_authorization_header(
|
||||||
|
path='/service/{}/users'.format(sample_service.id),
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
'/service/{}/users'.format(sample_service.id),
|
||||||
|
headers=[('Content-Type', 'application/json'), auth_header]
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
result = json.loads(response.get_data(as_text=True))
|
||||||
|
assert result['data'] == []
|
||||||
|
|||||||
40
tests/app/service/test_url_for.py
Normal file
40
tests/app/service/test_url_for.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from flask import url_for
|
||||||
|
|
||||||
|
service_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
|
||||||
|
def test_url_for_get_services(notify_api):
|
||||||
|
with notify_api.test_request_context():
|
||||||
|
url = url_for('service.get_services')
|
||||||
|
assert str(url) == '/service'
|
||||||
|
url_with_user_id = url_for('service.get_services', user_id=1)
|
||||||
|
assert str(url_with_user_id) == '/service?user_id=1'
|
||||||
|
|
||||||
|
|
||||||
|
def test_url_for_get_service_by_id(notify_api):
|
||||||
|
with notify_api.test_request_context():
|
||||||
|
url = url_for('service.get_service_by_id', service_id=service_id)
|
||||||
|
assert str(url) == '/service/{}'.format(service_id)
|
||||||
|
|
||||||
|
url_with_user_id = url_for('service.get_service_by_id', service_id=service_id, user_id=1)
|
||||||
|
assert str(url_with_user_id) == '/service/{0}?user_id={1}'.format(service_id, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_url_for_create_service(notify_api):
|
||||||
|
with notify_api.test_request_context():
|
||||||
|
url = url_for('service.create_service')
|
||||||
|
assert str(url) == '/service'.format(service_id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_url_for_update_service(notify_api):
|
||||||
|
with notify_api.test_request_context():
|
||||||
|
url = url_for('service.update_service', service_id=service_id)
|
||||||
|
assert str(url) == '/service/{}'.format(service_id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_url_for_renew_api_key(notify_api):
|
||||||
|
with notify_api.test_request_context():
|
||||||
|
url = url_for('service.renew_api_key', service_id=service_id)
|
||||||
|
assert str(url) == '/service/{}/api-key'.format(service_id)
|
||||||
Reference in New Issue
Block a user