Merge pull request #1695 from alphagov/org-user-endpoints

Organisation user endpoints
This commit is contained in:
Rebecca Law
2018-02-26 16:27:01 +00:00
committed by GitHub
23 changed files with 747 additions and 70 deletions

View File

@@ -108,6 +108,8 @@ def register_blueprint(application):
from app.letters.rest import letter_job
from app.billing.rest import billing_blueprint
from app.organisation.rest import organisation_blueprint
from app.organisation.invite_rest import organisation_invite_blueprint
from app.organisation.accept_organisation_invite import accept_organisation_invite_blueprint
service_blueprint.before_request(requires_admin_auth)
application.register_blueprint(service_blueprint, url_prefix='/service')
@@ -181,6 +183,12 @@ def register_blueprint(application):
organisation_blueprint.before_request(requires_admin_auth)
application.register_blueprint(organisation_blueprint, url_prefix='/organisations')
organisation_invite_blueprint.before_request(requires_admin_auth)
application.register_blueprint(organisation_invite_blueprint)
accept_organisation_invite_blueprint.before_request(requires_admin_auth)
application.register_blueprint(accept_organisation_invite_blueprint)
def register_v2_blueprints(application):
from app.v2.inbound_sms.get_inbound_sms import v2_inbound_sms_blueprint as get_inbound_sms

View File

