Merge pull request #977 from GSA/ADMIN-97_Resend_Expired_Invites

Admin 97 resend expired invites
This commit is contained in:
Carlo Costino
2023-12-21 16:23:31 -05:00
committed by GitHub
9 changed files with 106 additions and 11 deletions

3
.gitignore vendored
View File

@@ -129,3 +129,6 @@ playwright/
# Pyenv
.python-version
# Nodenv
.node-version

View File

@@ -24,6 +24,7 @@ EVENT_SCHEMAS = {
"service_id",
"ui_permissions",
},
"resend_user_invite_to_service": {"email_address", "resent_by_id", "service_id"},
"cancel_user_invite_to_service": {"email_address", "canceled_by_id", "service_id"},
"set_user_permissions": {
"user_id",
@@ -63,6 +64,10 @@ def create_cancel_user_invite_to_service_event(**kwargs):
_send_event("cancel_user_invite_to_service", **kwargs)
def create_resend_user_invite_to_service_event(**kwargs):
_send_event("resend_user_invite_to_service", **kwargs)
def create_add_user_to_service_event(**kwargs):
_send_event("add_user_to_service", **kwargs)

View File

@@ -9,6 +9,7 @@ from app.event_handlers import (
create_invite_user_to_service_event,
create_mobile_number_change_event,
create_remove_user_from_service_event,
create_resend_user_invite_to_service_event,
)
from app.formatters import redact_mobile_number
from app.main import main
@@ -331,3 +332,22 @@ def cancel_invited_user(service_id, invited_user_id):
flash(f"Invitation cancelled for {invited_user.email_address}", "default_with_tick")
return redirect(url_for("main.manage_users", service_id=service_id))
@main.route(
"/services/<uuid:service_id>/resend-invite/<uuid:invited_user_id>",
methods=["GET"],
)
@user_has_permissions("manage_service")
def resend_invite(service_id, invited_user_id):
current_service.resend_invite(invited_user_id)
invited_user = InvitedUser.by_id_and_service_id(service_id, invited_user_id)
create_resend_user_invite_to_service_event(
email_address=invited_user.email_address,
resent_by_id=current_user.id,
service_id=service_id,
)
flash(f"Invitation resent for {invited_user.email_address}", "default_with_tick")
return redirect(url_for("main.manage_users", service_id=service_id))

View File

@@ -190,6 +190,15 @@ class Service(JSONModel, SortByNameMixin):
invited_user_id=str(invited_user_id),
)
def resend_invite(self, invited_user_id):
if str(invited_user_id) not in {user.id for user in self.invited_users}:
abort(404)
return invite_api_client.resend_invite(
service_id=self.id,
invited_user_id=str(invited_user_id),
)
def get_team_member(self, user_id):
if str(user_id) not in {user.id for user in self.active_users}:
abort(404)

View File

