Merge pull request #1901 from alphagov/becca-invite-users

Organisation invite users
This commit is contained in:
Rebecca Law
2018-02-27 13:12:33 +00:00
committed by GitHub
26 changed files with 1123 additions and 132 deletions

View File

@@ -52,6 +52,7 @@ from app.notify_client.provider_client import ProviderClient
from app.notify_client.email_branding_client import EmailBrandingClient
from app.notify_client.models import AnonymousUser
from app.notify_client.organisations_api_client import OrganisationsClient
from app.notify_client.org_invite_api_client import OrgInviteApiClient
from app.notify_client.letter_jobs_client import LetterJobsClient
from app.notify_client.inbound_number_client import InboundNumberClient
from app.notify_client.billing_api_client import BillingAPIClient
@@ -74,6 +75,7 @@ events_api_client = EventsApiClient()
provider_client = ProviderClient()
email_branding_client = EmailBrandingClient()
organisations_client = OrganisationsClient()
org_invite_api_client = OrgInviteApiClient()
asset_fingerprinter = AssetFingerprinter()
statsd_client = StatsdClient()
deskpro_client = DeskproClient()
@@ -109,6 +111,7 @@ def create_app(application):
notification_api_client.init_app(application)
status_api_client.init_app(application)
invite_api_client.init_app(application)
org_invite_api_client.init_app(application)
template_statistics_client.init_app(application)
events_api_client.init_app(application)
provider_client.init_app(application)

View File