@@ -9,6 +9,7 @@ from itsdangerous import SignatureExpired
from notifications_utils.url_safe_token import check_token
from app.dao.invited_user_dao import get_invited_user_by_id
from app.dao.organisation_dao import dao_get_invited_organisation_user
from app.errors import (
register_errors,
@@ -24,7 +25,10 @@ register_errors(accept_invite)
@accept_invite.route('/<token>', methods=['GET'])
def get_invited_user_by_token(token):
"""
This method is now deprecated,
in favor of a single accept_invite endpoint for both service and organisation invitations
"""
max_age_seconds = 60 * 60 * 24 * current_app.config['INVITATION_EXPIRATION_DAYS']
try:
@@ -41,3 +45,29 @@ def get_invited_user_by_token(token):
invited_user = get_invited_user_by_id(invited_user_id)
return jsonify(data=invited_user_schema.dump(invited_user).data), 200
@accept_invite.route('/<invitation_type>/<token>', methods=['GET'])
def validate_invitation_token(invitation_type, token):
max_age_seconds = 60 * 60 * 24 * current_app.config['INVITATION_EXPIRATION_DAYS']
try:
invited_user_id = check_token(token,
current_app.config['SECRET_KEY'],
current_app.config['DANGEROUS_SALT'],
max_age_seconds)
except SignatureExpired:
errors = {'invitation':
['Your invitation to GOV.UK Notify has expired. '
'Please ask the person that invited you to send you another one']}
raise InvalidRequest(errors, status_code=400)
if invitation_type == 'service':
invited_user = get_invited_user_by_id(invited_user_id)
return jsonify(data=invited_user_schema.dump(invited_user).data), 200
elif invitation_type == 'organisation':
invited_user = dao_get_invited_organisation_user(invited_user_id)
return jsonify(data=invited_user.serialize()), 200
else:
raise InvalidRequest("Unrecognised invitation type: {}".format(invitation_type))

View File

@@ -22,6 +22,7 @@ from app import performance_platform_client, deskpro_client
from app.dao.date_util import get_month_start_and_end_date_in_utc
from app.dao.inbound_sms_dao import delete_inbound_sms_created_more_than_a_week_ago
from app.dao.invited_user_dao import delete_invitations_created_more_than_two_days_ago
from app.dao.invited_org_user_dao import delete_org_invitations_created_more_than_two_days_ago
from app.dao.jobs_dao import (
dao_get_letter_job_ids_by_status,
dao_set_scheduled_jobs_to_pending,
@@ -184,9 +185,10 @@ def delete_letter_notifications_older_than_seven_days():
def delete_invitations():
try:
start = datetime.utcnow()
deleted = delete_invitations_created_more_than_two_days_ago()
deleted_invites = delete_invitations_created_more_than_two_days_ago()
deleted_invites += delete_org_invitations_created_more_than_two_days_ago()
current_app.logger.info(
"Delete job started {} finished {} deleted {} invitations".format(start, datetime.utcnow(), deleted)
"Delete job started {} finished {} deleted {} invitations".format(start, datetime.utcnow(), deleted_invites)
)
except SQLAlchemyError:
current_app.logger.exception("Failed to delete invitations")

View File

@@ -149,6 +149,7 @@ class Config(object):
ALREADY_REGISTERED_EMAIL_TEMPLATE_ID = '0880fbb1-a0c6-46f0-9a8e-36c986381ceb'
CHANGE_EMAIL_CONFIRMATION_TEMPLATE_ID = 'eb4d9930-87ab-4aef-9bce-786762687884'
SERVICE_NOW_LIVE_TEMPLATE_ID = '618185c6-3636-49cd-b7d2-6f6f5eb3bdde'
ORGANISATION_INVITATION_EMAIL_TEMPLATE_ID = '203566f0-d835-47c5-aa06-932439c86573'
BROKER_URL = 'sqs://'
BROKER_TRANSPORT_OPTIONS = {

View File

@@ -0,0 +1,29 @@
from datetime import datetime, timedelta
from app import db
from app.models import InvitedOrganisationUser
def save_invited_org_user(invited_org_user):
db.session.add(invited_org_user)
db.session.commit()
def get_invited_org_user(organisation_id, invited_org_user_id):
return InvitedOrganisationUser.query.filter_by(organisation_id=organisation_id, id=invited_org_user_id).one()
def get_invited_org_user_by_id(invited_org_user_id):
return InvitedOrganisationUser.query.filter_by(id=invited_org_user_id).one()
def get_invited_org_users_for_organisation(organisation_id):
return InvitedOrganisationUser.query.filter_by(organisation_id=organisation_id).all()
def delete_org_invitations_created_more_than_two_days_ago():
deleted = db.session.query(InvitedOrganisationUser).filter(
InvitedOrganisationUser.created_at <= datetime.utcnow() - timedelta(days=2)
).delete()
db.session.commit()
return deleted

View File

@@ -1,6 +1,10 @@
from app import db
from app.dao.dao_utils import transactional
from app.models import Organisation
from app.models import (
Organisation,
InvitedOrganisationUser,
User
)
def dao_get_organisations():
@@ -42,3 +46,23 @@ def dao_add_service_to_organisation(service, organisation_id):
).one()
organisation.services.append(service)
def dao_get_invited_organisation_user(user_id):
return InvitedOrganisationUser.query.filter_by(id=user_id).one()
def dao_get_users_for_organisation(organisation_id):
return User.query.filter(
User.organisations.any(id=organisation_id),
User.state == 'active'
).order_by(User.created_at).all()
@transactional
def dao_add_user_to_organisation(organisation_id, user_id):
organisation = dao_get_organisation_by_id(organisation_id)
user = User.query.filter_by(id=user_id).one()
user.organisations.append(organisation)
db.session.add(organisation)
return user

View File

@@ -60,13 +60,6 @@ def get_invited_users_by_service(service_id):
return jsonify(data=invited_user_schema.dump(invited_users, many=True).data), 200
@invite.route('/<invited_user_id>', methods=['GET'])
def get_invited_user_by_service_and_id(service_id, invited_user_id):
invited_user = get_invited_user(service_id=service_id, invited_user_id=invited_user_id)
return jsonify(data=invited_user_schema.dump(invited_user).data), 200
@invite.route('/<invited_user_id>', methods=['POST'])
def update_invited_user(service_id, invited_user_id):
fetched = get_invited_user(service_id=service_id, invited_user_id=invited_user_id)

View File

@@ -116,6 +116,10 @@ class User(db.Model):
'Service',
secondary='user_to_service',
backref=db.backref('user_to_service', lazy='dynamic'))
organisations = db.relationship(
'Organisation',
secondary='user_to_organisation',
backref=db.backref('user_to_organisation', lazy='dynamic'))
@property
def password(self):
@@ -225,13 +229,7 @@ organisation_to_service = db.Table(
'organisation_to_service',
db.Model.metadata,
# service_id is a primary key as you can only have one organisation per service
db.Column(
'service_id',
UUID(as_uuid=True),
db.ForeignKey('services.id'),
primary_key=True,
unique=True,
nullable=False),
db.Column('service_id', UUID(as_uuid=True), db.ForeignKey('services.id'), primary_key=True, nullable=False),
db.Column('organisation_id', UUID(as_uuid=True), db.ForeignKey('organisation.id'), nullable=False),
)
@@ -249,11 +247,6 @@ class Organisation(db.Model):
secondary='organisation_to_service',
uselist=True)
users = db.relationship(
'User',
secondary='user_to_organisation',
backref=db.backref('organisations', lazy='dynamic'))
def serialize(self):
serialized = {
"id": str(self.id),
@@ -1452,6 +1445,16 @@ class InvitedOrganisationUser(db.Model):
default=INVITE_PENDING
)
def serialize(self):
return {
'id': str(self.id),
'email_address': self.email_address,
'invited_by': str(self.invited_by_id),
'organisation': str(self.organisation_id),
'created_at': self.created_at.strftime(DATETIME_FORMAT),
'status': self.status
}
# Service Permissions
MANAGE_USERS = 'manage_users'

