diff --git a/atst/models/invitation.py b/atst/models/invitation.py index fb92c178..62628d0a 100644 --- a/atst/models/invitation.py +++ b/atst/models/invitation.py @@ -103,6 +103,10 @@ class Invitation(Base, TimestampsMixin, AuditableMixin): def is_revokable(self): return self.is_pending and not self.is_expired + @property + def can_resend(self): + return self.is_pending or self.is_expired + @property def user_dod_id(self): return self.user.dod_id if self.user is not None else None diff --git a/atst/routes/portfolios/task_orders.py b/atst/routes/portfolios/task_orders.py index 385b4124..d8f4fba8 100644 --- a/atst/routes/portfolios/task_orders.py +++ b/atst/routes/portfolios/task_orders.py @@ -14,7 +14,6 @@ from atst.forms.dd_254 import DD254Form from atst.forms.ko_review import KOReviewForm from atst.forms.officers import EditTaskOrderOfficersForm from atst.models.task_order import Status as TaskOrderStatus -from atst.models.invitation import Status as InvitationStatus from atst.utils.flash import formatted_flash as flash from atst.services.invitation import ( update_officer_invitations, @@ -126,7 +125,10 @@ def resend_invite(portfolio_id, task_order_id, form=None): invitation = Invitations.lookup_by_portfolio_and_user(portfolio, officer) - if invitation.status is not InvitationStatus.PENDING: + if not invitation: + raise NotFoundError("invitation") + + if not invitation.can_resend: raise NoAccessError("invitation") Invitations.revoke(token=invitation.token) @@ -210,7 +212,7 @@ def task_order_invitations(portfolio_id, task_order_id): form=form, ) else: - raise NoAccessError("task_order") + raise NotFoundError("task_order") @portfolios_bp.route( diff --git a/tests/routes/portfolios/test_task_orders.py b/tests/routes/portfolios/test_task_orders.py index 9f3c5fe8..158adf10 100644 --- a/tests/routes/portfolios/test_task_orders.py +++ b/tests/routes/portfolios/test_task_orders.py @@ -1,6 +1,6 @@ from flask import url_for import pytest -from datetime import timedelta, date +from datetime import timedelta, date, datetime from atst.domain.permission_sets import PermissionSets from atst.domain.task_orders import TaskOrders @@ -694,3 +694,63 @@ def test_resend_invite_when_not_pending(app, client, user_session, portfolio, us assert original_invitation.status == InvitationStatus.ACCEPTED assert response.status_code == 404 assert len(queue.get_queue()) == queue_length + + +def test_resending_revoked_invite(app, client, user_session, portfolio, user): + task_order = TaskOrderFactory.create( + portfolio=portfolio, contracting_officer=user, ko_invite=True + ) + + portfolio_role = PortfolioRoleFactory.create(portfolio=portfolio, user=user) + + invite = InvitationFactory.create( + inviter=user, + portfolio_role=portfolio_role, + email=user.email, + status=InvitationStatus.REVOKED, + ) + + user_session(user) + + response = client.post( + url_for( + "portfolios.resend_invite", + portfolio_id=portfolio.id, + task_order_id=task_order.id, + invite_type="ko_invite", + _external=True, + ) + ) + + assert invite.is_revoked + assert response.status_code == 404 + + +def test_resending_expired_invite(app, client, user_session, portfolio, user): + queue_length = len(queue.get_queue()) + + task_order = TaskOrderFactory.create( + portfolio=portfolio, contracting_officer=user, ko_invite=True + ) + portfolio_role = PortfolioRoleFactory.create(portfolio=portfolio, user=user) + invite = InvitationFactory.create( + inviter=user, + portfolio_role=portfolio_role, + email=user.email, + expiration_time=datetime.now() - timedelta(days=1), + ) + user_session(user) + + response = client.post( + url_for( + "portfolios.resend_invite", + portfolio_id=portfolio.id, + task_order_id=task_order.id, + invite_type="ko_invite", + _external=True, + ) + ) + + assert invite.is_expired + assert response.status_code == 302 + assert len(queue.get_queue()) == queue_length + 1