diff --git a/atst/domain/authz/decorator.py b/atst/domain/authz/decorator.py index 8c02718a..5e0b876e 100644 --- a/atst/domain/authz/decorator.py +++ b/atst/domain/authz/decorator.py @@ -6,8 +6,8 @@ from . import user_can_access from atst.domain.portfolios import Portfolios from atst.domain.task_orders import TaskOrders from atst.domain.applications import Applications -from atst.domain.invitations import Invitations from atst.domain.environments import Environments +from atst.domain.invitations import PortfolioInvitations from atst.domain.exceptions import UnauthorizedError @@ -24,8 +24,8 @@ def check_access(permission, message, override, *args, **kwargs): access_args["portfolio"] = task_order.portfolio elif "token" in kwargs: - invite = Invitations._get(kwargs["token"]) - access_args["portfolio"] = invite.portfolio_role.portfolio + invite = PortfolioInvitations._get(kwargs["token"]) + access_args["portfolio"] = invite.role.portfolio elif "portfolio_id" in kwargs: access_args["portfolio"] = Portfolios.get( diff --git a/atst/domain/invitations.py b/atst/domain/invitations.py index c4d51248..cfedde11 100644 --- a/atst/domain/invitations.py +++ b/atst/domain/invitations.py @@ -2,7 +2,7 @@ import datetime from sqlalchemy.orm.exc import NoResultFound from atst.database import db -from atst.models.invitation import Invitation, Status as InvitationStatus +from atst.models import InvitationStatus, PortfolioInvitation from atst.domain.portfolio_roles import PortfolioRoles from .exceptions import NotFoundError @@ -38,27 +38,29 @@ class InvitationError(Exception): return "{} has a status of {}".format(self.invite.id, self.invite.status.value) -class Invitations(object): +class BaseInvitations(object): + model = None # number of minutes a given invitation is considered valid EXPIRATION_LIMIT_MINUTES = 360 @classmethod def _get(cls, token): try: - invite = db.session.query(Invitation).filter_by(token=token).one() + invite = db.session.query(cls.model).filter_by(token=token).one() except NoResultFound: raise NotFoundError("invite") return invite @classmethod - def create(cls, inviter, portfolio_role, email): - invite = Invitation( - portfolio_role=portfolio_role, + def create(cls, inviter, role, email): + # pylint: disable=not-callable + invite = cls.model( + role=role, inviter=inviter, - user=portfolio_role.user, + user=role.user, status=InvitationStatus.PENDING, - expiration_time=Invitations.current_expiration_time(), + expiration_time=cls.current_expiration_time(), email=email, ) db.session.add(invite) @@ -68,29 +70,29 @@ class Invitations(object): @classmethod def accept(cls, user, token): - invite = Invitations._get(token) + invite = cls._get(token) if invite.user.dod_id != user.dod_id: if invite.is_pending: - Invitations._update_status(invite, InvitationStatus.REJECTED_WRONG_USER) + cls._update_status(invite, InvitationStatus.REJECTED_WRONG_USER) raise WrongUserError(user, invite) elif invite.is_expired: - Invitations._update_status(invite, InvitationStatus.REJECTED_EXPIRED) + cls._update_status(invite, InvitationStatus.REJECTED_EXPIRED) raise ExpiredError(invite) elif invite.is_accepted or invite.is_revoked or invite.is_rejected: raise InvitationError(invite) elif invite.is_pending: # pragma: no branch - Invitations._update_status(invite, InvitationStatus.ACCEPTED) - PortfolioRoles.enable(invite.portfolio_role) + cls._update_status(invite, InvitationStatus.ACCEPTED) + PortfolioRoles.enable(invite.role) return invite @classmethod def current_expiration_time(cls): return datetime.datetime.now() + datetime.timedelta( - minutes=Invitations.EXPIRATION_LIMIT_MINUTES + minutes=cls.EXPIRATION_LIMIT_MINUTES ) @classmethod @@ -103,23 +105,25 @@ class Invitations(object): @classmethod def revoke(cls, token): - invite = Invitations._get(token) - return Invitations._update_status(invite, InvitationStatus.REVOKED) + invite = cls._get(token) + return cls._update_status(invite, InvitationStatus.REVOKED) @classmethod def lookup_by_portfolio_and_user(cls, portfolio, user): - portfolio_role = PortfolioRoles.get(portfolio.id, user.id) + role = PortfolioRoles.get(portfolio.id, user.id) - if portfolio_role.latest_invitation is None: + if role.latest_invitation is None: raise NotFoundError("invitation") - return portfolio_role.latest_invitation + return role.latest_invitation @classmethod def resend(cls, user, token): - previous_invitation = Invitations._get(token) - Invitations._update_status(previous_invitation, InvitationStatus.REVOKED) + previous_invitation = cls._get(token) + cls._update_status(previous_invitation, InvitationStatus.REVOKED) - return Invitations.create( - user, previous_invitation.portfolio_role, previous_invitation.email - ) + return cls.create(user, previous_invitation.role, previous_invitation.email) + + +class PortfolioInvitations(BaseInvitations): + model = PortfolioInvitation diff --git a/atst/models/__init__.py b/atst/models/__init__.py index 85130ceb..0369bc07 100644 --- a/atst/models/__init__.py +++ b/atst/models/__init__.py @@ -5,14 +5,16 @@ Base = declarative_base() from .permissions import Permissions from .permission_set import PermissionSet from .user import User -from .portfolio_role import PortfolioRole -from .application_role import ApplicationRole +from .portfolio_role import PortfolioRole, Status as PortfolioRoleStatus +from .application_role import ApplicationRole, Status as ApplicationRoleStatus from .environment_role import EnvironmentRole from .portfolio import Portfolio from .application import Application from .environment import Environment from .attachment import Attachment from .audit_event import AuditEvent -from .invitation import Invitation +from .portfolio_invitation import PortfolioInvitation from .task_order import TaskOrder from .dd_254 import DD254 + +from .mixins.invites import Status as InvitationStatus diff --git a/atst/models/mixins/__init__.py b/atst/models/mixins/__init__.py index 54c85d61..dd6d0faf 100644 --- a/atst/models/mixins/__init__.py +++ b/atst/models/mixins/__init__.py @@ -2,3 +2,4 @@ from .timestamps import TimestampsMixin from .auditable import AuditableMixin from .permissions import PermissionsMixin from .deletable import DeletableMixin +from .invites import InvitesMixin diff --git a/atst/models/invitation.py b/atst/models/mixins/invites.py similarity index 55% rename from atst/models/invitation.py rename to atst/models/mixins/invites.py index 62628d0a..39bedc08 100644 --- a/atst/models/invitation.py +++ b/atst/models/mixins/invites.py @@ -3,12 +3,11 @@ from enum import Enum import secrets from sqlalchemy import Column, ForeignKey, Enum as SQLAEnum, TIMESTAMP, String +from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.dialects.postgresql import UUID -from sqlalchemy.orm import relationship, backref +from sqlalchemy.orm import relationship -from atst.models import Base, types -from atst.models.mixins.timestamps import TimestampsMixin -from atst.models.mixins.auditable import AuditableMixin +from atst.models import types class Status(Enum): @@ -19,24 +18,24 @@ class Status(Enum): REJECTED_EXPIRED = "rejected_expired" -class Invitation(Base, TimestampsMixin, AuditableMixin): - __tablename__ = "invitations" - +class InvitesMixin(object): id = types.Id() - user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), index=True) - user = relationship("User", backref="invitations", foreign_keys=[user_id]) + @declared_attr + def user_id(cls): + return Column(UUID(as_uuid=True), ForeignKey("users.id"), index=True) - portfolio_role_id = Column( - UUID(as_uuid=True), ForeignKey("portfolio_roles.id"), index=True - ) - portfolio_role = relationship( - "PortfolioRole", - backref=backref("invitations", order_by="Invitation.time_created"), - ) + @declared_attr + def user(cls): + return relationship("User", foreign_keys=[cls.user_id]) - inviter_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), index=True) - inviter = relationship("User", backref="sent_invites", foreign_keys=[inviter_id]) + @declared_attr + def inviter_id(cls): + return Column(UUID(as_uuid=True), ForeignKey("users.id"), index=True) + + @declared_attr + def inviter(cls): + return relationship("User", foreign_keys=[cls.inviter_id]) status = Column(SQLAEnum(Status, native_enum=False, default=Status.PENDING)) @@ -47,8 +46,9 @@ class Invitation(Base, TimestampsMixin, AuditableMixin): email = Column(String, nullable=False) def __repr__(self): - return "".format( - self.user_id, self.portfolio_role_id, self.id, self.email + role_id = self.role.id if self.role else None + return "<{}(user='{}', role='{}', id='{}', email='{}')>".format( + self.__class__.__name__, self.user_id, role_id, self.id, self.email ) @property @@ -90,14 +90,9 @@ class Invitation(Base, TimestampsMixin, AuditableMixin): Status.REVOKED, ] - @property - def portfolio(self): - if self.portfolio_role: # pragma: no branch - return self.portfolio_role.portfolio - @property def user_name(self): - return self.portfolio_role.user.full_name + return self.role.user.full_name @property def is_revokable(self): @@ -110,21 +105,3 @@ class Invitation(Base, TimestampsMixin, AuditableMixin): @property def user_dod_id(self): return self.user.dod_id if self.user is not None else None - - @property - def event_details(self): - return {"email": self.email, "dod_id": self.user_dod_id} - - @property - def history(self): - changes = self.get_changes() - change_set = {} - - if "status" in changes: - change_set["status"] = [s.name for s in changes["status"]] - - return change_set - - @property - def portfolio_id(self): - return self.portfolio_role.portfolio_id diff --git a/atst/models/portfolio_invitation.py b/atst/models/portfolio_invitation.py new file mode 100644 index 00000000..916e63ae --- /dev/null +++ b/atst/models/portfolio_invitation.py @@ -0,0 +1,41 @@ +from sqlalchemy import Column, ForeignKey +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import relationship, backref + +from atst.models import Base +from atst.models.mixins import TimestampsMixin, AuditableMixin, InvitesMixin + + +class PortfolioInvitation(Base, TimestampsMixin, AuditableMixin, InvitesMixin): + __tablename__ = "invitations" + + portfolio_role_id = Column( + UUID(as_uuid=True), ForeignKey("portfolio_roles.id"), index=True + ) + role = relationship( + "PortfolioRole", + backref=backref("invitations", order_by="PortfolioInvitation.time_created"), + ) + + @property + def portfolio(self): + if self.role: # pragma: no branch + return self.role.portfolio + + @property + def portfolio_id(self): + return self.role.portfolio_id + + @property + def event_details(self): + return {"email": self.email, "dod_id": self.user_dod_id} + + @property + def history(self): + changes = self.get_changes() + change_set = {} + + if "status" in changes: + change_set["status"] = [s.name for s in changes["status"]] + + return change_set diff --git a/atst/models/user.py b/atst/models/user.py index 83f34326..0510d867 100644 --- a/atst/models/user.py +++ b/atst/models/user.py @@ -4,6 +4,7 @@ from sqlalchemy.dialects.postgresql import UUID from atst.models import Base, types, mixins from atst.models.permissions import Permissions +from atst.models.portfolio_invitation import PortfolioInvitation users_permission_sets = Table( @@ -31,6 +32,13 @@ class User( primaryjoin="and_(ApplicationRole.user_id==User.id, ApplicationRole.deleted==False)", ) + portfolio_invitations = relationship( + "PortfolioInvitation", foreign_keys=PortfolioInvitation.user_id + ) + sent_portfolio_invitations = relationship( + "PortfolioInvitation", foreign_keys=PortfolioInvitation.inviter_id + ) + email = Column(String) dod_id = Column(String, unique=True, nullable=False) first_name = Column(String) diff --git a/atst/routes/portfolios/invitations.py b/atst/routes/portfolios/invitations.py index e7fd6f92..a34fcee8 100644 --- a/atst/routes/portfolios/invitations.py +++ b/atst/routes/portfolios/invitations.py @@ -1,7 +1,7 @@ from flask import g, redirect, url_for, render_template from . import portfolios_bp -from atst.domain.invitations import Invitations +from atst.domain.invitations import PortfolioInvitations from atst.queue import queue from atst.utils.flash import formatted_flash as flash from atst.domain.authz.decorator import user_can_access_decorator as user_can @@ -19,7 +19,7 @@ def send_invite_email(owner_name, token, new_member_email): @portfolios_bp.route("/portfolios/invitations/", methods=["GET"]) def accept_invitation(token): - invite = Invitations.accept(g.current_user, token) + invite = PortfolioInvitations.accept(g.current_user, token) for task_order in invite.portfolio.task_orders: if g.current_user in task_order.officers: @@ -37,7 +37,7 @@ def accept_invitation(token): ) @user_can(Permissions.EDIT_PORTFOLIO_USERS, message="revoke invitation") def revoke_invitation(portfolio_id, token): - Invitations.revoke(token) + PortfolioInvitations.revoke(token) return redirect( url_for( @@ -54,7 +54,7 @@ def revoke_invitation(portfolio_id, token): ) @user_can(Permissions.EDIT_PORTFOLIO_USERS, message="resend invitation") def resend_invitation(portfolio_id, token): - invite = Invitations.resend(g.current_user, token) + invite = PortfolioInvitations.resend(g.current_user, token) send_invite_email(g.current_user.full_name, invite.token, invite.email) flash("resend_portfolio_invitation", user_name=invite.user_name) return redirect( diff --git a/atst/routes/task_orders/invitations.py b/atst/routes/task_orders/invitations.py index 024c0097..b89d6be2 100644 --- a/atst/routes/task_orders/invitations.py +++ b/atst/routes/task_orders/invitations.py @@ -7,7 +7,7 @@ from atst.domain.authz.decorator import user_can_access_decorator as user_can from atst.models.permissions import Permissions from atst.database import db from atst.domain.exceptions import NotFoundError, NoAccessError -from atst.domain.invitations import Invitations +from atst.domain.invitations import PortfolioInvitations from atst.domain.portfolios import Portfolios from atst.utils.localization import translate from atst.forms.officers import EditTaskOrderOfficersForm @@ -57,7 +57,7 @@ def resend_invite(task_order_id): if not officer: raise NotFoundError("officer") - invitation = Invitations.lookup_by_portfolio_and_user(portfolio, officer) + invitation = PortfolioInvitations.lookup_by_portfolio_and_user(portfolio, officer) if not invitation: raise NotFoundError("invitation") @@ -65,11 +65,11 @@ def resend_invite(task_order_id): if not invitation.can_resend: raise NoAccessError("invitation") - Invitations.revoke(token=invitation.token) + PortfolioInvitations.revoke(token=invitation.token) invite_service = InvitationService( g.current_user, - invitation.portfolio_role, + invitation.role, invitation.email, subject=invite_type_info["subject"], email_template=invite_type_info["template"], diff --git a/atst/services/invitation.py b/atst/services/invitation.py index b7c3c217..9d478776 100644 --- a/atst/services/invitation.py +++ b/atst/services/invitation.py @@ -1,6 +1,6 @@ from flask import render_template -from atst.domain.invitations import Invitations +from atst.domain.invitations import PortfolioInvitations from atst.queue import queue from atst.domain.task_orders import TaskOrders from atst.domain.portfolio_roles import PortfolioRoles @@ -68,7 +68,7 @@ class Invitation: return invite def _create_invite(self): - return Invitations.create(self.inviter, self.member, self.email) + return PortfolioInvitations.create(self.inviter, self.member, self.email) def _send_invite_email(self, token): body = render_template( diff --git a/tests/domain/test_invitations.py b/tests/domain/test_invitations.py index ef08c879..d914ba09 100644 --- a/tests/domain/test_invitations.py +++ b/tests/domain/test_invitations.py @@ -3,33 +3,32 @@ import pytest import re from atst.domain.invitations import ( - Invitations, + PortfolioInvitations, InvitationError, WrongUserError, ExpiredError, NotFoundError, ) -from atst.models.invitation import Status +from atst.domain.audit_log import AuditLog +from atst.models import InvitationStatus from tests.factories import ( PortfolioFactory, PortfolioRoleFactory, UserFactory, - InvitationFactory, + PortfolioInvitationFactory, ) -from atst.domain.audit_log import AuditLog - def test_create_invitation(): portfolio = PortfolioFactory.create() user = UserFactory.create() ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio) - invite = Invitations.create(portfolio.owner, ws_role, user.email) + invite = PortfolioInvitations.create(portfolio.owner, ws_role, user.email) assert invite.user == user - assert invite.portfolio_role == ws_role + assert invite.role == ws_role assert invite.inviter == portfolio.owner - assert invite.status == Status.PENDING + assert invite.status == InvitationStatus.PENDING assert re.match(r"^[\w\-_]+$", invite.token) @@ -37,9 +36,9 @@ def test_accept_invitation(): portfolio = PortfolioFactory.create() user = UserFactory.create() ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio) - invite = Invitations.create(portfolio.owner, ws_role, user.email) + invite = PortfolioInvitations.create(portfolio.owner, ws_role, user.email) assert invite.is_pending - accepted_invite = Invitations.accept(user, invite.token) + accepted_invite = PortfolioInvitations.accept(user, invite.token) assert accepted_invite.is_accepted @@ -47,16 +46,16 @@ def test_accept_expired_invitation(): user = UserFactory.create() portfolio = PortfolioFactory.create() ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio) - increment = Invitations.EXPIRATION_LIMIT_MINUTES + 1 + increment = PortfolioInvitations.EXPIRATION_LIMIT_MINUTES + 1 expiration_time = datetime.datetime.now() - datetime.timedelta(minutes=increment) - invite = InvitationFactory.create( + invite = PortfolioInvitationFactory.create( user=user, expiration_time=expiration_time, - status=Status.PENDING, - portfolio_role=ws_role, + status=InvitationStatus.PENDING, + role=ws_role, ) with pytest.raises(ExpiredError): - Invitations.accept(user, invite.token) + PortfolioInvitations.accept(user, invite.token) assert invite.is_rejected @@ -65,22 +64,22 @@ def test_accept_rejected_invite(): user = UserFactory.create() portfolio = PortfolioFactory.create() ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio) - invite = InvitationFactory.create( - user=user, status=Status.REJECTED_EXPIRED, portfolio_role=ws_role + invite = PortfolioInvitationFactory.create( + user=user, status=InvitationStatus.REJECTED_EXPIRED, role=ws_role ) with pytest.raises(InvitationError): - Invitations.accept(user, invite.token) + PortfolioInvitations.accept(user, invite.token) def test_accept_revoked_invite(): user = UserFactory.create() portfolio = PortfolioFactory.create() ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio) - invite = InvitationFactory.create( - user=user, status=Status.REVOKED, portfolio_role=ws_role + invite = PortfolioInvitationFactory.create( + user=user, status=InvitationStatus.REVOKED, role=ws_role ) with pytest.raises(InvitationError): - Invitations.accept(user, invite.token) + PortfolioInvitations.accept(user, invite.token) def test_wrong_user_accepts_invitation(): @@ -88,9 +87,9 @@ def test_wrong_user_accepts_invitation(): portfolio = PortfolioFactory.create() ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio) wrong_user = UserFactory.create() - invite = InvitationFactory.create(user=user, portfolio_role=ws_role) + invite = PortfolioInvitationFactory.create(user=user, role=ws_role) with pytest.raises(WrongUserError): - Invitations.accept(wrong_user, invite.token) + PortfolioInvitations.accept(wrong_user, invite.token) def test_user_cannot_accept_invitation_accepted_by_wrong_user(): @@ -98,30 +97,30 @@ def test_user_cannot_accept_invitation_accepted_by_wrong_user(): portfolio = PortfolioFactory.create() ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio) wrong_user = UserFactory.create() - invite = InvitationFactory.create(user=user, portfolio_role=ws_role) + invite = PortfolioInvitationFactory.create(user=user, role=ws_role) with pytest.raises(WrongUserError): - Invitations.accept(wrong_user, invite.token) + PortfolioInvitations.accept(wrong_user, invite.token) with pytest.raises(InvitationError): - Invitations.accept(user, invite.token) + PortfolioInvitations.accept(user, invite.token) def test_accept_invitation_twice(): portfolio = PortfolioFactory.create() user = UserFactory.create() ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio) - invite = Invitations.create(portfolio.owner, ws_role, user.email) - Invitations.accept(user, invite.token) + invite = PortfolioInvitations.create(portfolio.owner, ws_role, user.email) + PortfolioInvitations.accept(user, invite.token) with pytest.raises(InvitationError): - Invitations.accept(user, invite.token) + PortfolioInvitations.accept(user, invite.token) def test_revoke_invitation(): portfolio = PortfolioFactory.create() user = UserFactory.create() ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio) - invite = Invitations.create(portfolio.owner, ws_role, user.email) + invite = PortfolioInvitations.create(portfolio.owner, ws_role, user.email) assert invite.is_pending - Invitations.revoke(invite.token) + PortfolioInvitations.revoke(invite.token) assert invite.is_revoked @@ -129,8 +128,8 @@ def test_resend_invitation(): portfolio = PortfolioFactory.create() user = UserFactory.create() ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio) - invite = Invitations.create(portfolio.owner, ws_role, user.email) - Invitations.resend(user, invite.token) + invite = PortfolioInvitations.create(portfolio.owner, ws_role, user.email) + PortfolioInvitations.resend(user, invite.token) assert ws_role.invitations[0].is_revoked assert ws_role.invitations[1].is_pending @@ -139,8 +138,8 @@ def test_audit_event_for_accepted_invite(): portfolio = PortfolioFactory.create() user = UserFactory.create() ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio) - invite = Invitations.create(portfolio.owner, ws_role, user.email) - invite = Invitations.accept(user, invite.token) + invite = PortfolioInvitations.create(portfolio.owner, ws_role, user.email) + invite = PortfolioInvitations.accept(user, invite.token) accepted_event = AuditLog.get_by_resource(invite.id)[0] assert "email" in accepted_event.event_details @@ -151,9 +150,11 @@ def test_lookup_by_user_and_portfolio(): portfolio = PortfolioFactory.create() user = UserFactory.create() ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio) - invite = Invitations.create(portfolio.owner, ws_role, user.email) + invite = PortfolioInvitations.create(portfolio.owner, ws_role, user.email) - assert Invitations.lookup_by_portfolio_and_user(portfolio, user) == invite + assert PortfolioInvitations.lookup_by_portfolio_and_user(portfolio, user) == invite with pytest.raises(NotFoundError): - Invitations.lookup_by_portfolio_and_user(portfolio, UserFactory.create()) + PortfolioInvitations.lookup_by_portfolio_and_user( + portfolio, UserFactory.create() + ) diff --git a/tests/domain/test_portfolio_roles.py b/tests/domain/test_portfolio_roles.py index 312e12ca..d98eff1b 100644 --- a/tests/domain/test_portfolio_roles.py +++ b/tests/domain/test_portfolio_roles.py @@ -7,7 +7,7 @@ from atst.models.portfolio_role import Status as PortfolioRoleStatus from tests.factories import ( PortfolioFactory, UserFactory, - InvitationFactory, + PortfolioInvitationFactory, PortfolioRoleFactory, ) diff --git a/tests/factories.py b/tests/factories.py index a9ac984e..89999c63 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -7,11 +7,8 @@ import datetime from atst.forms import data from atst.models import * -from atst.models.portfolio_role import Status as PortfolioRoleStatus -from atst.models.application_role import Status as ApplicationRoleStatus -from atst.models.invitation import Status as InvitationStatus -from atst.models.environment_role import CSPRole -from atst.domain.invitations import Invitations + +from atst.domain.invitations import PortfolioInvitations from atst.domain.permission_sets import PermissionSets from atst.domain.portfolio_roles import PortfolioRoles @@ -240,13 +237,13 @@ class EnvironmentRoleFactory(Base): user = factory.SubFactory(UserFactory) -class InvitationFactory(Base): +class PortfolioInvitationFactory(Base): class Meta: - model = Invitation + model = PortfolioInvitation email = factory.Faker("email") status = InvitationStatus.PENDING - expiration_time = Invitations.current_expiration_time() + expiration_time = PortfolioInvitations.current_expiration_time() class AttachmentFactory(Base): diff --git a/tests/models/test_invitation.py b/tests/models/test_portfolio_invitations.py similarity index 72% rename from tests/models/test_invitation.py rename to tests/models/test_portfolio_invitations.py index 0ecfff59..faf9365d 100644 --- a/tests/models/test_invitation.py +++ b/tests/models/test_portfolio_invitations.py @@ -1,11 +1,9 @@ -import pytest import datetime -from atst.models.invitation import Invitation, Status -from atst.models.portfolio_role import Status as PortfolioRoleStatus +from atst.models import InvitationStatus, PortfolioRoleStatus from tests.factories import ( - InvitationFactory, + PortfolioInvitationFactory, PortfolioFactory, UserFactory, PortfolioRoleFactory, @@ -18,9 +16,9 @@ def test_expired_invite_is_not_revokable(): portfolio_role = PortfolioRoleFactory.create( portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING ) - invite = InvitationFactory.create( + invite = PortfolioInvitationFactory.create( expiration_time=datetime.datetime.now() - datetime.timedelta(minutes=60), - portfolio_role=portfolio_role, + role=portfolio_role, ) assert not invite.is_revokable @@ -31,7 +29,7 @@ def test_unexpired_invite_is_revokable(): portfolio_role = PortfolioRoleFactory.create( portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING ) - invite = InvitationFactory.create(portfolio_role=portfolio_role) + invite = PortfolioInvitationFactory.create(role=portfolio_role) assert invite.is_revokable @@ -41,7 +39,7 @@ def test_invite_is_not_revokable_if_invite_is_not_pending(): portfolio_role = PortfolioRoleFactory.create( portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING ) - invite = InvitationFactory.create( - portfolio_role=portfolio_role, status=Status.ACCEPTED + invite = PortfolioInvitationFactory.create( + role=portfolio_role, status=InvitationStatus.ACCEPTED ) assert not invite.is_revokable diff --git a/tests/models/test_portfolio_role.py b/tests/models/test_portfolio_role.py index 110df0eb..6438d2b1 100644 --- a/tests/models/test_portfolio_role.py +++ b/tests/models/test_portfolio_role.py @@ -6,14 +6,11 @@ from atst.domain.portfolios import Portfolios from atst.domain.portfolio_roles import PortfolioRoles from atst.domain.applications import Applications from atst.domain.permission_sets import PermissionSets -from atst.models.portfolio_role import Status -from atst.models.invitation import Status as InvitationStatus -from atst.models.audit_event import AuditEvent -from atst.models.portfolio_role import Status as PortfolioRoleStatus -from atst.models.environment_role import CSPRole +from atst.models import AuditEvent, InvitationStatus, PortfolioRoleStatus, CSPRole + from tests.factories import ( UserFactory, - InvitationFactory, + PortfolioInvitationFactory, PortfolioRoleFactory, EnvironmentFactory, EnvironmentRoleFactory, @@ -189,12 +186,12 @@ def test_has_environment_roles(): def test_status_when_member_is_active(): - portfolio_role = PortfolioRoleFactory.create(status=Status.ACTIVE) + portfolio_role = PortfolioRoleFactory.create(status=PortfolioRoleStatus.ACTIVE) assert portfolio_role.display_status == "Active" def test_status_when_member_is_disabled(): - portfolio_role = PortfolioRoleFactory.create(status=Status.DISABLED) + portfolio_role = PortfolioRoleFactory.create(status=PortfolioRoleStatus.DISABLED) assert portfolio_role.display_status == "Disabled" @@ -204,8 +201,8 @@ def test_status_when_invitation_has_been_rejected_for_expirations(): portfolio_role = PortfolioRoleFactory.create( portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING ) - invitation = InvitationFactory.create( - portfolio_role=portfolio_role, status=InvitationStatus.REJECTED_EXPIRED + PortfolioInvitationFactory.create( + role=portfolio_role, status=InvitationStatus.REJECTED_EXPIRED ) assert portfolio_role.display_status == "Invite expired" @@ -216,8 +213,8 @@ def test_status_when_invitation_has_been_rejected_for_wrong_user(): portfolio_role = PortfolioRoleFactory.create( portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING ) - invitation = InvitationFactory.create( - portfolio_role=portfolio_role, status=InvitationStatus.REJECTED_WRONG_USER + PortfolioInvitationFactory.create( + role=portfolio_role, status=InvitationStatus.REJECTED_WRONG_USER ) assert portfolio_role.display_status == "Error on invite" @@ -228,8 +225,8 @@ def test_status_when_invitation_has_been_revoked(): portfolio_role = PortfolioRoleFactory.create( portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING ) - invitation = InvitationFactory.create( - portfolio_role=portfolio_role, status=InvitationStatus.REVOKED + PortfolioInvitationFactory.create( + role=portfolio_role, status=InvitationStatus.REVOKED ) assert portfolio_role.display_status == "Invite revoked" @@ -240,8 +237,8 @@ def test_status_when_invitation_is_expired(): portfolio_role = PortfolioRoleFactory.create( portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING ) - invitation = InvitationFactory.create( - portfolio_role=portfolio_role, + PortfolioInvitationFactory.create( + role=portfolio_role, status=InvitationStatus.PENDING, expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1), ) @@ -254,8 +251,8 @@ def test_can_not_resend_invitation_if_active(): portfolio_role = PortfolioRoleFactory.create( portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING ) - invitation = InvitationFactory.create( - portfolio_role=portfolio_role, status=InvitationStatus.ACCEPTED + PortfolioInvitationFactory.create( + role=portfolio_role, status=InvitationStatus.ACCEPTED ) assert not portfolio_role.can_resend_invitation @@ -266,8 +263,8 @@ def test_can_resend_invitation_if_expired(): portfolio_role = PortfolioRoleFactory.create( portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING ) - invitation = InvitationFactory.create( - portfolio_role=portfolio_role, status=InvitationStatus.REJECTED_EXPIRED + PortfolioInvitationFactory.create( + role=portfolio_role, status=InvitationStatus.REJECTED_EXPIRED ) assert portfolio_role.can_resend_invitation diff --git a/tests/routes/portfolios/test_invitations.py b/tests/routes/portfolios/test_invitations.py index eddae72e..78eb1e77 100644 --- a/tests/routes/portfolios/test_invitations.py +++ b/tests/routes/portfolios/test_invitations.py @@ -6,12 +6,11 @@ from tests.factories import ( UserFactory, PortfolioFactory, PortfolioRoleFactory, - InvitationFactory, + PortfolioInvitationFactory, TaskOrderFactory, ) from atst.domain.portfolios import Portfolios -from atst.models.portfolio_role import Status as PortfolioRoleStatus -from atst.models.invitation import Status as InvitationStatus +from atst.models import InvitationStatus, PortfolioRoleStatus from atst.domain.users import Users from atst.domain.permission_sets import PermissionSets @@ -22,7 +21,7 @@ def test_existing_member_accepts_valid_invite(client, user_session): ws_role = PortfolioRoleFactory.create( portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING ) - invite = InvitationFactory.create(user_id=user.id, portfolio_role=ws_role) + invite = PortfolioInvitationFactory.create(user_id=user.id, role=ws_role) # the user does not have access to the portfolio before accepting the invite assert len(Portfolios.for_user(user)) == 0 @@ -60,7 +59,7 @@ def test_new_member_accepts_valid_invite(monkeypatch, client, user_session): assert response.status_code == 302 user = Users.get_by_dod_id(user_info["dod_id"]) - token = user.invitations[0].token + token = user.portfolio_invitations[0].token monkeypatch.setattr( "atst.domain.auth.should_redirect_to_user_profile", lambda *args: False @@ -84,10 +83,8 @@ def test_member_accepts_invalid_invite(client, user_session): ws_role = PortfolioRoleFactory.create( user=user, portfolio=portfolio, status=PortfolioRoleStatus.PENDING ) - invite = InvitationFactory.create( - user_id=user.id, - portfolio_role=ws_role, - status=InvitationStatus.REJECTED_WRONG_USER, + 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", token=invite.token)) @@ -119,7 +116,7 @@ def test_user_accepts_invite_with_wrong_dod_id(client, user_session): ws_role = PortfolioRoleFactory.create( user=user, portfolio=portfolio, status=PortfolioRoleStatus.PENDING ) - invite = InvitationFactory.create(user_id=user.id, portfolio_role=ws_role) + invite = PortfolioInvitationFactory.create(user_id=user.id, role=ws_role) user_session(different_user) response = client.get(url_for("portfolios.accept_invitation", token=invite.token)) @@ -132,9 +129,9 @@ def test_user_accepts_expired_invite(client, user_session): ws_role = PortfolioRoleFactory.create( user=user, portfolio=portfolio, status=PortfolioRoleStatus.PENDING ) - invite = InvitationFactory.create( + invite = PortfolioInvitationFactory.create( user_id=user.id, - portfolio_role=ws_role, + role=ws_role, status=InvitationStatus.REJECTED_EXPIRED, expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1), ) @@ -150,9 +147,9 @@ def test_revoke_invitation(client, user_session): ws_role = PortfolioRoleFactory.create( user=user, portfolio=portfolio, status=PortfolioRoleStatus.PENDING ) - invite = InvitationFactory.create( + invite = PortfolioInvitationFactory.create( user_id=user.id, - portfolio_role=ws_role, + role=ws_role, status=InvitationStatus.REJECTED_EXPIRED, expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1), ) @@ -176,9 +173,9 @@ def test_user_can_only_revoke_invites_in_their_portfolio(client, user_session): portfolio_role = PortfolioRoleFactory.create( user=user, portfolio=other_portfolio, status=PortfolioRoleStatus.PENDING ) - invite = InvitationFactory.create( + invite = PortfolioInvitationFactory.create( user_id=user.id, - portfolio_role=portfolio_role, + role=portfolio_role, status=InvitationStatus.REJECTED_EXPIRED, expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1), ) @@ -202,9 +199,9 @@ def test_user_can_only_resend_invites_in_their_portfolio(client, user_session, q portfolio_role = PortfolioRoleFactory.create( user=user, portfolio=other_portfolio, status=PortfolioRoleStatus.PENDING ) - invite = InvitationFactory.create( + invite = PortfolioInvitationFactory.create( user_id=user.id, - portfolio_role=portfolio_role, + role=portfolio_role, status=InvitationStatus.REJECTED_EXPIRED, expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1), ) @@ -227,8 +224,8 @@ def test_resend_invitation_sends_email(client, user_session, queue): ws_role = PortfolioRoleFactory.create( user=user, portfolio=portfolio, status=PortfolioRoleStatus.PENDING ) - invite = InvitationFactory.create( - user_id=user.id, portfolio_role=ws_role, status=InvitationStatus.PENDING + invite = PortfolioInvitationFactory.create( + user_id=user.id, role=ws_role, status=InvitationStatus.PENDING ) user_session(portfolio.owner) client.post( @@ -250,9 +247,9 @@ def test_existing_member_invite_resent_to_email_submitted_in_form( ws_role = PortfolioRoleFactory.create( user=user, portfolio=portfolio, status=PortfolioRoleStatus.PENDING ) - invite = InvitationFactory.create( + invite = PortfolioInvitationFactory.create( user_id=user.id, - portfolio_role=ws_role, + role=ws_role, status=InvitationStatus.PENDING, email="example@example.com", ) @@ -290,7 +287,7 @@ def test_contracting_officer_accepts_invite(monkeypatch, client, user_session): # contracting officer accepts invitation user = Users.get_by_dod_id(user_info["dod_id"]) - token = user.invitations[0].token + token = user.portfolio_invitations[0].token monkeypatch.setattr( "atst.domain.auth.should_redirect_to_user_profile", lambda *args: False ) @@ -324,7 +321,7 @@ def test_cor_accepts_invite(monkeypatch, client, user_session): # contracting officer representative accepts invitation user = Users.get_by_dod_id(user_info["dod_id"]) - token = user.invitations[0].token + token = user.portfolio_invitations[0].token monkeypatch.setattr( "atst.domain.auth.should_redirect_to_user_profile", lambda *args: False ) @@ -358,7 +355,7 @@ def test_so_accepts_invite(monkeypatch, client, user_session): # security officer accepts invitation user = Users.get_by_dod_id(user_info["dod_id"]) - token = user.invitations[0].token + token = user.portfolio_invitations[0].token monkeypatch.setattr( "atst.domain.auth.should_redirect_to_user_profile", lambda *args: False ) diff --git a/tests/routes/portfolios/test_members.py b/tests/routes/portfolios/test_members.py index 818a62a5..b3f8c430 100644 --- a/tests/routes/portfolios/test_members.py +++ b/tests/routes/portfolios/test_members.py @@ -45,7 +45,7 @@ def test_create_member(client, user_session): assert response.status_code == 200 assert user.full_name in response.data.decode() assert user.has_portfolios - assert user.invitations + assert user.portfolio_invitations assert len(queue.get_queue()) == queue_length + 1 portfolio_role = user.portfolio_roles[0] assert len(portfolio_role.permission_sets) == 5 diff --git a/tests/routes/task_orders/test_invitations.py b/tests/routes/task_orders/test_invitations.py index 795fe6f1..83978d8c 100644 --- a/tests/routes/task_orders/test_invitations.py +++ b/tests/routes/task_orders/test_invitations.py @@ -4,7 +4,7 @@ from flask import url_for import pytest from atst.domain.task_orders import TaskOrders -from atst.models.invitation import Status as InvitationStatus +from atst.models import InvitationStatus from atst.models.portfolio_role import Status as PortfolioStatus from atst.queue import queue @@ -13,7 +13,7 @@ from tests.factories import ( TaskOrderFactory, UserFactory, PortfolioRoleFactory, - InvitationFactory, + PortfolioInvitationFactory, ) @@ -79,7 +79,7 @@ def test_does_not_resend_officer_invitation(client, user_session): user_session(user) for i in range(2): client.post(url_for("task_orders.invite", task_order_id=task_order.id)) - assert len(contracting_officer.invitations) == 1 + assert len(contracting_officer.portfolio_invitations) == 1 def test_does_not_invite_if_task_order_incomplete(client, user_session, queue): @@ -272,9 +272,9 @@ class TestTaskOrderInvitations: cor_invite=True, ) portfolio_role = PortfolioRoleFactory.create(portfolio=self.portfolio, user=cor) - invitation = InvitationFactory.create( + PortfolioInvitationFactory.create( inviter=self.portfolio.owner, - portfolio_role=portfolio_role, + role=portfolio_role, user=cor, status=InvitationStatus.PENDING, ) @@ -369,9 +369,9 @@ def test_resend_invite_when_not_pending(app, client, user_session, portfolio, us portfolio=portfolio, user=user, status=PortfolioStatus.ACTIVE ) - original_invitation = InvitationFactory.create( + original_invitation = PortfolioInvitationFactory.create( inviter=user, - portfolio_role=portfolio_role, + role=portfolio_role, email=user.email, status=InvitationStatus.ACCEPTED, ) @@ -397,9 +397,9 @@ def test_resending_revoked_invite(app, client, user_session, portfolio, user): portfolio_role = PortfolioRoleFactory.create(portfolio=portfolio, user=user) - invite = InvitationFactory.create( + invite = PortfolioInvitationFactory.create( inviter=user, - portfolio_role=portfolio_role, + role=portfolio_role, email=user.email, status=InvitationStatus.REVOKED, ) @@ -427,9 +427,9 @@ def test_resending_expired_invite(app, client, user_session, portfolio): portfolio=portfolio, contracting_officer=ko, ko_invite=True ) portfolio_role = PortfolioRoleFactory.create(portfolio=portfolio, user=ko) - invite = InvitationFactory.create( + invite = PortfolioInvitationFactory.create( inviter=portfolio.owner, - portfolio_role=portfolio_role, + role=portfolio_role, email=ko.email, expiration_time=datetime.now() - timedelta(days=1), ) diff --git a/tests/services/test_invitation.py b/tests/services/test_invitation.py index 2227b541..364cdbcc 100644 --- a/tests/services/test_invitation.py +++ b/tests/services/test_invitation.py @@ -10,5 +10,5 @@ def test_invite_member(queue): ws_member = PortfolioRoleFactory.create(user=new_member, portfolio=portfolio) invite_service = Invitation(inviter, ws_member, new_member.email) new_invitation = invite_service.invite() - assert new_invitation == new_member.invitations[0] + assert new_invitation == new_member.portfolio_invitations[0] assert len(queue.get_queue()) == 1 diff --git a/tests/test_access.py b/tests/test_access.py index 0b568c58..ded929e3 100644 --- a/tests/test_access.py +++ b/tests/test_access.py @@ -17,7 +17,7 @@ from tests.factories import ( ApplicationRoleFactory, EnvironmentFactory, EnvironmentRoleFactory, - InvitationFactory, + PortfolioInvitationFactory, PortfolioFactory, PortfolioRoleFactory, TaskOrderFactory, @@ -75,7 +75,9 @@ def test_all_protected_routes_have_access_control( monkeypatch.setattr("atst.domain.portfolios.Portfolios.get", lambda *a: None) monkeypatch.setattr("atst.domain.task_orders.TaskOrders.get", lambda *a: Mock()) monkeypatch.setattr("atst.domain.applications.Applications.get", lambda *a: Mock()) - monkeypatch.setattr("atst.domain.invitations.Invitations._get", lambda *a: Mock()) + monkeypatch.setattr( + "atst.domain.invitations.PortfolioInvitations._get", lambda *a: Mock() + ) monkeypatch.setattr( "atst.utils.context_processors.get_portfolio_from_context", lambda *a: None ) @@ -402,7 +404,7 @@ def test_portfolios_resend_invitation_access(post_url_assert_status): portfolio = PortfolioFactory.create(owner=owner) prr = PortfolioRoleFactory.create(user=invitee, portfolio=portfolio) - invite = InvitationFactory.create(user=UserFactory.create(), portfolio_role=prr) + invite = PortfolioInvitationFactory.create(user=UserFactory.create(), role=prr) url = url_for( "portfolios.resend_invitation", portfolio_id=portfolio.id, token=invite.token @@ -423,7 +425,7 @@ def test_task_orders_resend_invite_access(post_url_assert_status): portfolio = PortfolioFactory.create(owner=owner) task_order = TaskOrderFactory.create(portfolio=portfolio, contracting_officer=ko) prr = PortfolioRoleFactory.create(user=ko, portfolio=portfolio) - invite = InvitationFactory.create(user=UserFactory.create(), portfolio_role=prr) + PortfolioInvitationFactory.create(user=UserFactory.create(), role=prr) url = url_for( "task_orders.resend_invite", @@ -449,7 +451,7 @@ def test_portfolios_revoke_invitation_access(post_url_assert_status): prr = PortfolioRoleFactory.create( user=prt_member, portfolio=portfolio, status=PortfolioRoleStatus.ACTIVE ) - invite = InvitationFactory.create(user=prt_member, portfolio_role=prr) + invite = PortfolioInvitationFactory.create(user=prt_member, role=prr) url = url_for( "portfolios.revoke_invitation", portfolio_id=portfolio.id,