# tests/unit/services/test_vendor_team_service.py """Unit tests for VendorTeamService.""" import uuid from datetime import datetime, timedelta from unittest.mock import patch import pytest from app.modules.tenancy.exceptions import ( CannotRemoveOwnerException, InvalidInvitationTokenException, TeamInvitationAlreadyAcceptedException, TeamMemberAlreadyExistsException, UserNotFoundException, ) from app.modules.tenancy.models import Role, User, Vendor, VendorUser, VendorUserType from app.modules.tenancy.services.vendor_team_service import vendor_team_service # ============================================================================= # FIXTURES # ============================================================================= @pytest.fixture def team_vendor(db, test_company): """Create a vendor for team tests.""" unique_id = str(uuid.uuid4())[:8] vendor = Vendor( company_id=test_company.id, vendor_code=f"TEAMVENDOR_{unique_id.upper()}", subdomain=f"teamvendor{unique_id.lower()}", name=f"Team Vendor {unique_id}", is_active=True, is_verified=True, ) db.add(vendor) db.commit() db.refresh(vendor) return vendor @pytest.fixture def vendor_owner(db, team_vendor, test_user): """Create an owner for the team vendor.""" vendor_user = VendorUser( vendor_id=team_vendor.id, user_id=test_user.id, user_type=VendorUserType.OWNER.value, is_active=True, ) db.add(vendor_user) db.commit() db.refresh(vendor_user) return vendor_user @pytest.fixture def team_member(db, team_vendor, other_user, auth_manager): """Create a team member for the vendor.""" # Create a role first role = Role( vendor_id=team_vendor.id, name="staff", permissions=["orders.view", "products.view"], ) db.add(role) db.commit() vendor_user = VendorUser( vendor_id=team_vendor.id, user_id=other_user.id, user_type=VendorUserType.TEAM_MEMBER.value, role_id=role.id, is_active=True, invitation_accepted_at=datetime.utcnow(), ) db.add(vendor_user) db.commit() db.refresh(vendor_user) return vendor_user @pytest.fixture def pending_invitation(db, team_vendor, test_user, auth_manager): """Create a pending invitation.""" unique_id = str(uuid.uuid4())[:8] # Create new user for invitation new_user = User( email=f"pending_{unique_id}@example.com", username=f"pending_{unique_id}", hashed_password=auth_manager.hash_password("temppass"), role="vendor", is_active=False, ) db.add(new_user) db.commit() # Create role role = Role( vendor_id=team_vendor.id, name="support", permissions=["support.view"], ) db.add(role) db.commit() # Create pending vendor user vendor_user = VendorUser( vendor_id=team_vendor.id, user_id=new_user.id, user_type=VendorUserType.TEAM_MEMBER.value, role_id=role.id, invited_by=test_user.id, invitation_token=f"pending_token_{unique_id}", invitation_sent_at=datetime.utcnow(), is_active=False, ) db.add(vendor_user) db.commit() db.refresh(vendor_user) return vendor_user @pytest.fixture def expired_invitation(db, team_vendor, test_user, auth_manager): """Create an expired invitation.""" unique_id = str(uuid.uuid4())[:8] # Create new user for invitation new_user = User( email=f"expired_{unique_id}@example.com", username=f"expired_{unique_id}", hashed_password=auth_manager.hash_password("temppass"), role="vendor", is_active=False, ) db.add(new_user) db.commit() # Create role role = Role( vendor_id=team_vendor.id, name="viewer", permissions=["read_only"], ) db.add(role) db.commit() # Create expired vendor user (sent 8 days ago, expires in 7) vendor_user = VendorUser( vendor_id=team_vendor.id, user_id=new_user.id, user_type=VendorUserType.TEAM_MEMBER.value, role_id=role.id, invited_by=test_user.id, invitation_token=f"expired_token_{unique_id}", invitation_sent_at=datetime.utcnow() - timedelta(days=8), is_active=False, ) db.add(vendor_user) db.commit() db.refresh(vendor_user) return vendor_user # ============================================================================= # INVITE TEAM MEMBER TESTS # ============================================================================= @pytest.mark.unit @pytest.mark.tenancy class TestVendorTeamServiceInvite: """Test suite for inviting team members.""" @patch("app.modules.billing.services.subscription_service.SubscriptionService.check_team_limit") def test_invite_new_user(self, mock_check_limit, db, team_vendor, test_user): """Test inviting a new user to the team.""" mock_check_limit.return_value = None # No limit reached unique_id = str(uuid.uuid4())[:8] email = f"newmember_{unique_id}@example.com" result = vendor_team_service.invite_team_member( db=db, vendor=team_vendor, inviter=test_user, email=email, role_name="staff", ) db.commit() assert result is not None assert result["email"] == email assert result["invitation_token"] is not None assert result["role"] == "staff" assert result["existing_user"] is False @patch("app.modules.billing.services.subscription_service.SubscriptionService.check_team_limit") def test_invite_existing_user(self, mock_check_limit, db, team_vendor, test_user, other_user): """Test inviting an existing user to the team.""" mock_check_limit.return_value = None result = vendor_team_service.invite_team_member( db=db, vendor=team_vendor, inviter=test_user, email=other_user.email, role_name="manager", ) db.commit() assert result is not None assert result["email"] == other_user.email assert result["invitation_token"] is not None @patch("app.modules.billing.services.subscription_service.SubscriptionService.check_team_limit") def test_invite_already_member_raises_error( self, mock_check_limit, db, team_vendor, test_user, team_member ): """Test inviting an existing member raises exception.""" mock_check_limit.return_value = None with pytest.raises(TeamMemberAlreadyExistsException): vendor_team_service.invite_team_member( db=db, vendor=team_vendor, inviter=test_user, email=team_member.user.email, role_name="staff", ) @patch("app.modules.billing.services.subscription_service.SubscriptionService.check_team_limit") def test_invite_with_custom_permissions(self, mock_check_limit, db, team_vendor, test_user): """Test inviting with custom permissions.""" mock_check_limit.return_value = None unique_id = str(uuid.uuid4())[:8] email = f"custom_{unique_id}@example.com" custom_perms = ["orders.view", "orders.edit", "products.view"] result = vendor_team_service.invite_team_member( db=db, vendor=team_vendor, inviter=test_user, email=email, role_name="custom_role", custom_permissions=custom_perms, ) db.commit() assert result is not None assert result["role"] == "custom_role" # ============================================================================= # ACCEPT INVITATION TESTS # ============================================================================= @pytest.mark.unit @pytest.mark.tenancy class TestVendorTeamServiceAccept: """Test suite for accepting invitations.""" def test_accept_invitation_success(self, db, pending_invitation): """Test accepting a valid invitation.""" result = vendor_team_service.accept_invitation( db=db, invitation_token=pending_invitation.invitation_token, password="newpassword123", first_name="John", last_name="Doe", ) db.commit() assert result is not None assert result["user"].is_active is True assert result["user"].first_name == "John" assert result["user"].last_name == "Doe" def test_accept_invitation_invalid_token(self, db): """Test accepting with invalid token raises exception.""" with pytest.raises(InvalidInvitationTokenException): vendor_team_service.accept_invitation( db=db, invitation_token="invalid_token_12345", password="password123", ) def test_accept_invitation_already_accepted(self, db, team_member): """Test accepting an already accepted invitation raises exception.""" # team_member already has invitation_accepted_at set with pytest.raises(InvalidInvitationTokenException): vendor_team_service.accept_invitation( db=db, invitation_token="some_token", # team_member has no token password="password123", ) def test_accept_invitation_expired(self, db, expired_invitation): """Test accepting an expired invitation raises exception.""" with pytest.raises(InvalidInvitationTokenException) as exc_info: vendor_team_service.accept_invitation( db=db, invitation_token=expired_invitation.invitation_token, password="password123", ) assert "expired" in str(exc_info.value).lower() # ============================================================================= # REMOVE TEAM MEMBER TESTS # ============================================================================= @pytest.mark.unit @pytest.mark.tenancy class TestVendorTeamServiceRemove: """Test suite for removing team members.""" def test_remove_team_member_success(self, db, team_vendor, team_member): """Test removing a team member.""" result = vendor_team_service.remove_team_member( db=db, vendor=team_vendor, user_id=team_member.user_id, ) db.commit() db.refresh(team_member) assert result is True assert team_member.is_active is False def test_remove_owner_raises_error(self, db, team_vendor, vendor_owner): """Test removing owner raises exception.""" with pytest.raises(CannotRemoveOwnerException): vendor_team_service.remove_team_member( db=db, vendor=team_vendor, user_id=vendor_owner.user_id, ) def test_remove_nonexistent_user_raises_error(self, db, team_vendor): """Test removing non-existent user raises exception.""" with pytest.raises(UserNotFoundException): vendor_team_service.remove_team_member( db=db, vendor=team_vendor, user_id=99999, ) # ============================================================================= # UPDATE MEMBER ROLE TESTS # ============================================================================= @pytest.mark.unit @pytest.mark.tenancy class TestVendorTeamServiceUpdateRole: """Test suite for updating member roles.""" def test_update_role_success(self, db, team_vendor, team_member): """Test updating a member's role.""" result = vendor_team_service.update_member_role( db=db, vendor=team_vendor, user_id=team_member.user_id, new_role_name="manager", ) db.commit() assert result is not None assert result.role.name == "manager" def test_update_owner_role_raises_error(self, db, team_vendor, vendor_owner): """Test updating owner's role raises exception.""" with pytest.raises(CannotRemoveOwnerException): vendor_team_service.update_member_role( db=db, vendor=team_vendor, user_id=vendor_owner.user_id, new_role_name="staff", ) def test_update_role_with_custom_permissions(self, db, team_vendor, team_member): """Test updating role with custom permissions.""" custom_perms = ["orders.view", "orders.edit", "reports.view"] result = vendor_team_service.update_member_role( db=db, vendor=team_vendor, user_id=team_member.user_id, new_role_name="analyst", custom_permissions=custom_perms, ) db.commit() assert result.role.name == "analyst" assert result.role.permissions == custom_perms def test_update_nonexistent_user_raises_error(self, db, team_vendor): """Test updating non-existent user raises exception.""" with pytest.raises(UserNotFoundException): vendor_team_service.update_member_role( db=db, vendor=team_vendor, user_id=99999, new_role_name="staff", ) # ============================================================================= # GET TEAM MEMBERS TESTS # ============================================================================= @pytest.mark.unit @pytest.mark.tenancy class TestVendorTeamServiceGetMembers: """Test suite for getting team members.""" def test_get_team_members(self, db, team_vendor, vendor_owner, team_member): """Test getting all team members.""" members = vendor_team_service.get_team_members(db, team_vendor) assert len(members) >= 2 user_ids = [m["id"] for m in members] assert vendor_owner.user_id in user_ids assert team_member.user_id in user_ids def test_get_team_members_excludes_inactive( self, db, team_vendor, vendor_owner, team_member ): """Test getting only active team members.""" # Deactivate team_member team_member.is_active = False db.commit() members = vendor_team_service.get_team_members( db, team_vendor, include_inactive=False ) user_ids = [m["id"] for m in members] assert vendor_owner.user_id in user_ids assert team_member.user_id not in user_ids def test_get_team_members_includes_inactive( self, db, team_vendor, vendor_owner, team_member ): """Test getting all members including inactive.""" # Deactivate team_member team_member.is_active = False db.commit() members = vendor_team_service.get_team_members( db, team_vendor, include_inactive=True ) user_ids = [m["id"] for m in members] assert vendor_owner.user_id in user_ids assert team_member.user_id in user_ids # ============================================================================= # GET VENDOR ROLES TESTS # ============================================================================= @pytest.mark.unit @pytest.mark.tenancy class TestVendorTeamServiceGetRoles: """Test suite for getting vendor roles.""" def test_get_vendor_roles_existing(self, db, team_vendor, team_member): """Test getting roles when they exist.""" # team_member fixture creates a role roles = vendor_team_service.get_vendor_roles(db, team_vendor.id) assert len(roles) >= 1 role_names = [r["name"] for r in roles] assert "staff" in role_names def test_get_vendor_roles_creates_defaults(self, db, team_vendor): """Test default roles are created if none exist.""" roles = vendor_team_service.get_vendor_roles(db, team_vendor.id) db.commit() assert len(roles) >= 5 # Default roles role_names = [r["name"] for r in roles] assert "manager" in role_names assert "staff" in role_names assert "support" in role_names assert "viewer" in role_names assert "marketing" in role_names def test_get_vendor_roles_returns_permissions(self, db, team_vendor): """Test roles include permissions.""" roles = vendor_team_service.get_vendor_roles(db, team_vendor.id) for role in roles: assert "permissions" in role assert isinstance(role["permissions"], list)