View File

@@ -0,0 +1,30 @@
from flask import Blueprint, jsonify, current_app
from itsdangerous import SignatureExpired
from notifications_utils.url_safe_token import check_token
from app.dao.organisation_dao import dao_get_invited_organisation_user
from app.errors import register_errors, InvalidRequest
accept_organisation_invite_blueprint = Blueprint(
'accept_organisation_invite', __name__,
url_prefix='/organisation-invitation')
register_errors(accept_organisation_invite_blueprint)
@accept_organisation_invite_blueprint.route("/<token>", methods=['GET'])
def accept_organisation_invitation(token):
max_age_seconds = 60 * 60 * 24 * current_app.config['INVITATION_EXPIRATION_DAYS']
try:
invited_user_id = check_token(token,
current_app.config['SECRET_KEY'],
current_app.config['DANGEROUS_SALT'],
max_age_seconds)
except SignatureExpired:
errors = {'invitation': ['Your invitation to GOV.UK Notify has expired. '
'Please ask the person that invited you to send you another one']}
raise InvalidRequest(errors, status_code=400)
invited_user = dao_get_invited_organisation_user(invited_user_id)
return jsonify(data=invited_user.serialize()), 200

View File

@@ -0,0 +1,98 @@
from flask import (
Blueprint,
request,
jsonify,
current_app)
from notifications_utils.url_safe_token import generate_token
from app.config import QueueNames
from app.dao.invited_org_user_dao import (
save_invited_org_user,
get_invited_org_user,
get_invited_org_users_for_organisation
)
from app.dao.templates_dao import dao_get_template_by_id
from app.errors import register_errors
from app.models import EMAIL_TYPE, KEY_TYPE_NORMAL, InvitedOrganisationUser
from app.notifications.process_notifications import persist_notification, send_notification_to_queue
from app.schema_validation import validate
from app.organisation.organisation_schema import (
post_create_invited_org_user_status_schema,
post_update_invited_org_user_status_schema
)
organisation_invite_blueprint = Blueprint(
'organisation_invite', __name__,
url_prefix='/organisation/<uuid:organisation_id>/invite')
register_errors(organisation_invite_blueprint)
@organisation_invite_blueprint.route('', methods=['POST'])
def invite_user_to_org(organisation_id):
data = request.get_json()
validate(data, post_create_invited_org_user_status_schema)
invited_org_user = InvitedOrganisationUser(
email_address=data['email_address'],
invited_by_id=data['invited_by'],
organisation_id=organisation_id
)
save_invited_org_user(invited_org_user)
template = dao_get_template_by_id(current_app.config['ORGANISATION_INVITATION_EMAIL_TEMPLATE_ID'])
saved_notification = persist_notification(
template_id=template.id,
template_version=template.version,
recipient=invited_org_user.email_address,
service=template.service,
personalisation={
'user_name': invited_org_user.invited_by.name,
'organisation_name': invited_org_user.organisation.name,
'url': invited_org_user_url(
invited_org_user.id,
data.get('invite_link_host'),
),
},
notification_type=EMAIL_TYPE,
api_key_id=None,
key_type=KEY_TYPE_NORMAL,
reply_to_text=invited_org_user.invited_by.email_address
)
send_notification_to_queue(saved_notification, research_mode=False, queue=QueueNames.NOTIFY)
return jsonify(data=invited_org_user.serialize()), 201
@organisation_invite_blueprint.route('', methods=['GET'])
def get_invited_org_users_by_organisation(organisation_id):
invited_org_users = get_invited_org_users_for_organisation(organisation_id)
return jsonify(data=[x.serialize() for x in invited_org_users]), 200
@organisation_invite_blueprint.route('/<invited_org_user_id>', methods=['POST'])
def update_org_invite_status(organisation_id, invited_org_user_id):
fetched = get_invited_org_user(organisation_id=organisation_id, invited_org_user_id=invited_org_user_id)
data = request.get_json()
validate(data, post_update_invited_org_user_status_schema)
fetched.status = data['status']
save_invited_org_user(fetched)
return jsonify(data=fetched.serialize()), 200
def invited_org_user_url(invited_org_user_id, invite_link_host=None):
token = generate_token(
str(invited_org_user_id),
current_app.config['SECRET_KEY'],
current_app.config['DANGEROUS_SALT']
)
if invite_link_host is None:
invite_link_host = current_app.config['ADMIN_BASE_URL']
return '{0}/organisation-invitation/{1}'.format(invite_link_host, token)

View File