@@ -237,6 +237,25 @@ class RegisterUserFromInviteForm(StripWhitespaceForm):
raise ValidationError('Cant be empty')
class RegisterUserFromOrgInviteForm(StripWhitespaceForm):
def __init__(self, invited_org_user):
super().__init__(
organisation=invited_org_user['organisation'],
email_address=invited_org_user['email_address'],
)
name = StringField(
'Full name',
validators=[DataRequired(message='Cant be empty')]
)
mobile_number = InternationalPhoneNumber('Mobile number', validators=[DataRequired(message='Cant be empty')])
password = password()
organisation = HiddenField('organisation')
email_address = HiddenField('email_address')
auth_type = HiddenField('auth_type', validators=[DataRequired()])
class PermissionsForm(StripWhitespaceForm):
send_messages = BooleanField("Send messages from existing templates")
manage_templates = BooleanField("Add and edit templates")
@@ -264,6 +283,18 @@ class InviteUserForm(PermissionsForm):
raise ValidationError("You cant send an invitation to yourself")
class InviteOrgUserForm(StripWhitespaceForm):
email_address = email_address(gov_user=False)
def __init__(self, invalid_email_address, *args, **kwargs):
super(InviteOrgUserForm, self).__init__(*args, **kwargs)
self.invalid_email_address = invalid_email_address.lower()
def validate_email_address(self, field):
if field.data.lower() == self.invalid_email_address:
raise ValidationError("You cant send an invitation to yourself")
class TwoFactorForm(StripWhitespaceForm):
def __init__(self, validate_code_func, *args, **kwargs):
'''
@@ -419,7 +450,7 @@ class ChangeEmailForm(StripWhitespaceForm):
def validate_email_address(self, field):
is_valid = self.validate_email_func(field.data)
if not is_valid:
if is_valid:
raise ValidationError("The email address is already in use")

View File

@@ -4,38 +4,23 @@ from flask import (
session,
flash,
render_template,
abort,
current_app
abort
)
from itsdangerous import SignatureExpired
from markupsafe import Markup
from notifications_utils.url_safe_token import check_token
from flask_login import current_user
from app.main import main
from app import (
invite_api_client,
org_invite_api_client,
user_api_client,
organisations_client,
service_api_client
)
@main.route("/invitation/<token>")
def accept_invite(token):
try:
check_token(
token,
current_app.config['SECRET_KEY'],
current_app.config['DANGEROUS_SALT'],
current_app.config['INVITATION_EXPIRY_SECONDS']
)
except SignatureExpired:
errors = [
'Your invitation to GOV.UK Notify has expired. '
'Please ask the person that invited you to send you another one'
]
return render_template("error/400.html", message=errors), 400
invited_user = invite_api_client.check_token(token)
if not current_user.is_anonymous and current_user.email_address.lower() != invited_user.email_address.lower():
@@ -88,3 +73,44 @@ def accept_invite(token):
return redirect(url_for('main.service_dashboard', service_id=invited_user.service))
else:
return redirect(url_for('main.register_from_invite'))
@main.route("/organisation-invitation/<token>")
def accept_org_invite(token):
invited_org_user = org_invite_api_client.check_token(token)
if not current_user.is_anonymous and current_user.email_address.lower() != invited_org_user.email_address.lower():
message = Markup("""
Youre signed in as {}.
This invite is for another email address.
<a href={}>Sign out</a> and click the link again to accept this invite.
""".format(
current_user.email_address,
url_for("main.sign_out", _external=True)))
flash(message=message)
abort(403)
if invited_org_user.status == 'cancelled':
invited_by = user_api_client.get_user(invited_org_user.invited_by)
organisation = organisations_client.get_organisation(invited_org_user.organisation)
return render_template('views/cancelled-invitation.html',
from_user=invited_by.name,
organisation_name=organisation['name'])
if invited_org_user.status == 'accepted':
session.pop('invited_org_user', None)
return redirect(url_for('main.organisation_dashboard', org_id=invited_org_user.organisation))
session['invited_org_user'] = invited_org_user.serialize()
existing_user = user_api_client.get_user_by_email_or_none(invited_org_user.email_address)
organisation_users = user_api_client.get_users_for_organisation(invited_org_user.organisation)
if existing_user:
org_invite_api_client.accept_invite(invited_org_user.organisation, invited_org_user.id)
if existing_user not in organisation_users:
user_api_client.add_user_to_organisation(invited_org_user.organisation, existing_user.id)
return redirect(url_for('main.organisation_dashboard', org_id=invited_org_user.organisation))
else:
return redirect(url_for('main.register_from_org_invite'))

View File

@@ -1,11 +1,31 @@
from flask import redirect, render_template, url_for
from flask_login import login_required
from flask import (
redirect,
render_template,
url_for,
flash,
request
)
from flask_login import (
login_required,
current_user
)
from app import organisations_client
from app import (
organisations_client,
org_invite_api_client,
user_api_client,
)
from app.main.forms import (
SearchUsersForm,
InviteOrgUserForm,
)
from app.main import main
from app.main.forms import CreateOrUpdateOrganisation
from app.utils import user_has_permissions
from notifications_python_client.errors import HTTPError
from werkzeug.exceptions import abort
@main.route("/organisations", methods=['GET'])
@login_required
@@ -38,7 +58,7 @@ def add_organisation():
)
@main.route("/organisation/<org_id>", methods=['GET'])
@main.route("/organisations/<org_id>", methods=['GET'])
@login_required
@user_has_permissions(admin_override=True)
def organisation_dashboard(org_id):
@@ -50,7 +70,7 @@ def organisation_dashboard(org_id):
)
@main.route("/organisation/<org_id>/edit", methods=['GET', 'POST'])
@main.route("/organisations/<org_id>/edit", methods=['GET', 'POST'])
@login_required
@user_has_permissions(admin_override=True)
def update_organisation(org_id):
@@ -75,11 +95,96 @@ def update_organisation(org_id):
)
@main.route("/organisation/<org_id>/users", methods=['GET'])
@main.route("/organisations/<org_id>/users", methods=['GET'])
@login_required
@user_has_permissions(admin_override=True)
def organisation_users(org_id):
def manage_org_users(org_id):
users = sorted(
user_api_client.get_users_for_organisation(org_id=org_id) + [
invite for invite in org_invite_api_client.get_invites_for_organisation(org_id=org_id)
if invite.status != 'accepted'
],
key=lambda user: user.email_address,
)
return render_template(
'views/organisations/organisation/users/index.html',
users=users,
show_search_box=(len(users) > 7),
form=SearchUsersForm(),
)
@main.route("/organisations/<org_id>/users/invite", methods=['GET', 'POST'])
@login_required
@user_has_permissions(admin_override=True)
def invite_org_user(org_id):
form = InviteOrgUserForm(
invalid_email_address=current_user.email_address
)
if form.validate_on_submit():
email_address = form.email_address.data
invited_org_user = org_invite_api_client.create_invite(
current_user.id,
org_id,
email_address
)
flash('Invite sent to {}'.format(invited_org_user.email_address), 'default_with_tick')
return redirect(url_for('.manage_org_users', org_id=org_id))
return render_template(
'views/organisations/organisation/users/invite-org-user.html',
form=form
)
@main.route("/organisations/<org_id>/users/<user_id>", methods=['GET', 'POST'])
@login_required
@user_has_permissions(admin_override=True)
def edit_user_org_permissions(org_id, user_id):
user = user_api_client.get_user(user_id)
return render_template(
'views/organisations/organisation/users/user/index.html',
user=user
)
@main.route("/organisations/<org_id>/users/<user_id>/delete", methods=['GET', 'POST'])
@login_required
@user_has_permissions(admin_override=True)
def remove_user_from_organisation(org_id, user_id):
user = user_api_client.get_user(user_id)
if request.method == 'POST':
try:
organisations_client.remove_user_from_organisation(org_id, user_id)
except HTTPError as e:
msg = "You cannot remove the only user for a service"
if e.status_code == 400 and msg in e.message:
flash(msg, 'info')
return redirect(url_for(
'.manage_org_users',
org_id=org_id))
else:
abort(500, e)
return redirect(url_for(
'.manage_org_users',
org_id=org_id
))
flash('Are you sure you want to remove {}?'.format(user.name), 'remove')
return render_template(
'views/organisations/organisation/users/user/index.html',
user=user,
)
@main.route("/organisations/<org_id>/cancel-invited-user/<invited_user_id>", methods=['GET'])
@login_required
@user_has_permissions(admin_override=True)
def cancel_invited_org_user(org_id, invited_user_id):
org_invite_api_client.cancel_invited_user(org_id=org_id, invited_user_id=invited_user_id)
return redirect(url_for('main.manage_org_users', org_id=org_id))

View File

@@ -17,13 +17,15 @@ from app.main import main
from app.main.forms import (
RegisterUserForm,
RegisterUserFromInviteForm
RegisterUserFromInviteForm,
RegisterUserFromOrgInviteForm
)
from app.main.views.verify import activate_user
from app import (
user_api_client,
invite_api_client
invite_api_client,
org_invite_api_client
)
@@ -65,20 +67,42 @@ def register_from_invite():
return render_template('views/register-from-invite.html', invited_user=invited_user, form=form)
def _do_registration(form, send_sms=True, send_email=True):
if user_api_client.is_email_unique(form.email_address.data):
@main.route('/register-from-org-invite', methods=['GET', 'POST'])
def register_from_org_invite():
invited_org_user = session.get('invited_org_user')
if not invited_org_user:
abort(404)
form = RegisterUserFromOrgInviteForm(
invited_org_user,
)
form.auth_type.data = 'sms_auth'
if form.validate_on_submit():
if (form.organisation.data != invited_org_user['organisation'] or
form.email_address.data != invited_org_user['email_address']):
abort(400)
_do_registration(form, send_email=False, send_sms=True, organisation_id=invited_org_user['organisation'])
org_invite_api_client.accept_invite(invited_org_user['organisation'], invited_org_user['id'])
user_api_client.add_user_to_organisation(invited_org_user['organisation'], session['user_details']['id'])
return redirect(url_for('main.verify'))
return render_template('views/register-from-org-invite.html', invited_org_user=invited_org_user, form=form)
def _do_registration(form, send_sms=True, send_email=True, organisation_id=None):
if user_api_client.is_email_already_in_use(form.email_address.data):
user = user_api_client.get_user_by_email(form.email_address.data)
if send_email:
user_api_client.send_already_registered_email(user.id, user.email_address)
session['expiry_date'] = str(datetime.utcnow() + timedelta(hours=1))
session['user_details'] = {"email": user.email_address, "id": user.id}
else:
user = user_api_client.register_user(form.name.data,
form.email_address.data,
form.mobile_number.data or None,
form.password.data,
form.auth_type.data)
# TODO possibly there should be some exception handling
# for sending sms and email codes.
# How do we report to the user there is a problem with
# sending codes apart from service unavailable?
# at the moment i believe http 500 is fine.
if send_email:
user_api_client.send_verify_email(user.id, user.email_address)
@@ -86,12 +110,8 @@ def _do_registration(form, send_sms=True, send_email=True):
user_api_client.send_verify_code(user.id, 'sms', user.mobile_number)
session['expiry_date'] = str(datetime.utcnow() + timedelta(hours=1))
session['user_details'] = {"email": user.email_address, "id": user.id}
else:
user = user_api_client.get_user_by_email(form.email_address.data)
if send_email:
user_api_client.send_already_registered_email(user.id, user.email_address)
session['expiry_date'] = str(datetime.utcnow() + timedelta(hours=1))
session['user_details'] = {"email": user.email_address, "id": user.id}
if organisation_id:
session['organisation_id'] = organisation_id
@main.route('/registration-continue')

View File

@@ -64,9 +64,9 @@ def user_profile_email():
if not is_gov_user(current_user.email_address):
abort(403)
def _is_email_unique(email):
return user_api_client.is_email_unique(email)
form = ChangeEmailForm(_is_email_unique,
def _is_email_already_in_use(email):
return user_api_client.is_email_already_in_use(email)
form = ChangeEmailForm(_is_email_already_in_use,
email_address=current_user.email_address)
if form.validate_on_submit():

View File

@@ -74,6 +74,10 @@ def activate_user(user_id):
user = user_api_client.get_user(user_id)
# the user will have a new current_session_id set by the API - store it in the cookie for future requests
session['current_session_id'] = user.current_session_id
organisation_id = session.get('organisation_id', None)
activated_user = user_api_client.activate_user(user)
login_user(activated_user)
return redirect(url_for('main.add_service', first='first'))
if organisation_id:
return redirect(url_for('main.organisation_dashboard', org_id=organisation_id))
else:
return redirect(url_for('main.add_service', first='first'))

View File

@@ -34,7 +34,7 @@ class InviteApiClient(NotifyAdminAPIClient):
return invited_users
def check_token(self, token):
resp = self.get(url='/invite/{}'.format(token))
resp = self.get(url='/invite/service/{}'.format(token))
return InvitedUser(**resp['data'])
def cancel_invited_user(self, service_id, invited_user_id):

View File

@@ -31,6 +31,7 @@ class User(UserMixin):
self.max_failed_login_count = max_failed_login_count
self.platform_admin = fields.get('platform_admin')
self.current_session_id = fields.get('current_session_id')
self._organisations = fields.get('organisations', [])
def get_id(self):
return self.id
@@ -158,6 +159,7 @@ class User(UserMixin):
"state": self.state,
"failed_login_count": self.failed_login_count,
"permissions": [x for x in self._permissions],
"organisations": self.organisations,
"current_session_id": self.current_session_id
}
if hasattr(self, '_password'):
@@ -167,6 +169,10 @@ class User(UserMixin):
def set_password(self, pwd):
self._password = pwd
@property
def organisations(self):
return self._organisations
class InvitedUser(object):
@@ -220,6 +226,38 @@ class InvitedUser(object):
return data
class InvitedOrgUser(object):
def __init__(self, id, organisation, invited_by, email_address, status, created_at):
self.id = id
self.organisation = str(organisation)
self.invited_by = invited_by
self.email_address = email_address
self.status = status
self.created_at = created_at
def __eq__(self, other):
return ((self.id,
self.organisation,
self.invited_by,
self.email_address,
self.status) == (other.id,
other.organisation,
other.invited_by,
other.email_address,
other.status))
def serialize(self, permissions_as_string=False):
data = {'id': self.id,
'organisation': self.organisation,
'invited_by': self.invited_by,
'email_address': self.email_address,
'status': self.status,
'created_at': str(self.created_at)
}
return data
class AnonymousUser(AnonymousUserMixin):
# set the anonymous user so that if a new browser hits us we don't error http://stackoverflow.com/a/19275188
def logged_in_elsewhere(self):

View File

@@ -0,0 +1,52 @@
from app.notify_client import _attach_current_user, NotifyAdminAPIClient
from app.notify_client.models import InvitedOrgUser
class OrgInviteApiClient(NotifyAdminAPIClient):
def __init__(self):
super().__init__("a" * 73, "b")
def init_app(self, app):
self.base_url = app.config['API_HOST_NAME']
self.admin_url = app.config['ADMIN_BASE_URL']
self.service_id = app.config['ADMIN_CLIENT_USER_NAME']
self.api_key = app.config['ADMIN_CLIENT_SECRET']
def create_invite(self, invite_from_id, org_id, email_address):
data = {
'email_address': email_address,
'invited_by': invite_from_id,
'invite_link_host': self.admin_url,
}
data = _attach_current_user(data)
resp = self.post(url='/organisation/{}/invite'.format(org_id), data=data)
return InvitedOrgUser(**resp['data'])
def get_invites_for_organisation(self, org_id):
endpoint = '/organisation/{}/invite'.format(org_id)
resp = self.get(endpoint)
invites = resp['data']
invited_users = self._get_invited_org_users(invites)
return invited_users
def check_token(self, token):
resp = self.get(url='/invite/organisation/{}'.format(token))
return InvitedOrgUser(**resp['data'])
def cancel_invited_user(self, org_id, invited_user_id):
data = {'status': 'cancelled'}
data = _attach_current_user(data)
self.post(url='/organisation/{0}/invite/{1}'.format(org_id, invited_user_id),
data=data)
def accept_invite(self, org_id, invited_user_id):
data = {'status': 'accepted'}
self.post(url='/organisation/{0}/invite/{1}'.format(org_id, invited_user_id),
data=data)
def _get_invited_org_users(self, invites):
invited_users = []
for invite in invites:
invited_user = InvitedOrgUser(**invite)
invited_users.append(invited_user)
return invited_users

View File

@@ -1,4 +1,4 @@
from app.notify_client import NotifyAdminAPIClient
from app.notify_client import _attach_current_user, NotifyAdminAPIClient
class OrganisationsClient(NotifyAdminAPIClient):
@@ -43,3 +43,10 @@ class OrganisationsClient(NotifyAdminAPIClient):
def get_organisation_services(self, org_id):
return self.get(url="/organisations/{}/services".format(org_id))
def remove_user_from_organisation(self, org_id, user_id):
endpoint = '/organisations/{}/users/{}'.format(
org_id=org_id,
user_id=user_id)
data = _attach_current_user({})
return self.delete(endpoint, data)

View File

@@ -137,12 +137,21 @@ class UserApiClient(NotifyAdminAPIClient):
if user.has_permissions(permission, any_=True)
])
def get_users_for_organisation(self, org_id):
endpoint = '/organisations/{}/users'.format(org_id)
resp = self.get(endpoint)
return [User(data) for data in resp['data']]
def add_user_to_service(self, service_id, user_id, permissions):
endpoint = '/service/{}/users/{}'.format(service_id, user_id)
data = [{'permission': x} for x in permissions]
resp = self.post(endpoint, data=data)
return User(resp['data'], max_failed_login_count=self.max_failed_login_count)
def add_user_to_organisation(self, org_id, user_id):
resp = self.post('/organisations/{}/users/{}'.format(org_id, user_id), data={})
return User(resp['data'], max_failed_login_count=self.max_failed_login_count)
def set_user_permissions(self, user_id, service_id, permissions):
data = [{'permission': x} for x in permissions]
endpoint = '/user/{}/service/{}/permission'.format(user_id, service_id)
@@ -153,10 +162,10 @@ class UserApiClient(NotifyAdminAPIClient):
data = {'email': email_address}
self.post(endpoint, data=data)
def is_email_unique(self, email_address):
def is_email_already_in_use(self, email_address):
if self.get_user_by_email_or_none(email_address):
return False
return True
return True
return False
def activate_user(self, user):
if user.state == 'pending':

View File

@@ -2,7 +2,7 @@
<ul>
<li><a href="{{ url_for('.organisation_dashboard', org_id=current_org.id) }}">Dashboard</a></li>
{% if current_user.has_permissions(admin_override=True) %}
<li><a href="{{ url_for('.organisation_users', org_id=current_org.id) }}">Team members</a></li>
<li><a href="{{ url_for('.manage_org_users', org_id=current_org.id) }}">Team members</a></li>
{% endif %}
</ul>
</nav>

View File

@@ -10,7 +10,7 @@
{{ from_user }} decided to cancel this invitation.
</p>
<p>
If you need access to {{ service_name }}, youll have to ask them to invite you again.
If you need access to {{ service_name or organisation_name }}, youll have to ask them to invite you again.
</p>
</div>
</div>

View File

@@ -1,4 +1,8 @@
{% extends "org_template.html" %}
{% from "components/table.html" import list_table, row, field, hidden_field_heading %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/tick-cross.html" import tick_cross %}
{% from "components/textbox.html" import textbox %}
{% block org_page_title %}
Team members
@@ -12,6 +16,52 @@
Team members
</h1>
</div>
<div class="column-one-third">
<a href="{{ url_for('.invite_org_user', org_id=current_org.id) }}" class="button align-with-heading">Invite team member</a>
</div>
</div>
{% if show_search_box %}
<div data-module="autofocus">
<div class="live-search" data-module="live-search" data-targets=".user-list-item">
{{ textbox(
form.search,
width='1-1'
) }}
</div>
</div>
{% endif %}
<div class="user-list">
{% for user in users %}
<div class="user-list-item">
<h3>
{%- if user.name -%}
<span class="heading-small">{{ user.name }}</span>&ensp;
{%- endif -%}
<span class="hint">
{%- if user.status == 'pending' -%}
{{ user.email_address }} (invited)
{%- elif user.status == 'cancelled' -%}
{{ user.email_address }} (cancelled invite)
{%- elif user.id == current_user.id -%}
(you)
{% else %}
{{ user.email_address }}
{% endif %}
</span>
</h3>
<ul class="tick-cross-list">
<li class="tick-cross-list-edit-link">
{% if user.status == 'pending' %}
<a href="{{ url_for('.cancel_invited_org_user', org_id=current_org.id, invited_user_id=user.id)}}">Cancel invitation</a>
{% endif %}
</li>
</ul>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@@ -0,0 +1,33 @@
{% extends "org_template.html" %}
{% from "components/textbox.html" import textbox %}
{% from "components/page-footer.html" import page_footer %}
{% block service_page_title %}
Invite a team member
{% endblock %}
{% block maincolumn_content %}
<h1 class="heading-large">
Invite a team member
</h1>
<div class="grid-row">
<form method="post" class="column-three-quarters" novalidate>
{{ textbox(form.email_address, width='1-1', safe_error_message=True) }}
<div class="bottom-gutter">
<p class="form-label">
All team members can see:
</p>
<ul class="list list-bullet">
<li>dashboard</li>
<li>who the other team members are</li>
</ul>
</div>
{{ page_footer('Send invitation email') }}
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,28 @@
{% extends "withnav_template.html" %}
{% from "components/textbox.html" import textbox %}
{% from "components/page-footer.html" import page_footer %}
{% block service_page_title %}
{{ user.name or user.email_localpart }}
{% endblock %}
{% block maincolumn_content %}
<h1 class="heading-large">
{{ user.name or user.email_localpart }}
</h1>
<p>
{{ user.email_address }}
</p>
<form method="post" class="column-three-quarters">
{{ page_footer(
'Save',
back_link=url_for('.manage_org_users', org_id=current_org.id),
back_link_text="Back",
delete_link=url_for('.remove_user_from_organisation', org_id=current_org.id, user_id=user.id) if user or None,
delete_link_text='Remove user from organisation'
) }}
</form>
{% endblock %}

View File

@@ -0,0 +1,28 @@
{% extends "withoutnav_template.html" %}
{% from "components/textbox.html" import textbox %}
{% from "components/page-footer.html" import page_footer %}
{% block per_page_title %}
Create an account
{% endblock %}
{% block maincolumn_content %}
<div class="grid-row">
<div class="column-two-thirds">
<h1 class="heading-large">Create an account</h1>
<p>Your account will be created with this email: {{invited_org_user.email_address}}</p>
<form method="post" autocomplete="off">
{{ textbox(form.name, width='3-4') }}
<div class="extra-tracking">
{{ textbox(form.mobile_number, width='3-4', hint='Well send you a security code by text message') }}
</div>
{{ textbox(form.password, hint="At least 8 characters", width='3-4') }}
{{ page_footer("Continue") }}
{{form.organisation}}
{{form.email_address}}
</form>
</div>
</div>
{% endblock %}

View File

@@ -5,6 +5,9 @@ from datetime import datetime, timedelta, timezone
from flask.testing import FlaskClient
from flask import url_for
from flask_login import login_user
from app.notify_client.models import (
InvitedOrgUser,
)
class TestClient(FlaskClient):
@@ -37,6 +40,85 @@ def created_by_json(id_, name='', email_address=''):
return {'id': id_, 'name': name, 'email_address': email_address}
def user_json(
id_='1234',
name='Test User',
email_address='test@gov.uk',
mobile_number='+447700900986',
password_changed_at=None,
permissions={generate_uuid(): [
'send_texts',
'send_emails',
'send_letters',
'manage_users',
'manage_templates',
'manage_settings',
'manage_api_keys']
},
auth_type='sms_auth',
failed_login_count=0,
state='active',
max_failed_login_count=3,
platform_admin=False,
current_session_id='1234',
organisations=[],
):
return {
'id': id_,
'name': name,
'email_address': email_address,
'mobile_number': mobile_number,
'password_changed_at': password_changed_at,
'permissions': permissions,
'auth_type': auth_type,
'failed_login_count': failed_login_count,
'state': state,
'max_failed_login_count': max_failed_login_count,
'platform_admin': platform_admin,
'current_session_id': current_session_id,
'organisations': organisations
}
def invited_user(
_id='1234',
service=None,
from_user='1234',
email_address='testinviteduser@gov.uk',
permissions=None,
status='pending',
created_at=datetime.utcnow(),
auth_type='sms_auth',
organisation=None
):
org_user = organisation is not None
data = {
'id': _id,
'from_user': from_user,
'email_address': email_address,
'status': status,
'created_at': created_at,
'auth_type': auth_type,
}
if service:
data['service'] = service
if permissions:
data['permissions'] = permissions
if organisation:
data['organisation'] = organisation
if org_user:
return InvitedOrgUser(
data['id'],
data['organisation'],
data['from_user'],
data['email_address'],
data['status'],
data['created_at']
)
def service_json(
id_='1234',
name='Test Service',
@@ -88,6 +170,28 @@ def service_json(
}
def organisation_json(
id_='1234',
name='Test Organisation',
users=None,
active=True,
created_at=None,
services=None
):
if users is None:
users = []
if services is None:
services = []
return {
'id': id_,
'name': name,
'active': active,
'users': users,
'services': services,
'created_at': created_at or str(datetime.utcnow())
}
def template_json(service_id,
id_,
name="sample template",
@@ -166,6 +270,17 @@ def invite_json(id_, from_user, service_id, email_address, permissions, created_
}
def org_invite_json(id_, invited_by, org_id, email_address, created_at, status):
return {
'id': id_,
'invited_by': invited_by,
'organisation': org_id,
'email_address': email_address,
'status': status,
'created_at': created_at,
}
TEST_USER_EMAIL = 'test@user.gov.uk'

View File

@@ -26,7 +26,6 @@ def test_load_service_before_request_handles_404(client_request, mocker):
@pytest.mark.parametrize('url', [
'/invitation/MALFORMED_TOKEN',
'/new-password/MALFORMED_TOKEN',
'/user-profile/email/confirm/MALFORMED_TOKEN',
'/verify-email/MALFORMED_TOKEN'

View File

@@ -1,7 +1,6 @@
from flask import url_for
from bs4 import BeautifulSoup
from unittest.mock import ANY
from itsdangerous import SignatureExpired
import app
from app.notify_client.models import InvitedUser
@@ -22,8 +21,6 @@ def test_existing_user_accept_invite_calls_api_and_redirects_to_dashboard(
mock_get_service,
mocker,
):
mocker.patch('app.main.views.invites.check_token')
expected_service = service_one['id']
expected_permissions = ['send_messages', 'manage_service', 'manage_api_keys']
@@ -50,8 +47,6 @@ def test_existing_user_with_no_permissions_accept_invite(
mock_add_user_to_service,
mock_get_service,
):
mocker.patch('app.main.views.invites.check_token')
expected_service = service_one['id']
sample_invite['permissions'] = ''
expected_permissions = []
@@ -69,8 +64,6 @@ def test_if_existing_user_accepts_twice_they_redirect_to_sign_in(
sample_invite,
mock_get_service,
):
mocker.patch('app.main.views.invites.check_token')
sample_invite['status'] = 'accepted'
invite = InvitedUser(**sample_invite)
mocker.patch('app.invite_api_client.check_token', return_value=invite)
@@ -96,7 +89,6 @@ def test_existing_user_of_service_get_redirected_to_signin(
mock_get_user_by_email,
mock_accept_invite,
):
mocker.patch('app.main.views.invites.check_token')
sample_invite['email_address'] = api_user_active.email_address
invite = InvitedUser(**sample_invite)
mocker.patch('app.invite_api_client.check_token', return_value=invite)
@@ -128,8 +120,6 @@ def test_existing_signed_out_user_accept_invite_redirects_to_sign_in(
mock_get_service,
mocker,
):
mocker.patch('app.main.views.invites.check_token')
expected_service = service_one['id']
expected_permissions = ['send_messages', 'manage_service', 'manage_api_keys']
@@ -161,8 +151,6 @@ def test_new_user_accept_invite_calls_api_and_redirects_to_registration(
mock_get_service,
mocker,
):
mocker.patch('app.main.views.invites.check_token')
expected_redirect_location = 'http://localhost/register-from-invite'
response = client.get(url_for('main.accept_invite', token='thisisnotarealtoken'))
@@ -184,8 +172,6 @@ def test_new_user_accept_invite_calls_api_and_views_registration_page(
mock_get_service,
mocker,
):
mocker.patch('app.main.views.invites.check_token')
response = client.get(url_for('main.accept_invite', token='thisisnotarealtoken'), follow_redirects=True)
mock_check_invite_token.assert_called_with('thisisnotarealtoken')
@@ -219,7 +205,6 @@ def test_cancelled_invited_user_accepts_invited_redirect_to_cancelled_invitation
mock_get_user,
mock_get_service,
):
mocker.patch('app.main.views.invites.check_token')
cancelled_invitation = create_sample_invite(mocker, service_one, status='cancelled')
mock_check_token_invite(mocker, cancelled_invitation)
response = client.get(url_for('main.accept_invite', token='thisisnotarealtoken'))
@@ -237,7 +222,7 @@ def test_new_user_accept_invite_completes_new_registration_redirects_to_verify(
api_user_active,
mock_check_invite_token,
mock_dont_get_user_by_email,
mock_is_email_unique,
mock_email_is_not_already_in_use,
mock_register_user,
mock_send_verify_code,
mock_accept_invite,
@@ -246,8 +231,6 @@ def test_new_user_accept_invite_completes_new_registration_redirects_to_verify(
mock_get_service,
mocker,
):
mocker.patch('app.main.views.invites.check_token')
expected_service = service_one['id']
expected_email = sample_invite['email_address']
expected_from_user = service_one['users'][0]
@@ -297,7 +280,6 @@ def test_signed_in_existing_user_cannot_use_anothers_invite(
mock_accept_invite,
mock_get_service,
):
mocker.patch('app.main.views.invites.check_token')
invite = InvitedUser(**sample_invite)
mocker.patch('app.invite_api_client.check_token', return_value=invite)
mocker.patch('app.user_api_client.get_users_for_service', return_value=[api_user_active])
@@ -324,8 +306,6 @@ def test_accept_invite_does_not_treat_email_addresses_as_case_sensitive(
mock_accept_invite,
mock_get_user_by_email
):
mocker.patch('app.main.views.invites.check_token')
# the email address of api_user_active is 'test@user.gov.uk'
sample_invite['email_address'] = 'TEST@user.gov.uk'
invite = InvitedUser(**sample_invite)
@@ -345,7 +325,7 @@ def test_new_invited_user_verifies_and_added_to_service(
api_user_active,
mock_check_invite_token,
mock_dont_get_user_by_email,
mock_is_email_unique,
mock_email_is_not_already_in_use,
mock_register_user,
mock_send_verify_code,
mock_check_verify_code,
@@ -363,8 +343,6 @@ def test_new_invited_user_verifies_and_added_to_service(
mock_get_usage,
mocker,
):
mocker.patch('app.main.views.invites.check_token')
# visit accept token page
response = client.get(url_for('main.accept_invite', token='thisisnotarealtoken'))
assert response.status_code == 302
@@ -404,24 +382,6 @@ def test_new_invited_user_verifies_and_added_to_service(
assert page.find('h1').text == 'Dashboard'
def test_gives_message_if_token_has_expired(
app_,
client,
mock_check_invite_token,
mocker,
):
check_token = mocker.patch('app.main.views.invites.check_token', side_effect=SignatureExpired('this is too old'))
response = client.get(url_for('main.accept_invite', token='a really old token'))
raw_html = response.data.decode('utf-8')
page = BeautifulSoup(raw_html, 'html.parser')
check_token.assert_called_once_with(ANY, ANY, ANY, 3600 * 24 * 2)
assert response.status_code == 400
assert 'Your invitation to GOV.UK Notify has expired' in page.find('h1').text
assert not mock_check_invite_token.called
def test_existing_user_accepts_and_sets_email_auth(
client_request,
api_user_active,
@@ -434,7 +394,6 @@ def test_existing_user_accepts_and_sets_email_auth(
mock_add_user_to_service,
mocker
):
mocker.patch('app.main.views.invites.check_token')
sample_invite['email_address'] = api_user_active.email_address
service_one['permissions'].append('email_auth')
@@ -465,7 +424,6 @@ def test_existing_user_doesnt_get_auth_changed_by_service_without_permission(
mock_add_user_to_service,
mocker
):
mocker.patch('app.main.views.invites.check_token')
sample_invite['email_address'] = api_user_active.email_address
assert 'email_auth' not in service_one['permissions']
@@ -495,7 +453,6 @@ def test_existing_email_auth_user_without_phone_cannot_set_sms_auth(
mock_add_user_to_service,
mocker
):
mocker.patch('app.main.views.invites.check_token')
sample_invite['email_address'] = api_user_active.email_address
service_one['permissions'].append('email_auth')
@@ -529,7 +486,6 @@ def test_existing_email_auth_user_with_phone_can_set_sms_auth(
mock_add_user_to_service,
mocker
):
mocker.patch('app.main.views.invites.check_token')
sample_invite['email_address'] = api_user_active.email_address
service_one['permissions'].append('email_auth')

View File

@@ -1,9 +1,18 @@
import pytest
from bs4 import BeautifulSoup
from flask import url_for
from datetime import (
datetime,
timedelta
)
from tests.conftest import (
normalize_spaces
)
from unittest.mock import ANY
from app.notify_client.models import InvitedOrgUser
def test_organisation_page_shows_all_organisations(
@@ -151,3 +160,364 @@ def test_organisation_services_show(
assert normalize_spaces(
page.select('.browse-list-item a')[i]['href']
) == '/services/{}'.format(service_id)
def test_view_team_members(
logged_in_platform_admin_client,
mocker,
mock_get_organisation,
mock_get_users_for_organisation,
mock_get_invited_users_for_organisation,
fake_uuid
):
response = logged_in_platform_admin_client.get(
url_for('.manage_org_users', org_id=fake_uuid),
)
assert response.status_code == 200
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
for i in range(0, 2):
assert normalize_spaces(
page.select('.user-list-item .heading-small')[i].text
) == 'Test User {}'.format(i + 1)
assert normalize_spaces(
page.select('.tick-cross-list-edit-link')[1].text
) == 'Cancel invitation'
def test_invite_org_user(
logged_in_platform_admin_client,
mocker,
mock_get_organisation,
sample_org_invite,
fake_uuid
):
mock_invite_org_user = mocker.patch(
'app.org_invite_api_client.create_invite',
return_value=InvitedOrgUser(**sample_org_invite)
)
logged_in_platform_admin_client.post(
url_for('.invite_org_user', org_id=mock_get_organisation['id']),
data={'email_address': 'test@example.gov.uk'}
)
mock_invite_org_user.assert_called_once_with(
sample_org_invite['invited_by'],
'{}'.format(mock_get_organisation['id']),
'test@example.gov.uk',
)
def test_invite_org_user_errors_when_same_email_as_inviter(
logged_in_platform_admin_client,
mocker,
mock_get_organisation,
sample_org_invite,
fake_uuid
):
new_org_user_data = {
'email_address': 'platform@admin.gov.uk',
}
mock_invite_org_user = mocker.patch(
'app.org_invite_api_client.create_invite',
return_value=InvitedOrgUser(**sample_org_invite)
)
response = logged_in_platform_admin_client.post(
url_for('.invite_org_user', org_id=mock_get_organisation['id']),
data=new_org_user_data
)
assert response.status_code == 200
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
assert mock_invite_org_user.called is False
assert normalize_spaces(page.select_one('.error-message').text) == 'You cant send an invitation to yourself'
# Broken
def test_accepted_invite_when_user_already_logged_in(
logged_in_client,
mock_check_org_invite_token
):
response = logged_in_client.get(
url_for('main.accept_org_invite', token='thisisnotarealtoken'),
follow_redirects=True
)
assert response.status_code == 403
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
assert 'This invite is for another email address.' in normalize_spaces(page.select_one('.banner-dangerous').text)
def test_cancelled_invite_opened_by_user(
client,
mock_check_org_cancelled_invite_token,
mock_get_organisation,
mock_get_user,
fake_uuid
):
response = client.get(url_for('main.accept_org_invite', token='thisisnotarealtoken'), follow_redirects=True)
assert response.status_code == 200
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
assert normalize_spaces(
page.select_one('h1').text
) == 'The invitation you were sent has been cancelled'
assert normalize_spaces(
page.select('main p')[0].text
) == 'Test User decided to cancel this invitation.'
assert normalize_spaces(
page.select('main p')[1].text
) == 'If you need access to Org 1, youll have to ask them to invite you again.'
mock_get_user.assert_called_once_with(fake_uuid)
mock_get_organisation.assert_called_once_with('596364a0-858e-42c8-9062-a8fe822260af')
def test_user_invite_already_accepted(
client,
mock_check_org_accepted_invite_token
):
response = client.get(url_for('main.accept_org_invite', token='thisisnotarealtoken'))
assert response.status_code == 302
assert response.location == url_for(
'main.organisation_dashboard',
org_id='596364a0-858e-42c8-9062-a8fe822260af',
_external=True
)
def test_existing_user_invite_already_is_member_of_organisation(
client,
mock_check_org_invite_token,
mock_get_user_by_email,
mock_get_users_for_organisation,
mock_accept_org_invite,
mock_add_user_to_organisation,
fake_uuid
):
response = client.get(url_for('main.accept_org_invite', token='thisisnotarealtoken'))
assert response.status_code == 302
assert response.location == url_for(
'main.organisation_dashboard',
org_id='596364a0-858e-42c8-9062-a8fe822260af',
_external=True
)
mock_accept_org_invite.assert_called_once_with('596364a0-858e-42c8-9062-a8fe822260af', ANY)
mock_get_user_by_email.assert_called_once_with('invited_user@test.gov.uk')
mock_get_users_for_organisation.assert_called_once_with('596364a0-858e-42c8-9062-a8fe822260af')
def test_existing_user_invite_not_a_member_of_organisation(
client,
mock_check_org_invite_token,
mock_get_user_by_email,
mock_get_users_for_organisation,
mock_accept_org_invite,
mock_add_user_to_organisation,
fake_uuid
):
response = client.get(url_for('main.accept_org_invite', token='thisisnotarealtoken'))
assert response.status_code == 302
assert response.location == url_for(
'main.organisation_dashboard',
org_id='596364a0-858e-42c8-9062-a8fe822260af',
_external=True
)
mock_accept_org_invite.assert_called_once_with('596364a0-858e-42c8-9062-a8fe822260af', ANY)
mock_get_user_by_email.assert_called_once_with('invited_user@test.gov.uk')
mock_get_users_for_organisation.assert_called_once_with('596364a0-858e-42c8-9062-a8fe822260af')
mock_add_user_to_organisation.assert_called_once_with(
'596364a0-858e-42c8-9062-a8fe822260af',
'6ce466d0-fd6a-11e5-82f5-e0accb9d11a6'
)
def test_user_accepts_invite(
client,
mock_check_org_invite_token,
mock_dont_get_user_by_email,
mock_get_users_for_organisation,
):
response = client.get(url_for('main.accept_org_invite', token='thisisnotarealtoken'))
assert response.status_code == 302
assert response.location == url_for('main.register_from_org_invite', _external=True)
mock_check_org_invite_token.assert_called_once_with('thisisnotarealtoken')
mock_dont_get_user_by_email.assert_called_once_with('invited_user@test.gov.uk')
mock_get_users_for_organisation.assert_called_once_with('596364a0-858e-42c8-9062-a8fe822260af')
def test_registration_from_org_invite_404s_if_user_not_in_session(
client,
):
response = client.get(url_for('main.register_from_org_invite'))
assert response.status_code == 404
@pytest.mark.parametrize('data, error', [
[{
'name': 'Bad Mobile',
'mobile_number': 'not good',
'password': 'validPassword!'
}, 'Must not contain letters or symbols'],
[{
'name': 'Bad Password',
'mobile_number': '+44123412345',
'password': 'password'
}, 'Choose a password thats harder to guess'],
])
def test_registration_from_org_invite_has_bad_data(
client,
sample_org_invite,
data,
error
):
invited_org_user = InvitedOrgUser(**sample_org_invite)
with client.session_transaction() as session:
session['invited_org_user'] = invited_org_user.serialize()
response = client.post(url_for('main.register_from_org_invite'), data=data)
assert response.status_code == 200
assert error in response.get_data(as_text=True)
@pytest.mark.parametrize('diff_data', [
['email_address'],
['organisation'],
['email_address', 'organisation']
])
def test_registration_from_org_invite_has_different_email_or_organisation(
client,
sample_org_invite,
diff_data
):
invited_org_user = InvitedOrgUser(**sample_org_invite)
with client.session_transaction() as session:
session['invited_org_user'] = invited_org_user.serialize()
for data in diff_data:
session['invited_org_user'][data] = 'different'
response = client.post(url_for('main.register_from_org_invite'), data={
'name': 'Test User',
'mobile_number': '+4407700900460',
'password': 'validPassword!',
'email_address': session['invited_org_user']['email_address'],
'organisation': session['invited_org_user']['organisation']
})
assert response.status_code == 400
def test_org_user_registers_with_email_already_in_use(
client,
sample_org_invite,
mock_email_is_already_in_use,
mock_get_user_by_email,
mock_accept_org_invite,
mock_add_user_to_organisation,
mock_send_already_registered_email,
mock_register_user
):
invited_org_user = InvitedOrgUser(**sample_org_invite)
with client.session_transaction() as session:
session['invited_org_user'] = invited_org_user.serialize()
response = client.post(url_for('main.register_from_org_invite'), data={
'name': 'Test User',
'mobile_number': '+4407700900460',
'password': 'validPassword!',
'email_address': session['invited_org_user']['email_address'],
'organisation': session['invited_org_user']['organisation']
})
assert response.status_code == 302
assert response.location == url_for('main.verify', _external=True)
mock_get_user_by_email.assert_called_once_with(
session['invited_org_user']['email_address']
)
assert mock_register_user.called is False
assert mock_send_already_registered_email.called is False
def test_org_user_registration(
client,
sample_org_invite,
mock_email_is_not_already_in_use,
mock_register_user,
mock_send_verify_code,
mock_get_user_by_email,
mock_send_verify_email,
mock_accept_org_invite,
mock_add_user_to_organisation,
):
invited_org_user = InvitedOrgUser(**sample_org_invite)
with client.session_transaction() as session:
session['invited_org_user'] = invited_org_user.serialize()
response = client.post(url_for('main.register_from_org_invite'), data={
'name': 'Test User',
'email_address': session['invited_org_user']['email_address'],
'mobile_number': '+4407700900460',
'password': 'validPassword!',
'organisation': session['invited_org_user']['organisation']
})
assert response.status_code == 302
assert response.location == url_for('main.verify', _external=True)
mock_get_user_by_email.called is False
mock_register_user.assert_called_once_with(
'Test User',
session['invited_org_user']['email_address'],
'+4407700900460',
'validPassword!',
'sms_auth'
)
mock_send_verify_code.assert_called_once_with(
'6ce466d0-fd6a-11e5-82f5-e0accb9d11a6',
'sms',
'+4407700900460',
)
def test_verified_org_user_redirects_to_dashboard(
client,
sample_org_invite,
mock_check_verify_code,
mock_get_user,
mock_activate_user,
mock_login,
):
invited_org_user = InvitedOrgUser(**sample_org_invite).serialize()
with client.session_transaction() as session:
session['expiry_date'] = str(datetime.utcnow() + timedelta(hours=1))
session['user_details'] = {"email": invited_org_user['email_address'], "id": invited_org_user['id']}
session['organisation_id'] = invited_org_user['organisation']
response = client.post(url_for('main.verify'), data={'sms_code': '12345'})
assert response.status_code == 302
assert response.location == url_for(
'main.organisation_dashboard',
org_id=invited_org_user['organisation'],
_external=True
)

View File

@@ -48,7 +48,7 @@ def test_register_creates_new_user_and_redirects_to_continue_page(
mock_send_verify_code,
mock_register_user,
mock_get_user_by_email_not_found,
mock_is_email_unique,
mock_email_is_not_already_in_use,
mock_send_verify_email,
mock_login,
phone_number_to_register_with,
@@ -122,7 +122,7 @@ def test_should_add_user_details_to_session(
mock_register_user,
mock_get_user,
mock_get_user_by_email_not_found,
mock_is_email_unique,
mock_email_is_not_already_in_use,
mock_send_verify_email,
mock_login,
):
@@ -176,7 +176,7 @@ def test_register_with_existing_email_sends_emails(
def test_register_from_invite(
client,
fake_uuid,
mock_is_email_unique,
mock_email_is_not_already_in_use,
mock_register_user,
mock_send_verify_code,
mock_accept_invite,
@@ -207,7 +207,6 @@ def test_register_from_invite(
def test_register_from_invite_when_user_registers_in_another_browser(
client,
api_user_active,
mock_is_email_not_unique,
mock_get_user_by_email,
mock_accept_invite,
):
@@ -237,7 +236,7 @@ def test_register_from_invite_when_user_registers_in_another_browser(
def test_register_from_email_auth_invite(
client,
sample_invite,
mock_is_email_unique,
mock_email_is_not_already_in_use,
mock_register_user,
mock_get_user,
mock_send_verify_email,
@@ -285,7 +284,7 @@ def test_register_from_email_auth_invite(
def test_can_register_email_auth_without_phone_number(
client,
sample_invite,
mock_is_email_unique,
mock_email_is_not_already_in_use,
mock_register_user,
mock_get_user,
mock_send_verify_email,

View File

@@ -30,6 +30,7 @@ def test_should_redirect_after_name_change(
logged_in_client,
api_user_active,
mock_update_user_attribute,
mock_email_is_not_already_in_use
):
new_name = 'New Name'
data = {'new_name': new_name}
@@ -54,7 +55,7 @@ def test_should_show_email_page(
def test_should_redirect_after_email_change(
logged_in_client,
mock_login,
mock_is_email_unique,
mock_email_is_not_already_in_use,
):
data = {'email_address': 'new_notify@notify.gov.uk'}
response = logged_in_client.post(

View File

@@ -11,11 +11,15 @@ from bs4 import BeautifulSoup
from app import create_app
from app.notify_client.models import (
User,
InvitedUser
InvitedUser,
InvitedOrgUser
)
from . import (
service_json,
organisation_json,
user_json,
invited_user,
TestClient,
template_json,
template_version_json,
@@ -25,7 +29,8 @@ from . import (
invite_json,
sample_uuid,
generate_uuid,
single_notification_json
single_notification_json,
org_invite_json
)
from notifications_utils.url_safe_token import generate_token
@@ -1050,7 +1055,8 @@ def api_user_pending(fake_uuid):
'mobile_number': '07700 900762',
'state': 'pending',
'failed_login_count': 0,
'permissions': {}
'permissions': {},
'organisations': []
}
user = User(user_data)
return user
@@ -1075,7 +1081,8 @@ def platform_admin_user(fake_uuid):
'manage_api_keys',
'view_activity']},
'platform_admin': True,
'auth_type': 'sms_auth'
'auth_type': 'sms_auth',
'organisations': []
}
user = User(user_data)
return user
@@ -1094,7 +1101,8 @@ def api_user_active(fake_uuid, email_address='test@user.gov.uk'):
'permissions': {},
'platform_admin': False,
'auth_type': 'sms_auth',
'password_changed_at': str(datetime.utcnow())
'password_changed_at': str(datetime.utcnow()),
'organisations': []
}
user = User(user_data)
return user
@@ -1113,7 +1121,8 @@ def api_user_active_email_auth(fake_uuid, email_address='test@user.gov.uk'):
'permissions': {},
'platform_admin': False,
'auth_type': 'email_auth',
'password_changed_at': str(datetime.utcnow())
'password_changed_at': str(datetime.utcnow()),
'organisations': []
}
user = User(user_data)
return user
@@ -1132,7 +1141,8 @@ def api_nongov_user_active(fake_uuid):
'permissions': {},
'platform_admin': False,
'auth_type': 'sms_auth',
'password_changed_at': str(datetime.utcnow())
'password_changed_at': str(datetime.utcnow()),
'organisations': []
}
user = User(user_data)
return user
@@ -1159,7 +1169,8 @@ def active_user_with_permissions(fake_uuid):
'manage_api_keys',
'view_activity']},
'platform_admin': False,
'auth_type': 'sms_auth'
'auth_type': 'sms_auth',
'organisations': []
}
user = User(user_data)
return user
@@ -1186,7 +1197,8 @@ def active_user_no_mobile(fake_uuid):
'manage_api_keys',
'view_activity']},
'platform_admin': False,
'auth_type': 'email_auth'
'auth_type': 'email_auth',
'organisations': []
}
user = User(user_data)
return user
@@ -1206,7 +1218,8 @@ def active_user_view_permissions(fake_uuid):
'failed_login_count': 0,
'permissions': {SERVICE_ONE_ID: ['view_activity']},
'platform_admin': False,
'auth_type': 'sms_auth'
'auth_type': 'sms_auth',
'organisations': []
}
user = User(user_data)
return user
@@ -1230,7 +1243,8 @@ def active_user_manage_template_permission(fake_uuid):
'view_activity',
]},
'platform_admin': False,
'auth_type': 'sms_auth'
'auth_type': 'sms_auth',
'organisations': []
}
user = User(user_data)
return user
@@ -1255,7 +1269,8 @@ def active_user_no_api_key_permission(fake_uuid):
'view_activity',
]},
'platform_admin': False,
'auth_type': 'sms_auth'
'auth_type': 'sms_auth',
'organisations': []
}
user = User(user_data)
return user
@@ -1297,7 +1312,8 @@ def api_user_locked(fake_uuid):
'state': 'active',
'failed_login_count': 5,
'permissions': {},
'auth_type': 'sms_auth'
'auth_type': 'sms_auth',
'organisations': []
}
user = User(user_data)
return user
@@ -1315,7 +1331,8 @@ def api_user_request_password_reset(fake_uuid):
'failed_login_count': 5,
'permissions': {},
'password_changed_at': None,
'auth_type': 'sms_auth'
'auth_type': 'sms_auth',
'organisations': []
}
user = User(user_data)
return user
@@ -1333,7 +1350,8 @@ def api_user_changed_password(fake_uuid):
'failed_login_count': 5,
'permissions': {},
'auth_type': 'sms_auth',
'password_changed_at': str(datetime.utcnow() + timedelta(minutes=1))
'password_changed_at': str(datetime.utcnow() + timedelta(minutes=1)),
'organisations': []
}
user = User(user_data)
return user
@@ -1383,6 +1401,19 @@ def mock_get_user(mocker, user=None):
'app.user_api_client.get_user', side_effect=_get_user)
@pytest.fixture(scope='function')
def mock_get_organisation_user(mocker, user=None):
if user is None:
user = api_user_active(fake_uuid())
def _get_user(id_):
user.id = id_
return user
return mocker.patch(
'app.user_api_client.get_user', side_effect=_get_user)
@pytest.fixture(scope='function')
def mock_get_locked_user(mocker, api_user_locked):
return mock_get_user(mocker, user=api_user_locked)
@@ -1520,13 +1551,13 @@ def mock_activate_user(mocker):
@pytest.fixture(scope='function')
def mock_is_email_unique(mocker):
return mocker.patch('app.user_api_client.is_email_unique', return_value=True)
def mock_email_is_already_in_use(mocker):
return mocker.patch('app.user_api_client.is_email_already_in_use', return_value=True)
@pytest.fixture(scope='function')
def mock_is_email_not_unique(mocker):
return mocker.patch('app.user_api_client.is_email_unique', return_value=False)
def mock_email_is_not_already_in_use(mocker):
return mocker.patch('app.user_api_client.is_email_already_in_use', return_value=False)
@pytest.fixture(scope='function')
@@ -1925,14 +1956,14 @@ def mock_get_users_by_service(mocker):
'manage_users',
'manage_templates',
'manage_settings',
'manage_api_keys',
'access_developer_docs']},
'manage_api_keys']},
'state': 'active',
'password_changed_at': None,
'name': 'Test User',
'email_address': 'notify@digital.cabinet-office.gov.uk',
'auth_type': 'sms_auth',
'failed_login_count': 0}]
'failed_login_count': 0,
'organisations': []}]
return [User(data[0])]
return mocker.patch('app.user_api_client.get_users_for_service', side_effect=_get_users_for_service, autospec=True)
@@ -2658,6 +2689,11 @@ def mock_update_service_callback_api(mocker):
return mocker.patch('app.service_api_client.update_service_callback_api', side_effect=_update_service_callback_api)
@pytest.fixture(scope='function')
def organisation_one(api_user_active):
return organisation_json('596364a0-858e-42c8-9062-a8fe822260af', 'organisation one', [api_user_active.id])
@pytest.fixture(scope='function')
def mock_get_organisations(mocker):
def _get_organisations():
@@ -2684,10 +2720,10 @@ def mock_get_organisations(mocker):
@pytest.fixture(scope='function')
def mock_get_organisation(mocker):
def _get_organisation(organisation_id):
def _get_organisation(org_id):
return {
'name': 'Org 1',
'id': organisation_id,
'id': org_id,
'active': True
}
@@ -2730,3 +2766,84 @@ def mock_get_organisation_services(mocker):
'app.organisations_client.get_organisation_services',
side_effect=_get_organisation_services
)
@pytest.fixture(scope='function')
def mock_get_users_for_organisation(mocker):
def _get_users_for_organisation(org_id):
return [
User(user_json(id_='1234', name='Test User 1')),
User(user_json(id_='5678', name='Test User 2', email_address='testt@gov.uk'))
]
return mocker.patch(
'app.user_api_client.get_users_for_organisation',
side_effect=_get_users_for_organisation
)
@pytest.fixture(scope='function')
def mock_get_invited_users_for_organisation(mocker):
def _get_invited_invited_users_for_organisation(org_id):
return [
invited_user(organisation='1234')
]
return mocker.patch(
'app.org_invite_api_client.get_invites_for_organisation',
side_effect=_get_invited_invited_users_for_organisation
)
@pytest.fixture(scope='function')
def sample_org_invite(mocker, organisation_one, status='pending'):
id_ = str(generate_uuid())
invited_by = organisation_one['users'][0]
email_address = 'invited_user@test.gov.uk'
organisation = organisation_one['id']
created_at = str(datetime.utcnow())
return org_invite_json(id_, invited_by, organisation, email_address, created_at, status)
@pytest.fixture(scope='function')
def mock_check_org_invite_token(mocker, sample_org_invite):
def _check_org_token(token):
return InvitedOrgUser(**sample_org_invite)
return mocker.patch('app.org_invite_api_client.check_token', side_effect=_check_org_token)
@pytest.fixture(scope='function')
def mock_check_org_cancelled_invite_token(mocker, sample_org_invite):
def _check_org_token(token):
sample_org_invite['status'] = 'cancelled'
return InvitedOrgUser(**sample_org_invite)
return mocker.patch('app.org_invite_api_client.check_token', side_effect=_check_org_token)
@pytest.fixture(scope='function')
def mock_check_org_accepted_invite_token(mocker, sample_org_invite):
sample_org_invite['status'] = 'accepted'
def _check_org_token(token):
return InvitedOrgUser(**sample_org_invite)
return mocker.patch('app.org_invite_api_client.check_token', side_effect=_check_org_token)
@pytest.fixture(scope='function')
def mock_accept_org_invite(mocker, sample_org_invite):
def _accept(organisation_id, invite_id):
return InvitedOrgUser(**sample_org_invite)
return mocker.patch('app.org_invite_api_client.accept_invite', side_effect=_accept)
@pytest.fixture(scope='function')
def mock_add_user_to_organisation(mocker, organisation_one, api_user_active):
def _add_user(organisation_id, user_id):
return api_user_active
return mocker.patch('app.user_api_client.add_user_to_organisation', side_effect=_add_user)