diff --git a/django/thunderstore/account/forms.py b/django/thunderstore/account/forms.py index 572a2934f..53901cb78 100644 --- a/django/thunderstore/account/forms.py +++ b/django/thunderstore/account/forms.py @@ -2,6 +2,7 @@ from django.db import transaction from thunderstore.account.models import ServiceAccount +from thunderstore.core.exceptions import PermissionValidationError from thunderstore.core.types import UserType from thunderstore.repository.models import Team @@ -18,7 +19,9 @@ def __init__(self, user: UserType, *args, **kwargs) -> None: def clean_team(self) -> Team: team = self.cleaned_data["team"] - team.ensure_can_create_service_account(self.user) + errors, _ = team.validate_can_create_service_account(self.user) + if errors: + raise forms.ValidationError(errors) return team @transaction.atomic @@ -42,7 +45,11 @@ def __init__(self, user: UserType, *args, **kwargs) -> None: def clean_service_account(self) -> ServiceAccount: service_account = self.cleaned_data["service_account"] - service_account.owner.ensure_can_delete_service_account(self.user) + errors, is_public = service_account.owner.validate_can_delete_service_account( + self.user + ) + if errors: + raise PermissionValidationError(errors, is_public=is_public) return service_account def save(self) -> None: @@ -61,7 +68,11 @@ def __init__(self, user: UserType, *args, **kwargs) -> None: def clean_service_account(self) -> ServiceAccount: service_account = self.cleaned_data["service_account"] - service_account.owner.ensure_can_edit_service_account(self.user) + errors, is_public = service_account.owner.validate_can_edit_service_account( + self.user + ) + if errors: + raise PermissionValidationError(errors, is_public=is_public) return service_account def save(self) -> ServiceAccount: diff --git a/django/thunderstore/api/cyberstorm/services/team.py b/django/thunderstore/api/cyberstorm/services/team.py index 4ea78033c..e34fc9b5f 100644 --- a/django/thunderstore/api/cyberstorm/services/team.py +++ b/django/thunderstore/api/cyberstorm/services/team.py @@ -12,8 +12,13 @@ def disband_team(user: UserType, team_name: str) -> None: teams = Team.objects.exclude(is_active=False) team = get_object_or_404(teams, name=team_name) - team.ensure_user_can_access(user) - team.ensure_user_can_disband(user) + validators = [team.validate_user_can_access, team.validate_user_can_disband] + + for validator in validators: + errors, is_public = validator(user) + if errors: + raise PermissionValidationError(errors, is_public=is_public) + team.delete() @@ -35,8 +40,15 @@ def create_team(user: UserType, team_name: str) -> Team: @transaction.atomic def update_team(agent: UserType, team: Team, donation_link: str) -> Team: - team.ensure_user_can_access(agent) - team.ensure_user_can_edit_info(agent) + validators = [ + team.validate_user_can_access, + team.validate_user_can_edit_info, + ] + + for validator in validators: + errors, is_public = validator(agent) + if errors: + raise PermissionValidationError(errors, is_public=is_public) team.donation_link = donation_link team.save() diff --git a/django/thunderstore/permissions/utils.py b/django/thunderstore/permissions/utils.py index 1f09bd6aa..a4b95cff9 100644 --- a/django/thunderstore/permissions/utils.py +++ b/django/thunderstore/permissions/utils.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import List, Optional, Tuple from thunderstore.core.exceptions import PermissionValidationError from thunderstore.core.types import UserType @@ -16,3 +16,20 @@ def validate_user( "Service accounts are unable to perform this action" ) return user + + +def check_user_permissions( + user: Optional[UserType], allow_serviceaccount: bool = False +) -> Tuple[List[str], bool]: + errors = [] + public_error = True # Set to False if you want to hide certain errors + + if not user or not user.is_authenticated: + errors.append("Must be authenticated") + elif not user.is_active: + errors.append("User has been deactivated") + public_error = False + elif hasattr(user, "service_account") and not allow_serviceaccount: + errors.append("Service accounts are unable to perform this action") + + return errors, public_error diff --git a/django/thunderstore/repository/forms/team.py b/django/thunderstore/repository/forms/team.py index bf4923858..5a542b01b 100644 --- a/django/thunderstore/repository/forms/team.py +++ b/django/thunderstore/repository/forms/team.py @@ -80,10 +80,13 @@ def __init__(self, user: Optional[UserType], *args, **kwargs): def clean(self): result = super().clean() team = self.cleaned_data.get("team") - if team: - team.ensure_user_can_manage_members(self.user) - else: - raise ValidationError("Invalid team") + if not team: + raise ValidationError("Team is required") + + errors, is_public = team.validate_can_manage_members(self.user) + if errors: + raise PermissionValidationError(errors, is_public=is_public) + return result @@ -97,8 +100,14 @@ def __init__(self, user: Optional[UserType], *args, **kwargs): def clean_membership(self): membership = self.cleaned_data["membership"] if membership.user != self.user: - membership.team.ensure_user_can_manage_members(self.user) - membership.team.ensure_member_can_be_removed(membership) + errors, is_public = membership.team.validate_can_manage_members(self.user) + if errors: + raise PermissionValidationError(errors, is_public=is_public) + + error, is_public = membership.team.validate_member_can_be_removed(membership) + if error: + raise PermissionValidationError(error, is_public=is_public) + return membership def save(self): @@ -120,12 +129,14 @@ def clean_role(self): team = self.instance.team except ObjectDoesNotExist: team = None - if team: - team.ensure_member_role_can_be_changed( - member=self.instance, new_role=new_role - ) - else: + + if not team: raise ValidationError("Team is missing") + + error, is_public = team.validate_member_role_be_changed(self.instance, new_role) + if error: + raise PermissionValidationError(error, is_public=is_public) + return new_role def clean(self): @@ -133,10 +144,14 @@ def clean(self): team = self.instance.team except ObjectDoesNotExist: team = None - if team: - team.ensure_user_can_manage_members(self.user) - else: + + if not team: raise ValidationError("Team is missing") + + errors, is_public = team.validate_can_manage_members(self.user) + if errors: + raise PermissionValidationError(errors, is_public=is_public) + return super().clean() @@ -161,12 +176,16 @@ def clean_verification(self): def clean(self): if not self.instance.pk: raise ValidationError("Missing team instance") - self.instance.ensure_user_can_disband(self.user) + error, is_public = self.instance.validate_user_can_access(self.user) + if error: + raise PermissionValidationError(error, is_public=is_public) return super().clean() @transaction.atomic def save(self, **kwargs): - self.instance.ensure_user_can_disband(self.user) + error, is_public = self.instance.validate_user_can_access(self.user) + if error: + raise PermissionValidationError(error, is_public=is_public) self.instance.delete() diff --git a/django/thunderstore/repository/models/package.py b/django/thunderstore/repository/models/package.py index db1ab52c2..c3b3bd9fb 100644 --- a/django/thunderstore/repository/models/package.py +++ b/django/thunderstore/repository/models/package.py @@ -16,6 +16,7 @@ from thunderstore.cache.enums import CacheBustCondition from thunderstore.cache.tasks import invalidate_cache_on_commit_async from thunderstore.core.enums import OptionalBoolChoice +from thunderstore.core.exceptions import PermissionValidationError from thunderstore.core.mixins import AdminLinkMixin from thunderstore.core.types import UserType from thunderstore.core.utils import check_validity @@ -335,13 +336,17 @@ def ensure_user_can_manage_deprecation(self, user: Optional[UserType]) -> None: ): return - self.owner.ensure_user_can_manage_packages(user) + errors, is_public = self.owner.validate_user_can_manage_packages(user) + if errors: + raise PermissionValidationError(errors, is_public=is_public) def can_user_manage_deprecation(self, user: Optional[UserType]) -> bool: return check_validity(lambda: self.ensure_user_can_manage_deprecation(user)) def ensure_user_can_manage_wiki(self, user: Optional[UserType]) -> None: - return self.owner.ensure_user_can_manage_packages(user) + errors, is_public = self.owner.validate_user_can_manage_packages(user) + if errors: + raise PermissionValidationError(errors, is_public=is_public) def can_user_manage_wiki(self, user: Optional[UserType]) -> bool: return self.owner.can_user_manage_packages(user) diff --git a/django/thunderstore/repository/models/team.py b/django/thunderstore/repository/models/team.py index a77782a0f..1ce5cd7cf 100644 --- a/django/thunderstore/repository/models/team.py +++ b/django/thunderstore/repository/models/team.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import List, Optional, Tuple from django.conf import settings from django.core.exceptions import ValidationError @@ -11,7 +11,7 @@ from thunderstore.core.exceptions import PermissionValidationError from thunderstore.core.types import UserType from thunderstore.core.utils import ChoiceEnum, capture_exception, check_validity -from thunderstore.permissions.utils import validate_user +from thunderstore.permissions.utils import check_user_permissions, validate_user from thunderstore.repository.models import Namespace, Package from thunderstore.repository.validators import PackageReferenceComponentValidator @@ -247,138 +247,202 @@ def get_membership_for_user(self, user) -> Optional[TeamMember]: self.__membership_cache[user.pk] = self.members.filter(user=user).first() return self.__membership_cache[user.pk] - def ensure_can_create_service_account(self, user: Optional[UserType]) -> None: - user = validate_user(user) + def validate_can_create_service_account( + self, user: Optional[UserType] + ) -> Tuple[List[str], bool]: + errors, public_error = check_user_permissions(user) + if errors: + return errors, public_error + membership = self.get_membership_for_user(user) if not membership: - raise PermissionValidationError( - "Must be a member to create a service account" - ) - if membership.role != TeamMemberRole.owner: - raise PermissionValidationError( - "Must be an owner to create a service account" - ) + errors.append("Must be a member to create a service account") + if membership and membership.role != TeamMemberRole.owner: + errors.append("Must be an owner to create a service account") + + return errors, public_error + + def validate_can_edit_service_account( + self, user: Optional[UserType] + ) -> Tuple[List[str], bool]: + errors, public_error = check_user_permissions(user) + if errors: + return errors, public_error - def ensure_can_edit_service_account(self, user: Optional[UserType]) -> None: - user = validate_user(user) membership = self.get_membership_for_user(user) if not membership: - raise PermissionValidationError( - "Must be a member to edit a service account" - ) - if membership.role != TeamMemberRole.owner: - raise PermissionValidationError( - "Must be an owner to edit a service account" - ) + errors.append("Must be a member to edit a service account") + if membership and membership.role != TeamMemberRole.owner: + errors.append("Must be an owner to edit a service account") + + return errors, public_error + + def validate_can_delete_service_account( + self, user: Optional[UserType] + ) -> Tuple[List[str], bool]: + errors, public_error = check_user_permissions(user) + if errors: + return errors, public_error - def ensure_can_delete_service_account(self, user: Optional[UserType]) -> None: - user = validate_user(user) membership = self.get_membership_for_user(user) if not membership: - raise PermissionValidationError( - "Must be a member to delete a service account" - ) - if membership.role != TeamMemberRole.owner: - raise PermissionValidationError( - "Must be an owner to delete a service account" - ) + errors.append("Must be a member to delete a service account") + if membership and membership.role != TeamMemberRole.owner: + errors.append("Must be an owner to delete a service account") + + return errors, public_error + + def validate_can_manage_members( + self, user: Optional[UserType] + ) -> Tuple[List[str], bool]: + errors, public_error = check_user_permissions(user) + if errors: + return errors, public_error - def ensure_user_can_manage_members(self, user: Optional[UserType]) -> None: - user = validate_user(user) membership = self.get_membership_for_user(user) if not membership or membership.role != TeamMemberRole.owner: - raise PermissionValidationError("Must be an owner to manage team members") + errors.append("Must be an owner to manage team members") - def ensure_user_can_access(self, user: Optional[UserType]) -> None: - user = validate_user(user, allow_serviceaccount=True) - if not self.get_membership_for_user(user): - raise PermissionValidationError("Must be a member to access team") + return errors, public_error + + def validate_user_can_access( + self, user: Optional[UserType] + ) -> Tuple[List[str], bool]: + errors, public_error = check_user_permissions(user, allow_serviceaccount=True) + if errors: + return errors, public_error - def ensure_can_upload_package(self, user: Optional[UserType]) -> None: - user = validate_user(user, allow_serviceaccount=True) membership = self.get_membership_for_user(user) if not membership: - raise PermissionValidationError( - "Must be a member of team to upload package" - ) - if not self.is_active: - raise ValidationError( + errors.append("Must be a member to access team") + + return errors, public_error + + def validate_can_upload_package( + self, user: Optional[UserType] + ) -> Tuple[List[str], bool]: + errors, public_error = check_user_permissions(user, allow_serviceaccount=True) + if errors: + return errors, public_error + + membership = self.get_membership_for_user(user) + if not membership: + errors.append("Must be a member of team to upload package") + elif not self.is_active: + errors.append( "The team has been deactivated and as such cannot receive new packages" ) - def ensure_user_can_manage_packages(self, user: Optional[UserType]) -> None: - user = validate_user(user) + return errors, public_error + + def validate_user_can_manage_packages( + self, user: Optional[UserType] + ) -> Tuple[List[str], bool]: + errors, public_error = check_user_permissions(user) + if errors: + return errors, public_error + membership = self.get_membership_for_user(user) if not membership: - raise PermissionValidationError( - "Must be a member of team to manage packages" - ) + return ["Must be a member of team to manage packages"], True + + return [], True + + def validate_member_can_be_removed( + self, member: Optional[TeamMember] + ) -> Tuple[List[str], bool]: + public_error = True - def ensure_member_can_be_removed(self, member: Optional[TeamMember]) -> None: if not member: - raise ValidationError("Invalid member") + return ["Invalid member"], public_error if member.team != self: - raise ValidationError("Member is not a part of this team") + return ["Member is not a part of this team"], public_error if self.is_last_owner(member): - raise ValidationError("Cannot remove last owner from team") + return ["Cannot remove last owner from team"], public_error - def ensure_member_role_can_be_changed( + return [], True + + def validate_member_role_can_be_changed( self, member: Optional[TeamMember], new_role: Optional[str] - ) -> None: + ) -> Tuple[List[str], bool]: + public_error = True + if not member: - raise ValidationError("Invalid member") + return ["Invalid member"], public_error if member.team != self: - raise ValidationError("Member is not a part of this team") + return ["Member is not a part of this team"], public_error if not new_role or new_role not in TeamMemberRole.options(): - raise ValidationError("New role is invalid") + return ["New role is invalid"], public_error if new_role != TeamMemberRole.owner: if self.is_last_owner(member): - raise ValidationError("Cannot remove last owner from team") + return ["Cannot remove last owner from team"], public_error + + return [], public_error + + def validate_user_can_disband(self, user: Optional[UserType]) -> Tuple[str, bool]: + error, public_error = check_user_permissions(user) + if error: + return error, public_error - def ensure_user_can_disband(self, user: Optional[UserType]): - user = validate_user(user) membership = self.get_membership_for_user(user) if not membership or membership.role != TeamMemberRole.owner: - raise PermissionValidationError("Must be an owner to disband team") + return ["Must be an owner to disband team"], public_error if self.owned_packages.exists(): - raise ValidationError("Unable to disband teams with packages") + return ["Unable to disband teams with packages"], public_error + + return [], True + + def validate_user_can_edit_info( + self, user: Optional[UserType] + ) -> Tuple[List[str], bool]: + errors, public_error = check_user_permissions(user) + if errors: + return errors, public_error - def ensure_user_can_edit_info(self, user: Optional[UserType]): - user = validate_user(user) membership = self.get_membership_for_user(user) if not membership or membership.role != TeamMemberRole.owner: - raise PermissionValidationError("Must be an owner to edit team info") + return ["Must be an owner to edit team info"], public_error + + return [], True def can_user_upload(self, user: Optional[UserType]) -> bool: - return check_validity(lambda: self.ensure_can_upload_package(user)) + errors, _ = self.validate_can_upload_package(user) + return len(errors) == 0 def can_user_manage_packages(self, user: Optional[UserType]) -> bool: - return check_validity(lambda: self.ensure_user_can_manage_packages(user)) + errors, _ = self.validate_user_can_manage_packages(user) + return len(errors) == 0 def can_user_manage_members(self, user: Optional[UserType]) -> bool: - return check_validity(lambda: self.ensure_user_can_manage_members(user)) + errors, _ = self.validate_can_manage_members(user) + return len(errors) == 0 def can_user_create_service_accounts(self, user: Optional[UserType]) -> bool: - return check_validity(lambda: self.ensure_can_create_service_account(user)) + errors, _ = self.validate_can_create_service_account(user) + return len(errors) == 0 def can_user_delete_service_accounts(self, user: Optional[UserType]) -> bool: - return check_validity(lambda: self.ensure_can_delete_service_account(user)) + errors, _ = self.validate_can_delete_service_account(user) + return len(errors) == 0 def can_user_access(self, user: Optional[UserType]) -> bool: - return check_validity(lambda: self.ensure_user_can_access(user)) + errors, _ = self.validate_user_can_access(user) + return len(errors) == 0 def can_member_be_removed(self, member: Optional[TeamMember]) -> bool: - return check_validity(lambda: self.ensure_member_can_be_removed(member)) + error, _ = self.validate_member_can_be_removed(member) + return len(error) == 0 def can_member_role_be_changed( self, member: Optional[TeamMember], new_role: Optional[str] ) -> bool: - return check_validity( - lambda: self.ensure_member_role_can_be_changed(member, new_role) - ) + error, _ = self.validate_member_role_can_be_changed(member, new_role) + return len(error) == 0 def can_user_disband(self, user: Optional[UserType]) -> bool: - return check_validity(lambda: self.ensure_user_can_disband(user)) + error, _ = self.validate_user_can_disband(user) + return len(error) == 0 def can_user_edit_info(self, user: Optional[UserType]) -> bool: - return check_validity(lambda: self.ensure_user_can_edit_info(user)) + error, _ = self.validate_user_can_edit_info(user) + return len(error) == 0 diff --git a/django/thunderstore/repository/package_upload.py b/django/thunderstore/repository/package_upload.py index bf27e341e..c45c3c9ef 100644 --- a/django/thunderstore/repository/package_upload.py +++ b/django/thunderstore/repository/package_upload.py @@ -8,6 +8,7 @@ from django.db import transaction from thunderstore.community.models import Community, PackageCategory +from thunderstore.core.exceptions import PermissionValidationError from thunderstore.core.types import UserType from thunderstore.repository.consts import PackageVersionReviewStatus from thunderstore.repository.filetree import create_file_tree_from_zip_data @@ -110,7 +111,9 @@ def validate_markdown(self, name: str, data: bytes) -> Optional[str]: def clean_team(self): team = self.cleaned_data["team"] - team.ensure_can_upload_package(self.user) + errors, is_public = team.validate_can_upload_package(self.user) + if errors: + raise PermissionValidationError(errors, is_public=is_public) return team def clean_community_categories(self): @@ -203,7 +206,11 @@ def save(self, *args, **kwargs) -> PackageVersion: self.instance.review_status = PackageVersionReviewStatus.unreviewed team = self.cleaned_data["team"] - team.ensure_can_upload_package(self.user) + + errors, is_public = team.validate_can_upload_package(self.user) + if errors: + raise PermissionValidationError(errors, is_public=is_public) + # We just take the namespace with team name for now namespace = team.get_namespace() self.instance.package = Package.objects.get_or_create( diff --git a/django/thunderstore/repository/tests/test_team.py b/django/thunderstore/repository/tests/test_team.py index 79cb2be82..fe51605f7 100644 --- a/django/thunderstore/repository/tests/test_team.py +++ b/django/thunderstore/repository/tests/test_team.py @@ -316,14 +316,14 @@ def test_team_ensure_user_can_manage_members( user = TestUserTypes.get_user_by_type(user_type) if user_type in TestUserTypes.fake_users(): assert team.can_user_manage_members(user) is False - with pytest.raises(ValidationError) as e: - team.ensure_user_can_manage_members(user) - assert "Must be authenticated" in str(e.value) + errors, is_public = team.validate_can_manage_members(user) + assert errors == ["Must be authenticated"] + assert is_public is True elif user_type == TestUserTypes.deactivated_user: assert team.can_user_manage_members(user) is False - with pytest.raises(ValidationError) as e: - team.ensure_user_can_manage_members(user) - assert "User has been deactivated" in str(e.value) + errors, is_public = team.validate_can_manage_members(user) + assert errors == ["User has been deactivated"] + assert is_public is False else: if role is not None: TeamMember.objects.create( @@ -333,35 +333,37 @@ def test_team_ensure_user_can_manage_members( ) if user_type == TestUserTypes.service_account: assert team.can_user_manage_members(user) is False - with pytest.raises(ValidationError) as e: - team.ensure_user_can_manage_members(user) - assert "Service accounts are unable to perform this action" in str(e.value) + + errors, is_public = team.validate_can_manage_members(user) + assert errors == ["Service accounts are unable to perform this action"] + assert is_public is True else: if role == TeamMemberRole.owner: assert team.can_user_manage_members(user) is True - assert team.ensure_user_can_manage_members(user) is None + errors, is_public = team.validate_can_manage_members(user) + assert errors == [] else: assert team.can_user_manage_members(user) is False - with pytest.raises(ValidationError) as e: - team.ensure_user_can_manage_members(user) - assert "Must be an owner to manage team members" in str(e.value) + errors, is_public = team.validate_can_manage_members(user) + errors = ["Must be an owner to manage team members"] + is_public is True @pytest.mark.django_db @pytest.mark.parametrize("user_type", TestUserTypes.options()) @pytest.mark.parametrize("role", TeamMemberRole.options() + [None]) -def test_team_ensure_user_can_access(team: Team, user_type: str, role: str) -> None: +def test_team_validate_user_can_access(team: Team, user_type: str, role: str) -> None: user = TestUserTypes.get_user_by_type(user_type) if user_type in TestUserTypes.fake_users(): + errors, is_public = team.validate_user_can_access(user) assert team.can_user_access(user) is False - with pytest.raises(ValidationError) as e: - team.ensure_user_can_access(user) - assert "Must be authenticated" in str(e.value) + assert errors == ["Must be authenticated"] + assert is_public is True elif user_type == TestUserTypes.deactivated_user: + errors, is_public = team.validate_user_can_access(user) assert team.can_user_access(user) is False - with pytest.raises(ValidationError) as e: - team.ensure_user_can_access(user) - assert "User has been deactivated" in str(e.value) + assert errors == ["User has been deactivated"] + assert is_public is False else: if role is not None: TeamMember.objects.create( @@ -371,19 +373,20 @@ def test_team_ensure_user_can_access(team: Team, user_type: str, role: str) -> N ) if role is not None: assert team.can_user_access(user) is True - assert team.ensure_user_can_access(user) is None + errors, is_public = team.validate_user_can_access(user) + assert errors == [] else: assert team.can_user_access(user) is False - with pytest.raises(ValidationError) as e: - team.ensure_user_can_access(user) - assert "Must be a member to access team" in str(e.value) + errors, is_public = team.validate_user_can_access(user) + assert errors == ["Must be a member to access team"] + assert is_public is True @pytest.mark.django_db @pytest.mark.parametrize("team_active", (False, True)) @pytest.mark.parametrize("user_type", TestUserTypes.options()) @pytest.mark.parametrize("role", TeamMemberRole.options() + [None]) -def test_team_ensure_can_upload_package( +def test_team_validate_can_upload_package( team: Team, team_active: bool, user_type: str, @@ -394,9 +397,9 @@ def test_team_ensure_can_upload_package( user = TestUserTypes.get_user_by_type(user_type) if user_type in TestUserTypes.fake_users(): assert team.can_user_upload(user) is False - with pytest.raises(ValidationError) as e: - team.ensure_can_upload_package(user) - assert "Must be authenticated" in str(e.value) + errors, is_public = team.validate_can_upload_package(user) + assert errors == ["Must be authenticated"] + assert is_public is True else: if role is not None: TeamMember.objects.create( @@ -407,34 +410,36 @@ def test_team_ensure_can_upload_package( if role is not None: if user_type == TestUserTypes.deactivated_user: assert team.can_user_upload(user) is False - with pytest.raises(ValidationError) as e: - team.ensure_can_upload_package(user) - assert "User has been deactivated" in str(e.value) + errors, is_public = team.validate_can_upload_package(user) + assert errors == ["User has been deactivated"] + assert is_public is False else: if team_active: assert team.can_user_upload(user) is True - assert team.ensure_user_can_access(user) is None + errors, is_public = team.validate_can_upload_package(user) + assert errors == [] else: assert team.can_user_upload(user) is False - with pytest.raises(ValidationError) as e: - team.ensure_can_upload_package(user) - assert ( + errors, is_public = team.validate_can_upload_package(user) + assert errors == [ "The team has been deactivated and as such cannot receive new packages" - in str(e.value) - ) + ] + assert is_public is True else: assert team.can_user_upload(user) is False - with pytest.raises(ValidationError) as e: - team.ensure_can_upload_package(user) + errors, is_public = team.validate_can_upload_package(user) + assert errors, is_public == ["Must be a member of team to upload package"] if user_type == TestUserTypes.deactivated_user: - assert "User has been deactivated" in str(e.value) + assert is_public is False + assert errors == ["User has been deactivated"] else: - assert "Must be a member of team to upload package" in str(e.value) + assert is_public is True + assert errors == ["Must be a member of team to upload package"] @pytest.mark.django_db @pytest.mark.parametrize("role", TeamMemberRole.options()) -def test_team_ensure_member_can_be_removed(team: Team, role: str) -> None: +def test_team_validate_member_can_be_removed(team: Team, role: str) -> None: member = TeamMemberFactory( role=role, team=team, @@ -445,32 +450,34 @@ def test_team_ensure_member_can_be_removed(team: Team, role: str) -> None: role=TeamMemberRole.owner, ) assert team.can_member_be_removed(member) is True - team.ensure_member_can_be_removed(member) + errors, is_public = team.validate_member_can_be_removed(member) + assert errors == [] + assert is_public is True @pytest.mark.django_db -def test_team_ensure_member_can_be_removed_wrong_team( +def test_team_validate_member_can_be_removed_wrong_team( team: Team, ) -> None: member = TeamMemberFactory(role=TeamMemberRole.member) assert team.can_member_be_removed(member) is False - with pytest.raises(ValidationError) as e: - team.ensure_member_can_be_removed(member) - assert "Member is not a part of this team" in str(e.value) + errors, is_public = team.validate_member_can_be_removed(member) + assert errors == ["Member is not a part of this team"] + assert is_public is True @pytest.mark.django_db -def test_team_ensure_member_can_be_removed_no_member( +def test_team_validate_member_can_be_removed_no_member( team: Team, ) -> None: assert team.can_member_be_removed(None) is False - with pytest.raises(ValidationError) as e: - team.ensure_member_can_be_removed(None) - assert "Invalid member" in str(e.value) + errors, is_public = team.validate_member_can_be_removed(None) + assert errors == ["Invalid member"] + assert is_public is True @pytest.mark.django_db -def test_team_ensure_member_can_be_removed_last_owner( +def test_team_validate_member_can_be_removed_last_owner( team: Team, ) -> None: owner = TeamMemberFactory( @@ -479,62 +486,62 @@ def test_team_ensure_member_can_be_removed_last_owner( ) assert team.members.count() == 1 assert team.can_member_be_removed(owner) is False - with pytest.raises(ValidationError) as e: - team.ensure_member_can_be_removed(owner) - assert "Cannot remove last owner from team" in str(e.value) + errors, is_public = team.validate_member_can_be_removed(owner) + assert errors == ["Cannot remove last owner from team"] + assert is_public is True @pytest.mark.django_db @pytest.mark.parametrize("new_role", TeamMemberRole.options()) -def test_team_ensure_member_role_can_be_changed_wrong_team( +def test_team_validate_member_role_can_be_changed_wrong_team( team: Team, new_role: str ) -> None: member = TeamMemberFactory(role=TeamMemberRole.member) assert team.can_member_role_be_changed(member, new_role) is False - with pytest.raises(ValidationError) as e: - team.ensure_member_role_can_be_changed(member, new_role) - assert "Member is not a part of this team" in str(e.value) + error, is_public = team.validate_member_role_can_be_changed(member, new_role) + assert error == ["Member is not a part of this team"] + assert is_public is True @pytest.mark.django_db @pytest.mark.parametrize("new_role", TeamMemberRole.options()) -def test_team_ensure_member_role_can_be_changed_no_member( +def test_team_validate_member_role_can_be_changed_no_member( team: Team, new_role: str ) -> None: assert team.can_member_role_be_changed(None, new_role) is False - with pytest.raises(ValidationError) as e: - team.ensure_member_role_can_be_changed(None, new_role) - assert "Invalid member" in str(e.value) + error, is_public = team.validate_member_role_can_be_changed(None, new_role) + assert error == ["Invalid member"] + assert is_public is True @pytest.mark.django_db @pytest.mark.parametrize("role", ("invalid", None)) -def test_team_ensure_member_role_can_be_changed_invalid_role( +def test_team_validate_member_role_can_be_changed_invalid_role( team: Team, role: Optional[str] ) -> None: member = TeamMemberFactory(team=team, role=TeamMemberRole.member) assert team.can_member_role_be_changed(member, role) is False - with pytest.raises(ValidationError) as e: - team.ensure_member_role_can_be_changed(member, role) - assert "New role is invalid" in str(e.value) + error, is_public = team.validate_member_role_can_be_changed(member, role) + assert error == ["New role is invalid"] + assert is_public is True @pytest.mark.django_db -def test_team_ensure_member_role_can_be_changed_last_owner( +def test_team_validate_member_role_can_be_changed_last_owner( team: Team, ) -> None: new_role = TeamMemberRole.member member = TeamMemberFactory(team=team, role=TeamMemberRole.owner) assert team.can_member_role_be_changed(member, new_role) is False - with pytest.raises(ValidationError) as e: - team.ensure_member_role_can_be_changed(member, new_role) - assert "Cannot remove last owner from team" in str(e.value) + error, is_public = team.validate_member_role_can_be_changed(member, new_role) + assert error == ["Cannot remove last owner from team"] + assert is_public is True @pytest.mark.django_db @pytest.mark.parametrize("old_role", TeamMemberRole.options()) @pytest.mark.parametrize("new_role", TeamMemberRole.options()) -def test_team_ensure_member_role_can_be_changed( +def test_team_validate_member_role_can_be_changed( team: Team, old_role: str, new_role: str ) -> None: member = TeamMemberFactory(team=team, role=old_role) @@ -544,30 +551,32 @@ def test_team_ensure_member_role_can_be_changed( if is_last_owner: TeamMemberFactory(team=team, role=TeamMemberRole.owner) assert team.can_member_role_be_changed(member, new_role) is True - team.ensure_member_role_can_be_changed(member, new_role) + error, is_public = team.validate_member_role_can_be_changed(member, new_role) + assert error == [] + assert is_public is True @pytest.mark.django_db @pytest.mark.parametrize("user_type", TestUserTypes.options()) @pytest.mark.parametrize("role", TeamMemberRole.options() + [None]) -def test_team_ensure_user_can_disband(team: Team, user_type: str, role: str) -> None: +def test_team_validate_user_can_disband(team: Team, user_type: str, role: str) -> None: user = TestUserTypes.get_user_by_type(user_type) if not user or not user.is_authenticated: assert team.can_user_disband(user) is False - with pytest.raises(ValidationError) as e: - team.ensure_user_can_disband(user) - assert "Must be authenticated" in str(e.value) + errors, is_public = team.validate_user_can_disband(user) + assert errors == ["Must be authenticated"] + assert is_public is True elif user_type == TestUserTypes.deactivated_user: assert team.can_user_disband(user) is False - with pytest.raises(ValidationError) as e: - team.ensure_user_can_disband(user) - assert "User has been deactivated" in str(e.value) + errors, is_public = team.validate_user_can_disband(user) + assert errors == ["User has been deactivated"] + assert is_public is False elif user_type == TestUserTypes.service_account: assert team.can_user_disband(user) is False - with pytest.raises(ValidationError) as e: - team.ensure_user_can_disband(user) - assert "Service accounts are unable to perform this action" in str(e.value) + errors, is_public = team.validate_user_can_disband(user) + assert errors == ["Service accounts are unable to perform this action"] + assert is_public is True else: if role is not None: TeamMember.objects.create( @@ -577,23 +586,24 @@ def test_team_ensure_user_can_disband(team: Team, user_type: str, role: str) -> ) if role != TeamMemberRole.owner: assert team.can_user_disband(user) is False - with pytest.raises(ValidationError) as e: - team.ensure_user_can_disband(user) - assert "Must be an owner to disband team" in str(e.value) + errors, is_public = team.validate_user_can_disband(user) + assert errors == ["Must be an owner to disband team"] + assert is_public is True else: assert team.can_user_disband(user) is True - team.ensure_user_can_disband(user) + errors, is_public = team.validate_user_can_disband(user) + assert errors == [] @pytest.mark.django_db -def test_team_ensure_user_can_disband_has_packages( +def test_team_validate_user_can_disband_has_packages( team: Team, package: Package ) -> None: member = TeamMemberFactory(team=team, role=TeamMemberRole.owner) assert team.can_user_disband(member.user) is False - with pytest.raises(ValidationError) as e: - team.ensure_user_can_disband(member.user) - assert "Unable to disband teams with packages" in str(e.value) + errors, is_public = team.validate_user_can_disband(member.user) + assert errors == ["Unable to disband teams with packages"] + assert is_public is True @pytest.mark.django_db @@ -638,27 +648,26 @@ def test_team_settings_url(team: Team): @pytest.mark.django_db @pytest.mark.parametrize("user_type", TestUserTypes.options()) @pytest.mark.parametrize("role", TeamMemberRole.options() + [None]) -def test_team_ensure_can_create_service_account( +def test_team_validate_can_create_service_account( team: Team, user_type: str, role: str ) -> None: user = TestUserTypes.get_user_by_type(user_type) if user_type in TestUserTypes.fake_users(): - with pytest.raises(ValidationError) as e: - team.ensure_can_create_service_account(user) - assert "Must be authenticated" in str(e.value) + errors, _ = team.validate_can_create_service_account(user) + assert team.can_user_create_service_accounts(user) is False + assert errors == ["Must be authenticated"] elif user_type == TestUserTypes.deactivated_user: - with pytest.raises(ValidationError) as e: - team.ensure_can_create_service_account(user) - assert "User has been deactivated" in str(e.value) + errors, _ = team.validate_can_create_service_account(user) + assert team.can_user_create_service_accounts(user) is False + assert errors == ["User has been deactivated"] elif user_type == TestUserTypes.service_account: - with pytest.raises( - ValidationError, match="Service accounts are unable to perform this action" - ): - team.ensure_can_create_service_account(user) + errors, _ = team.validate_can_create_service_account(user) + assert team.can_user_create_service_accounts(user) is False + assert errors == ["Service accounts are unable to perform this action"] elif role is None: - with pytest.raises(ValidationError) as e: - team.ensure_can_create_service_account(user) - assert "Must be a member to create a service account" in str(e.value) + errors, _ = team.validate_can_create_service_account(user) + assert team.can_user_create_service_accounts(user) is False + assert errors == ["Must be a member to create a service account"] else: TeamMember.objects.create( user=user, @@ -666,11 +675,81 @@ def test_team_ensure_can_create_service_account( role=role, ) if role == TeamMemberRole.member: - with pytest.raises(ValidationError) as e: - team.ensure_can_create_service_account(user) - assert "Must be an owner to create a service account" in str(e.value) + errors, _ = team.validate_can_create_service_account(user) + assert team.can_user_create_service_accounts(user) is False + assert errors == ["Must be an owner to create a service account"] if role == TeamMemberRole.owner: - assert team.ensure_can_create_service_account(user) is None + errors, _ = team.validate_can_create_service_account(user) + assert team.can_user_create_service_accounts(user) is True + assert errors == [] + + +@pytest.mark.django_db +@pytest.mark.parametrize("user_type", TestUserTypes.options()) +@pytest.mark.parametrize("role", TeamMemberRole.options() + [None]) +def test_team_validate_can_edit_service_account( + team: Team, user_type: str, role: str +) -> None: + user = TestUserTypes.get_user_by_type(user_type) + + user_type_errors = { + **{type_: ["Must be authenticated"] for type_ in TestUserTypes.fake_users()}, + TestUserTypes.deactivated_user: ["User has been deactivated"], + TestUserTypes.service_account: [ + "Service accounts are unable to perform this action" + ], + } + + if user_type in user_type_errors: + expected_errors = user_type_errors[user_type] + elif role is None: + expected_errors = ["Must be a member to edit a service account"] + else: + TeamMember.objects.create(user=user, team=team, role=role) + if role == TeamMemberRole.member: + expected_errors = ["Must be an owner to edit a service account"] + else: # owner + expected_errors = [] + + errors, _ = team.validate_can_edit_service_account(user) + assert errors == expected_errors + + +@pytest.mark.django_db +@pytest.mark.parametrize("user_type", TestUserTypes.options()) +@pytest.mark.parametrize("role", TeamMemberRole.options() + [None]) +def test_team_validate_can_delete_service_account( + team: Team, user_type: str, role: str +) -> None: + user = TestUserTypes.get_user_by_type(user_type) + + user_type_errors = { + **{type_: ["Must be authenticated"] for type_ in TestUserTypes.fake_users()}, + TestUserTypes.deactivated_user: ["User has been deactivated"], + TestUserTypes.service_account: [ + "Service accounts are unable to perform this action" + ], + } + + if user_type in user_type_errors: + expected_errors = user_type_errors[user_type] + elif role is None: + expected_errors = ["Must be a member to delete a service account"] + else: + TeamMember.objects.create(user=user, team=team, role=role) + if role == TeamMemberRole.member: + expected_errors = ["Must be an owner to delete a service account"] + else: # owner + expected_errors = [] + + errors, _ = team.validate_can_delete_service_account(user) + + if errors == []: + assert team.can_user_delete_service_accounts(user) is True + else: + assert team.can_user_delete_service_accounts(user) is False + + assert errors == expected_errors @pytest.mark.django_db @@ -716,7 +795,9 @@ def test_team_donation_link_validation( @pytest.mark.django_db @pytest.mark.parametrize("user_type", TestUserTypes.options()) @pytest.mark.parametrize("role", TeamMemberRole.options() + [None]) -def test_team_ensure_user_can_edit_info(team: Team, user_type: str, role: str) -> None: +def test_team_validate_user_can_edit_info( + team: Team, user_type: str, role: str +) -> None: user = TestUserTypes.get_user_by_type(user_type) if role is not None and user_type not in TestUserTypes.fake_users(): TeamMember.objects.create(user=user, team=team, role=role) @@ -734,17 +815,18 @@ def test_team_ensure_user_can_edit_info(team: Team, user_type: str, role: str) - if expected_error is not None: assert team.can_user_edit_info(user) is False - with pytest.raises(ValidationError, match=expected_error): - team.ensure_user_can_edit_info(user) + errors, _ = team.validate_user_can_edit_info(user) + assert errors == [expected_error] else: assert team.can_user_edit_info(user) is True - assert team.ensure_user_can_edit_info(user) is None + errors, _ = team.validate_user_can_edit_info(user) + assert errors == [] @pytest.mark.django_db @pytest.mark.parametrize("user_type", TestUserTypes.options()) @pytest.mark.parametrize("role", TeamMemberRole.options() + [None]) -def test_team_ensure_user_can_manage_packages( +def test_team_validate_user_can_manage_packages( team: Team, user_type: str, role: str ) -> None: user = TestUserTypes.get_user_by_type(user_type) @@ -764,8 +846,9 @@ def test_team_ensure_user_can_manage_packages( if expected_error is not None: assert team.can_user_manage_packages(user) is False - with pytest.raises(ValidationError, match=expected_error): - team.ensure_user_can_manage_packages(user) + error, _ = team.validate_user_can_manage_packages(user) + assert error == [expected_error] else: assert team.can_user_manage_packages(user) is True - assert team.ensure_user_can_manage_packages(user) is None + error, _ = team.validate_user_can_manage_packages(user) + assert error == []