@@ -1,3 +1,4 @@
from app.models import INVITED_USER_STATUS_TYPES
from app.schema_validation.definitions import uuid
post_create_organisation_schema = {
@@ -31,3 +32,27 @@ post_link_service_to_organisation_schema = {
},
"required": ["service_id"]
}
post_create_invited_org_user_status_schema = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "POST create organisation invite schema",
"type": "object",
"properties": {
"email_address": {"type": "string", "format": "email_address"},
"invited_by": uuid,
"invite_link_host": {"type": "string"}
},
"required": ["email_address", "invited_by"]
}
post_update_invited_org_user_status_schema = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "POST update organisation invite schema",
"type": "object",
"properties": {
"status": {"enum": INVITED_USER_STATUS_TYPES}
},
"required": ["status"]
}

View File

@@ -8,6 +8,8 @@ from app.dao.organisation_dao import (
dao_get_organisation_services,
dao_update_organisation,
dao_add_service_to_organisation,
dao_get_users_for_organisation,
dao_add_user_to_organisation
)
from app.dao.services_dao import dao_fetch_service_by_id
from app.errors import register_errors, InvalidRequest
@@ -18,6 +20,7 @@ from app.organisation.organisation_schema import (
post_link_service_to_organisation_schema,
)
from app.schema_validation import validate
from app.schemas import user_schema
organisation_blueprint = Blueprint('organisation', __name__)
register_errors(organisation_blueprint)
@@ -90,3 +93,17 @@ def get_organisation_services(organisation_id):
services = dao_get_organisation_services(organisation_id)
sorted_services = sorted(services, key=lambda s: (-s.active, s.name))
return jsonify([s.serialize_for_org_dashboard() for s in sorted_services])
@organisation_blueprint.route('/<uuid:organisation_id>/users/<uuid:user_id>', methods=['POST'])
def add_user_to_organisation(organisation_id, user_id):
new_org_user = dao_add_user_to_organisation(organisation_id, user_id)
return jsonify(data=user_schema.dump(new_org_user).data), 200
@organisation_blueprint.route('/<uuid:organisation_id>/users', methods=['GET'])
def get_organisation_users(organisation_id):
org_users = dao_get_users_for_organisation(organisation_id)
result = user_schema.dump(org_users, many=True)
return jsonify(data=result.data)

View File

@@ -100,8 +100,13 @@ class UserSchema(BaseSchema):
class Meta:
model = models.User
exclude = (
"updated_at", "created_at", "user_to_service",
"_password", "verify_codes")
"updated_at",
"created_at",
"user_to_service",
"user_to_organisation",
"_password",
"verify_codes"
)
strict = True
@validates('name')

View File

@@ -0,0 +1,80 @@
"""
Revision ID: 0168_add_org_invite_template
Revises: 0167_add_precomp_letter_svc_perm
Create Date: 2018-02-16 14:16:43.618062
"""
from datetime import datetime
from alembic import op
from flask import current_app
revision = '0168_add_org_invite_template'
down_revision = '0167_add_precomp_letter_svc_perm'
template_id = '203566f0-d835-47c5-aa06-932439c86573'
def upgrade():
template_insert = """
INSERT INTO templates (id, name, template_type, created_at, content, archived, service_id, subject, created_by_id, version, process_type)
VALUES ('{}', '{}', '{}', '{}', '{}', False, '{}', '{}', '{}', 1, '{}')
"""
template_history_insert = """
INSERT INTO templates_history (id, name, template_type, created_at, content, archived, service_id, subject, created_by_id, version, process_type)
VALUES ('{}', '{}', '{}', '{}', '{}', False, '{}', '{}', '{}', 1, '{}')
"""
template_content = '\n'.join([
"((user_name)) has invited you to collaborate on ((organisation_name)) on GOV.UK Notify.",
"",
"GOV.UK Notify makes it easy to keep people updated by helping you send text messages, emails and letters.",
"",
"Open this link to create an account on GOV.UK Notify:",
"((url))",
"",
"This invitation will stop working at midnight tomorrow. This is to keep ((organisation_name)) secure.",
])
template_name = "Notify organisation invitation email"
template_subject = '((user_name)) has invited you to collaborate on ((organisation_name)) on GOV.UK Notify'
op.execute(
template_history_insert.format(
template_id,
template_name,
'email',
datetime.utcnow(),
template_content,
current_app.config['NOTIFY_SERVICE_ID'],
template_subject,
current_app.config['NOTIFY_USER_ID'],
'normal'
)
)
op.execute(
template_insert.format(
template_id,
template_name,
'email',
datetime.utcnow(),
template_content,
current_app.config['NOTIFY_SERVICE_ID'],
template_subject,
current_app.config['NOTIFY_USER_ID'],
'normal'
)
)
# clean up constraints on org_to_service - service_id-org_id constraint is redundant
op.drop_constraint('organisation_to_service_service_id_organisation_id_key', 'organisation_to_service', type_='unique')
def downgrade():
op.execute("DELETE FROM templates_history WHERE id = '{}'".format(template_id))
op.execute("DELETE FROM templates WHERE id = '{}'".format(template_id))
op.create_unique_constraint('organisation_to_service_service_id_organisation_id_key', 'organisation_to_service', ['service_id', 'organisation_id'])

