atst/tests/routes/portfolios/test_invitations.py
graham-dds 108f65f928 Use pendulum for datetime operations when possible
Currently, we use both Python's built-in datetime library and Pendulum
to do datetime operations. For the sake of consistency, we should try to
stick to one library for datetimes. We could have used either, but
Pendulum has a more ergonomic API, so I decided to go with it when
possible.

The places where were we didn't / couldn't replace datetime are:
- checking instances of datetimes. Pendulum's objects are subclasses of
  python native datetime objects, so it's still useful to import
  datetime in those cases of using is_instance()
- WTForms date validators expect datetime style string formats --
  Pendulum has its own format for formatting/ parsing strings. As such,
  our custom validator DateRange needs to use datetime.stptime() to
  account for this format.
2020-02-17 10:38:52 -05:00

297 lines
9.7 KiB
Python

import pendulum
from unittest.mock import Mock
from flask import url_for
from atst.domain.portfolios import Portfolios
from atst.models import InvitationStatus, PortfolioRoleStatus
from atst.domain.permission_sets import PermissionSets
from tests.factories import *
def test_existing_member_accepts_valid_invite(client, user_session):
portfolio = PortfolioFactory.create()
user = UserFactory.create()
role = PortfolioRoleFactory.create(
portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING
)
invite = PortfolioInvitationFactory.create(dod_id=user.dod_id, role=role)
# the user does not have access to the portfolio before accepting the invite
assert len(Portfolios.for_user(user)) == 0
user_session(user)
response = client.get(
url_for("portfolios.accept_invitation", portfolio_token=invite.token)
)
# user is redirected to the portfolio view
assert response.status_code == 302
assert (
url_for("applications.portfolio_applications", portfolio_id=invite.portfolio.id)
in response.headers["Location"]
)
# the one-time use invite is no longer usable
assert invite.is_accepted
# the user has access to the portfolio
assert len(Portfolios.for_user(user)) == 1
def test_new_member_accepts_valid_invite(monkeypatch, client, user_session):
portfolio = PortfolioFactory.create()
user_info = UserFactory.dictionary()
role = PortfolioRoleFactory.create(portfolio=portfolio)
invite = PortfolioInvitationFactory.create(role=role, dod_id=user_info["dod_id"])
monkeypatch.setattr(
"atst.domain.auth.should_redirect_to_user_profile", lambda *args: False
)
user_session(UserFactory.create(dod_id=user_info["dod_id"]))
response = client.get(
url_for("portfolios.accept_invitation", portfolio_token=invite.token)
)
# user is redirected to the portfolio view
assert response.status_code == 302
assert (
url_for("applications.portfolio_applications", portfolio_id=portfolio.id)
in response.headers["Location"]
)
# the user has access to the portfolio
assert role.user.dod_id == user_info["dod_id"]
assert len(role.user.portfolio_roles) == 1
def test_member_accepts_invalid_invite(client, user_session):
portfolio = PortfolioFactory.create()
user = UserFactory.create()
ws_role = PortfolioRoleFactory.create(
user=user, portfolio=portfolio, status=PortfolioRoleStatus.PENDING
)
invite = PortfolioInvitationFactory.create(
user_id=user.id, role=ws_role, status=InvitationStatus.REJECTED_WRONG_USER
)
user_session(user)
response = client.get(
url_for("portfolios.accept_invitation", portfolio_token=invite.token)
)
assert response.status_code == 404
def test_user_who_has_not_accepted_portfolio_invite_cannot_view(client, user_session):
user = UserFactory.create()
portfolio = PortfolioFactory.create()
# create user in portfolio with invitation
user_session(portfolio.owner)
response = client.post(
url_for("portfolios.invite_member", portfolio_id=portfolio.id),
data=user.to_dictionary(),
)
# user tries to view portfolio before accepting invitation
user_session(user)
response = client.get("/portfolios/{}/applications".format(portfolio.id))
assert response.status_code == 404
def test_user_accepts_invite_with_wrong_dod_id(client, user_session):
portfolio = PortfolioFactory.create()
user = UserFactory.create()
different_user = UserFactory.create()
ws_role = PortfolioRoleFactory.create(
user=user, portfolio=portfolio, status=PortfolioRoleStatus.PENDING
)
invite = PortfolioInvitationFactory.create(user_id=user.id, role=ws_role)
user_session(different_user)
response = client.get(
url_for("portfolios.accept_invitation", portfolio_token=invite.token)
)
assert response.status_code == 404
def test_user_accepts_expired_invite(client, user_session):
portfolio = PortfolioFactory.create()
user = UserFactory.create()
ws_role = PortfolioRoleFactory.create(
user=user, portfolio=portfolio, status=PortfolioRoleStatus.PENDING
)
invite = PortfolioInvitationFactory.create(
user_id=user.id,
role=ws_role,
status=InvitationStatus.REJECTED_EXPIRED,
expiration_time=pendulum.now(tz="utc").subtract(seconds=1),
)
user_session(user)
response = client.get(
url_for("portfolios.accept_invitation", portfolio_token=invite.token)
)
assert response.status_code == 404
def test_revoke_invitation(client, user_session):
portfolio = PortfolioFactory.create()
user = UserFactory.create()
ws_role = PortfolioRoleFactory.create(
user=user, portfolio=portfolio, status=PortfolioRoleStatus.PENDING
)
invite = PortfolioInvitationFactory.create(
user_id=user.id,
role=ws_role,
status=InvitationStatus.REJECTED_EXPIRED,
expiration_time=pendulum.now(tz="utc").subtract(seconds=1),
)
user_session(portfolio.owner)
response = client.post(
url_for(
"portfolios.revoke_invitation",
portfolio_id=portfolio.id,
portfolio_token=invite.token,
)
)
assert response.status_code == 302
assert invite.is_revoked
def test_user_can_only_revoke_invites_in_their_portfolio(client, user_session):
portfolio = PortfolioFactory.create()
other_portfolio = PortfolioFactory.create()
user = UserFactory.create()
portfolio_role = PortfolioRoleFactory.create(
user=user, portfolio=other_portfolio, status=PortfolioRoleStatus.PENDING
)
invite = PortfolioInvitationFactory.create(
user_id=user.id,
role=portfolio_role,
status=InvitationStatus.REJECTED_EXPIRED,
expiration_time=pendulum.now(tz="utc").subtract(seconds=1),
)
user_session(portfolio.owner)
response = client.post(
url_for(
"portfolios.revoke_invitation",
portfolio_id=portfolio.id,
portfolio_token=invite.token,
)
)
assert response.status_code == 404
assert not invite.is_revoked
def test_user_can_only_resend_invites_in_their_portfolio(
monkeypatch, client, user_session
):
job_mock = Mock()
monkeypatch.setattr("atst.jobs.send_mail.delay", job_mock)
portfolio = PortfolioFactory.create()
other_portfolio = PortfolioFactory.create()
user = UserFactory.create()
portfolio_role = PortfolioRoleFactory.create(
user=user, portfolio=other_portfolio, status=PortfolioRoleStatus.PENDING
)
invite = PortfolioInvitationFactory.create(
user_id=user.id,
role=portfolio_role,
status=InvitationStatus.REJECTED_EXPIRED,
expiration_time=pendulum.now(tz="utc").subtract(seconds=1),
)
user_session(portfolio.owner)
response = client.post(
url_for(
"portfolios.resend_invitation",
portfolio_id=portfolio.id,
portfolio_token=invite.token,
)
)
assert response.status_code == 404
assert not job_mock.called
def test_resend_invitation_sends_email(monkeypatch, client, user_session):
job_mock = Mock()
monkeypatch.setattr("atst.jobs.send_mail.delay", job_mock)
user = UserFactory.create()
portfolio = PortfolioFactory.create()
portfolio_role = PortfolioRoleFactory.create(
user=user, portfolio=portfolio, status=PortfolioRoleStatus.PENDING
)
invite = PortfolioInvitationFactory.create(
user_id=user.id, role=portfolio_role, status=InvitationStatus.PENDING
)
user_session(portfolio.owner)
client.post(
url_for(
"portfolios.resend_invitation",
portfolio_id=portfolio.id,
portfolio_token=invite.token,
),
data={
"user_data-dod_id": user.dod_id,
"user_data-first_name": user.first_name,
"user_data-last_name": user.last_name,
"user_data-email": user.email,
},
)
assert job_mock.called
_DEFAULT_PERMS_FORM_DATA = {
"permission_sets-perms_app_mgmt": "n",
"permission_sets-perms_funding": "n",
"permission_sets-perms_reporting": "n",
"permission_sets-perms_portfolio_mgmt": "n",
}
def test_user_with_permission_has_add_member_link(client, user_session):
portfolio = PortfolioFactory.create()
user_session(portfolio.owner)
response = client.get(url_for("portfolios.admin", portfolio_id=portfolio.id))
assert response.status_code == 200
assert (
url_for("portfolios.invite_member", portfolio_id=portfolio.id).encode()
in response.data
)
def test_invite_member(monkeypatch, client, user_session, session):
job_mock = Mock()
monkeypatch.setattr("atst.jobs.send_mail.delay", job_mock)
user_data = UserFactory.dictionary()
portfolio = PortfolioFactory.create()
user_session(portfolio.owner)
response = client.post(
url_for("portfolios.invite_member", portfolio_id=portfolio.id),
data={
"user_data-dod_id": user_data.get("dod_id"),
"user_data-first_name": user_data.get("first_name"),
"user_data-last_name": user_data.get("last_name"),
"user_data-email": user_data.get("email"),
**_DEFAULT_PERMS_FORM_DATA,
},
follow_redirects=True,
)
assert response.status_code == 200
full_name = "{} {}".format(user_data.get("first_name"), user_data.get("last_name"))
assert full_name in response.data.decode()
invitation = (
session.query(PortfolioInvitation)
.filter_by(dod_id=user_data.get("dod_id"))
.one()
)
assert invitation.role.portfolio == portfolio
assert job_mock.called
assert len(invitation.role.permission_sets) == 5