@@ -32,11 +32,11 @@ class InviteApiClient(NotifyAdminAPIClient):
"folder_permissions": folder_permissions,
}
data = _attach_current_user(data)
resp = self.post(url="/service/{}/invite".format(service_id), data=data)
resp = self.post(url=f"/service/{service_id}/invite", data=data)
return resp["data"]
def get_invites_for_service(self, service_id):
return self.get("/service/{}/invite".format(service_id))["data"]
return self.get(f"/service/{service_id}/invite")["data"]
def get_invited_user(self, invited_user_id):
return self.get(f"/invite/service/{invited_user_id}")["data"]
@@ -46,7 +46,7 @@ class InviteApiClient(NotifyAdminAPIClient):
def get_count_of_invites_with_permission(self, service_id, permission):
if permission not in all_ui_permissions:
raise TypeError("{} is not a valid permission".format(permission))
raise TypeError(f"{permission} is not a valid permission")
return len(
[
invited_user
@@ -56,22 +56,21 @@ class InviteApiClient(NotifyAdminAPIClient):
)
def check_token(self, token):
return self.get(url="/invite/service/check/{}".format(token))["data"]
return self.get(url=f"/invite/service/check/{token}")["data"]
def cancel_invited_user(self, service_id, invited_user_id):
data = {"status": "cancelled"}
data = _attach_current_user(data)
self.post(
url="/service/{0}/invite/{1}".format(service_id, invited_user_id), data=data
)
self.post(url=f"/service/{service_id}/invite/{invited_user_id}", data=data)
def resend_invite(self, service_id, invited_user_id):
self.post(url=f"/service/{service_id}/invite/{invited_user_id}/resend", data={})
@cache.delete("service-{service_id}")
@cache.delete("user-{invited_user_id}")
def accept_invite(self, service_id, invited_user_id):
data = {"status": "accepted"}
self.post(
url="/service/{0}/invite/{1}".format(service_id, invited_user_id), data=data
)
self.post(url=f"/service/{service_id}/invite/{invited_user_id}", data=data)
invite_api_client = InviteApiClient()

View File

@@ -43,6 +43,8 @@
<span class="live-search-relevant">{{ user.email_address }}</span> (invited)
{%- elif user.status == 'cancelled' -%}
<span class="live-search-relevant">{{ user.email_address }}</span> (cancelled invite)
{%- elif user.status == 'expired' -%}
<span class="live-search-relevant">{{ user.email_address }}</span> (expired invite)
{%- elif user.id == current_user.id -%}
<span class="live-search-relevant">(you)</span>
{% else %}
@@ -84,6 +86,8 @@
{% if current_user.has_permissions('manage_service') %}
{% if user.status == 'pending' %}
<a class="user-list-edit-link usa-link" href="{{ url_for('.cancel_invited_user', service_id=current_service.id, invited_user_id=user.id)}}">Cancel invitation<span class="usa-sr-only"> for {{ user.email_address }}</span></a>
{% elif user.status == 'expired' %}
<a class="user-list-edit-link usa-link" href="{{ url_for('.resend_invite', service_id=current_service.id, invited_user_id=user.id)}}">Resend invite<span class="usa-sr-only"> for {{ user.email_address }}</span></a>
{% elif user.is_editable_by(current_user) %}
<a class="user-list-edit-link usa-link" href="{{ url_for('.edit_user_permissions', service_id=current_service.id, user_id=user.id)}}">Change details<span class="usa-sr-only"> for {{ user.name }} {{ user.email_address }}</span></a>
{% endif %}

View File

@@ -1157,6 +1157,35 @@ def test_invite_user_with_email_auth_service(
)
def test_resend_expired_invitation(
client_request,
mock_get_invites_for_service,
expired_invite,
active_user_with_permissions,
mock_get_users_by_service,
mock_get_template_folders,
mocker,
):
mock_resend = mocker.patch("app.invite_api_client.resend_invite")
mocker.patch(
"app.invite_api_client.get_invited_user_for_service",
return_value=expired_invite,
)
page = client_request.get(
"main.resend_invite",
service_id=SERVICE_ONE_ID,
invited_user_id=expired_invite["id"],
_follow_redirects=True,
)
assert normalize_spaces(page.h1.text) == "Team members"
assert mock_resend.called
called_args = set(mock_resend.call_args.args) | set(
mock_resend.call_args.kwargs.values()
)
assert SERVICE_ONE_ID in called_args
assert expired_invite["id"] in called_args
def test_cancel_invited_user_cancels_user_invitations(
client_request,
mock_get_invites_for_service,
@@ -1168,7 +1197,8 @@ def test_cancel_invited_user_cancels_user_invitations(
):
mock_cancel = mocker.patch("app.invite_api_client.cancel_invited_user")
mocker.patch(
"app.invite_api_client.get_invited_user_for_service", return_value=sample_invite
"app.invite_api_client.get_invited_user_for_service",
return_value=sample_invite,
)
page = client_request.get(

View File

@@ -36,6 +36,7 @@ EXCLUDED_ENDPOINTS = tuple(
"callbacks",
"cancel_invited_org_user",
"cancel_invited_user",
"resend_invite",
"cancel_job",
"change_user_auth",
"check_and_resend_text_code",

View File

@@ -1892,6 +1892,30 @@ def sample_invite(mocker, service_one):
)
@pytest.fixture()
def expired_invite(service_one):
id_ = USER_ONE_ID
from_user = service_one["users"][0]
email_address = "invited_user@test.gsa.gov"
service_id = service_one["id"]
permissions = "view_activity,send_emails,send_texts,manage_settings,manage_users,manage_api_keys"
created_at = str(datetime.utcnow() - timedelta(days=3))
auth_type = "sms_auth"
folder_permissions = []
return invite_json(
id_,
from_user,
service_id,
email_address,
permissions,
created_at,
"expired",
auth_type,
folder_permissions,
)
@pytest.fixture()
def mock_create_invite(mocker, sample_invite):
def _create_invite(