View File

@@ -1,6 +1,7 @@
import uuid
from flask import json
import pytest
from flask import json, current_app
from freezegun import freeze_time
from notifications_utils.url_safe_token import generate_token
from tests import create_authorization_header
@@ -56,3 +57,59 @@ def test_accept_invite_returns_400_when_invited_user_does_not_exist(notify_api):
json_resp = json.loads(response.get_data(as_text=True))
assert json_resp['result'] == 'error'
assert json_resp['message'] == 'No result found'
@pytest.mark.parametrize('invitation_type', ['service', 'organisation'])
def test_validate_invitation_token_for_expired_token_returns_400(client, invitation_type):
with freeze_time('2016-01-01T12:00:00'):
token = generate_token(str(uuid.uuid4()), current_app.config['SECRET_KEY'],
current_app.config['DANGEROUS_SALT'])
url = '/invite/{}/{}'.format(invitation_type, token)
auth_header = create_authorization_header()
response = client.get(url, headers=[('Content-Type', 'application/json'), auth_header])
assert response.status_code == 400
json_resp = json.loads(response.get_data(as_text=True))
assert json_resp['result'] == 'error'
assert json_resp['message'] == {'invitation': [
'Your invitation to GOV.UK Notify has expired. '
'Please ask the person that invited you to send you another one']}
@pytest.mark.parametrize('invitation_type', ['service', 'organisation'])
def test_validate_invitation_token_returns_200_when_token_valid(
client, invitation_type, sample_invited_user, sample_invited_org_user
):
invited_user = sample_invited_user if invitation_type == 'service' else sample_invited_org_user
token = generate_token(str(invited_user.id), current_app.config['SECRET_KEY'],
current_app.config['DANGEROUS_SALT'])
url = '/invite/{}/{}'.format(invitation_type, token)
auth_header = create_authorization_header()
response = client.get(url, headers=[('Content-Type', 'application/json'), auth_header])
assert response.status_code == 200
json_resp = json.loads(response.get_data(as_text=True))
if invitation_type == 'service':
assert json_resp['data']['id'] == str(sample_invited_user.id)
assert json_resp['data']['email_address'] == sample_invited_user.email_address
assert json_resp['data']['from_user'] == str(sample_invited_user.user_id)
assert json_resp['data']['service'] == str(sample_invited_user.service_id)
assert json_resp['data']['status'] == sample_invited_user.status
assert json_resp['data']['permissions'] == sample_invited_user.permissions
if invitation_type == 'organisation':
assert json_resp['data'] == sample_invited_org_user.serialize()
@pytest.mark.parametrize('invitation_type', ['service', 'organisation'])
def test_validate_invitation_token_returns_400_when_invited_user_does_not_exist(client, invitation_type):
token = generate_token(str(uuid.uuid4()), current_app.config['SECRET_KEY'],
current_app.config['DANGEROUS_SALT'])
url = '/invite/{}/{}'.format(invitation_type, token)
auth_header = create_authorization_header()
response = client.get(url, headers=[('Content-Type', 'application/json'), auth_header])
assert response.status_code == 404
json_resp = json.loads(response.get_data(as_text=True))
assert json_resp['result'] == 'error'
assert json_resp['message'] == 'No result found'

View File

@@ -59,7 +59,8 @@ from tests.app.db import (
create_service,
create_api_key,
create_inbound_number,
create_letter_contact
create_letter_contact,
create_invited_org_user,
)
@@ -747,6 +748,16 @@ def sample_invited_user(notify_db,
return invited_user
@pytest.fixture(scope='function')
def sample_invited_org_user(
notify_db,
notify_db_session,
sample_user,
sample_organisation
):
return create_invited_org_user(sample_organisation, sample_user)
@pytest.fixture(scope='function')
def sample_permission(notify_db,
notify_db_session,
@@ -920,6 +931,20 @@ def invitation_email_template(notify_db,
)
@pytest.fixture(scope='function')
def org_invite_email_template(notify_db, notify_db_session):
service, user = notify_service(notify_db, notify_db_session)
return create_custom_template(
service=service,
user=user,
template_config_name='ORGANISATION_INVITATION_EMAIL_TEMPLATE_ID',
content='((user_name)) ((organisation_name)) ((url))',
subject='Invitation to ((organisation_name))',
template_type='email'
)
@pytest.fixture(scope='function')
def password_reset_email_template(notify_db,
notify_db_session):

View File

@@ -1,5 +1,7 @@
import uuid
import pytest
from sqlalchemy.exc import IntegrityError
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from app.dao.organisation_dao import (
dao_get_organisations,
@@ -8,15 +10,18 @@ from app.dao.organisation_dao import (
dao_get_organisation_services,
dao_update_organisation,
dao_add_service_to_organisation,
dao_get_invited_organisation_user,
dao_get_users_for_organisation,
dao_add_user_to_organisation
)
from app.models import Organisation
from tests.app.db import create_organisation, create_service
from tests.app.db import create_organisation, create_service, create_user
def test_get_organisations_gets_all_organisations_alphabetically_with_active_organisations_first(
notify_db,
notify_db_session
notify_db,
notify_db_session
):
m_active_org = create_organisation(name='m_active_organisation')
z_inactive_org = create_organisation(name='z_inactive_organisation', active=False)
@@ -104,3 +109,65 @@ def test_get_organisation_by_service_id(notify_db, notify_db_session, sample_ser
assert organisation_1 == sample_organisation
assert organisation_2 == another_org
def test_dao_get_invited_organisation_user(sample_invited_org_user):
invited_org_user = dao_get_invited_organisation_user(sample_invited_org_user.id)
assert invited_org_user == sample_invited_org_user
def test_dao_get_invited_organisation_user_returns_none(notify_db):
with pytest.raises(expected_exception=SQLAlchemyError):
dao_get_invited_organisation_user(uuid.uuid4())
def test_dao_get_users_for_organisation(sample_organisation):
first = create_user(email='first@invited.com')
second = create_user(email='another@invited.com')
dao_add_user_to_organisation(organisation_id=sample_organisation.id, user_id=first.id)
dao_add_user_to_organisation(organisation_id=sample_organisation.id, user_id=second.id)
results = dao_get_users_for_organisation(organisation_id=sample_organisation.id)
assert len(results) == 2
assert results[0] == first
assert results[1] == second
def test_dao_get_users_for_organisation_returns_empty_list(sample_organisation):
results = dao_get_users_for_organisation(organisation_id=sample_organisation.id)
assert len(results) == 0
def test_dao_get_users_for_organisation_only_returns_active_users(sample_organisation):
first = create_user(email='first@invited.com')
second = create_user(email='another@invited.com')
dao_add_user_to_organisation(organisation_id=sample_organisation.id, user_id=first.id)
dao_add_user_to_organisation(organisation_id=sample_organisation.id, user_id=second.id)
second.state = 'inactive'
results = dao_get_users_for_organisation(organisation_id=sample_organisation.id)
assert len(results) == 1
assert results[0] == first
def test_add_user_to_organisation_returns_user(sample_organisation):
org_user = create_user()
assert not org_user.organisations
added_user = dao_add_user_to_organisation(organisation_id=sample_organisation.id, user_id=org_user.id)
assert len(added_user.organisations) == 1
assert added_user.organisations[0] == sample_organisation
def test_add_user_to_organisation_when_user_does_not_exist(sample_organisation):
with pytest.raises(expected_exception=SQLAlchemyError):
dao_add_user_to_organisation(organisation_id=sample_organisation.id, user_id=uuid.uuid4())
def test_add_user_to_organisation_when_organisation_does_not_exist(sample_user):
with pytest.raises(expected_exception=SQLAlchemyError):
dao_add_user_to_organisation(organisation_id=uuid.uuid4(), user_id=sample_user.id)

View File

@@ -6,6 +6,7 @@ from app.dao.jobs_dao import dao_create_job
from app.dao.service_inbound_api_dao import save_service_inbound_api
from app.dao.service_callback_api_dao import save_service_callback_api
from app.dao.service_sms_sender_dao import update_existing_sms_sender_with_inbound_number, dao_update_service_sms_sender
from app.dao.invited_org_user_dao import save_invited_org_user
from app.models import (
ApiKey,
InboundSms,
@@ -30,7 +31,8 @@ from app.models import (
SMS_TYPE,
KEY_TYPE_NORMAL,
AnnualBilling,
LetterRate
LetterRate,
InvitedOrganisationUser,
)
from app.dao.users_dao import save_model_user
from app.dao.notifications_dao import (
@@ -492,3 +494,13 @@ def create_organisation(name='test_org_1', active=True):
dao_create_organisation(organisation)
return organisation
def create_invited_org_user(organisation, invited_by, email_address='invite@example.com'):
invited_org_user = InvitedOrganisationUser(
email_address=email_address,
invited_by=invited_by,
organisation=organisation,
)
save_invited_org_user(invited_org_user)
return invited_org_user

View File

@@ -1,6 +1,5 @@
import json
import pytest
import uuid
from app.models import Notification, SMS_AUTH_TYPE, EMAIL_AUTH_TYPE
from tests import create_authorization_header
@@ -161,40 +160,6 @@ def test_get_invited_users_by_service_with_no_invites(client, notify_db, notify_
assert len(json_resp['data']) == 0
def test_get_invited_user_by_service_and_id(client, sample_service, sample_invited_user):
url = '/service/{}/invite/{}'.format(sample_service.id, sample_invited_user.id)
auth_header = create_authorization_header()
response = client.get(
url,
headers=[('Content-Type', 'application/json'), auth_header]
)
assert response.status_code == 200
json_resp = json.loads(response.get_data(as_text=True))
invite_email_address = sample_invited_user.email_address
invite_from = sample_service.users[0]
assert json_resp['data']['service'] == str(sample_service.id)
assert json_resp['data']['email_address'] == invite_email_address
assert json_resp['data']['from_user'] == str(invite_from.id)
assert json_resp['data']['id']
def test_get_invited_user_by_service_but_unknown_invite_id_returns_404(client, sample_service):
unknown_id = uuid.uuid4()
url = '/service/{}/invite/{}'.format(sample_service.id, unknown_id)
auth_header = create_authorization_header()
response = client.get(
url,
headers=[('Content-Type', 'application/json'), auth_header]
)
assert response.status_code == 404
def test_update_invited_user_set_status_to_cancelled(client, sample_invited_user):
data = {'status': 'cancelled'}
url = '/service/{0}/invite/{1}'.format(sample_invited_user.service_id, sample_invited_user.id)

View File

@@ -0,0 +1,17 @@
import json
from flask import current_app
from notifications_utils.url_safe_token import generate_token
from tests import create_authorization_header
def test_accept_organisation_invitation(client, sample_invited_org_user):
token = generate_token(str(sample_invited_org_user.id), current_app.config['SECRET_KEY'],
current_app.config['DANGEROUS_SALT'])
url = '/organisation-invitation/{}'.format(token)
auth_header = create_authorization_header()
response = client.get(url, headers=[('Content-Type', 'application/json'), auth_header])
assert response.status_code == 200
json_resp = json.loads(response.get_data(as_text=True))
assert json_resp['data'] == sample_invited_org_user.serialize()

View File

@@ -0,0 +1,148 @@
import pytest
from app.models import Notification, INVITE_PENDING
from tests.app.db import create_invited_org_user
@pytest.mark.parametrize('extra_args, expected_start_of_invite_url', [
(
{},
'http://localhost:6012/organisation-invitation/'
),
(
{'invite_link_host': 'https://www.example.com'},
'https://www.example.com/organisation-invitation/'
),
])
def test_create_invited_org_user(
admin_request,
sample_organisation,
sample_user,
mocker,
org_invite_email_template,
extra_args,
expected_start_of_invite_url,
):
mocked = mocker.patch('app.celery.provider_tasks.deliver_email.apply_async')
email_address = 'invited_user@example.com'
data = dict(
organisation=str(sample_organisation.id),
email_address=email_address,
invited_by=str(sample_user.id),
**extra_args
)
json_resp = admin_request.post(
'organisation_invite.invite_user_to_org',
organisation_id=sample_organisation.id,
_data=data,
_expected_status=201
)
assert json_resp['data']['organisation'] == str(sample_organisation.id)
assert json_resp['data']['email_address'] == email_address
assert json_resp['data']['invited_by'] == str(sample_user.id)
assert json_resp['data']['status'] == INVITE_PENDING
assert json_resp['data']['id']
notification = Notification.query.first()
assert notification.reply_to_text == sample_user.email_address
assert len(notification.personalisation.keys()) == 3
assert notification.personalisation['organisation_name'] == 'sample organisation'
assert notification.personalisation['user_name'] == 'Test User'
assert notification.personalisation['url'].startswith(expected_start_of_invite_url)
assert len(notification.personalisation['url']) > len(expected_start_of_invite_url)
mocked.assert_called_once_with([(str(notification.id))], queue="notify-internal-tasks")
def test_create_invited_user_invalid_email(admin_request, sample_organisation, sample_user, mocker):
mocked = mocker.patch('app.celery.provider_tasks.deliver_email.apply_async')
email_address = 'notanemail'
data = {
'service': str(sample_organisation.id),
'email_address': email_address,
'invited_by': str(sample_user.id),
}
json_resp = admin_request.post(
'organisation_invite.invite_user_to_org',
organisation_id=sample_organisation.id,
_data=data,
_expected_status=400
)
assert json_resp['errors'][0]['message'] == 'email_address Not a valid email address'
assert mocked.call_count == 0
def test_get_all_invited_users_by_service(admin_request, sample_organisation, sample_user):
for i in range(5):
create_invited_org_user(
sample_organisation,
sample_user,
email_address='invited_user_{}@service.gov.uk'.format(i)
)
json_resp = admin_request.get(
'organisation_invite.get_invited_org_users_by_organisation',
organisation_id=sample_organisation.id
)
assert len(json_resp['data']) == 5
for invite in json_resp['data']:
assert invite['organisation'] == str(sample_organisation.id)
assert invite['invited_by'] == str(sample_user.id)
assert invite['id']
def test_get_invited_users_by_service_with_no_invites(admin_request, sample_organisation):
json_resp = admin_request.get(
'organisation_invite.get_invited_org_users_by_organisation',
organisation_id=sample_organisation.id
)
assert len(json_resp['data']) == 0
def test_update_org_invited_user_set_status_to_cancelled(admin_request, sample_invited_org_user):
data = {'status': 'cancelled'}
json_resp = admin_request.post(
'organisation_invite.update_org_invite_status',
organisation_id=sample_invited_org_user.organisation_id,
invited_org_user_id=sample_invited_org_user.id,
_data=data
)
assert json_resp['data']['status'] == 'cancelled'
def test_update_org_invited_user_for_wrong_service_returns_404(admin_request, sample_invited_org_user, fake_uuid):
data = {'status': 'cancelled'}
json_resp = admin_request.post(
'organisation_invite.update_org_invite_status',
organisation_id=fake_uuid,
invited_org_user_id=sample_invited_org_user.id,
_data=data,
_expected_status=404
)
assert json_resp['message'] == 'No result found'
def test_update_org_invited_user_for_invalid_data_returns_400(admin_request, sample_invited_org_user):
data = {'status': 'garbage'}
json_resp = admin_request.post(
'organisation_invite.update_org_invite_status',
organisation_id=sample_invited_org_user.organisation_id,
invited_org_user_id=sample_invited_org_user.id,
_data=data,
_expected_status=400
)
assert len(json_resp['errors']) == 1
assert json_resp['errors'][0]['message'] == 'status garbage is not one of [pending, accepted, cancelled]'

View File

@@ -1,6 +1,8 @@
import uuid
from app.models import Organisation
from app.dao.organisation_dao import dao_add_service_to_organisation
from tests.app.db import create_organisation, create_service
from app.dao.organisation_dao import dao_add_service_to_organisation, dao_add_user_to_organisation
from tests.app.db import create_organisation, create_service, create_user
def test_get_all_organisations(admin_request, notify_db_session):
@@ -270,3 +272,41 @@ def test_rest_get_organisation_services_inactive_services_at_end(
assert response[0]['name'] == service.name
assert response[1]['name'] == inactive_service.name
assert response[2]['name'] == inactive_service_1.name
def test_add_user_to_organisation_returns_added_user(admin_request, sample_organisation, sample_user):
response = admin_request.post(
'organisation.add_user_to_organisation',
organisation_id=str(sample_organisation.id),
user_id=str(sample_user.id),
_expected_status=200
)
assert response['data']['id'] == str(sample_user.id)
assert len(response['data']['organisations']) == 1
assert response['data']['organisations'][0] == str(sample_organisation.id)
def test_add_user_to_organisation_returns_404_if_user_does_not_exist(admin_request, sample_organisation):
admin_request.post(
'organisation.add_user_to_organisation',
organisation_id=str(sample_organisation.id),
user_id=str(uuid.uuid4()),
_expected_status=404
)
def test_get_organisation_users_returns_users_for_organisation(admin_request, sample_organisation):
first = create_user(email='first@invited.com')
second = create_user(email='another@invited.com')
dao_add_user_to_organisation(organisation_id=sample_organisation.id, user_id=first.id)
dao_add_user_to_organisation(organisation_id=sample_organisation.id, user_id=second.id)
response = admin_request.get(
'organisation.get_organisation_users',
organisation_id=sample_organisation.id,
_expected_status=200
)
assert len(response['data']) == 2
assert response['data'][0]['id'] == str(first.id)

View File

@@ -104,7 +104,8 @@ def notify_db_session(notify_db):
"dvla_organisation",
"notification_status_types",
"service_permission_types",
"auth_type"]:
"auth_type",
"invite_status_type"]:
notify_db.engine.execute(tbl.delete())
notify_db.session.commit()