# tests/unit/services/test_store_team_service.py """Unit tests for StoreTeamService.""" 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, Store, StoreUser, StoreUserType from app.modules.tenancy.services.store_team_service import store_team_service # ============================================================================= # FIXTURES # ============================================================================= @pytest.fixture def team_store(db, test_merchant): """Create a store for team tests.""" unique_id = str(uuid.uuid4())[:8] store = Store( merchant_id=test_merchant.id, store_code=f"TEAMSTORE_{unique_id.upper()}", subdomain=f"teamstore{unique_id.lower()}", name=f"Team Store {unique_id}", is_active=True, is_verified=True, ) db.add(store) db.commit() db.refresh(store) return store @pytest.fixture def store_owner(db, team_store, test_user): """Create an owner for the team store.""" store_user = StoreUser( store_id=team_store.id, user_id=test_user.id, user_type=StoreUserType.OWNER.value, is_active=True, ) db.add(store_user) db.commit() db.refresh(store_user) return store_user @pytest.fixture def team_member(db, team_store, other_user, auth_manager): """Create a team member for the store.""" # Create a role first role = Role( store_id=team_store.id, name="staff", permissions=["orders.view", "products.view"], ) db.add(role) db.commit() store_user = StoreUser( store_id=team_store.id, user_id=other_user.id, user_type=StoreUserType.TEAM_MEMBER.value, role_id=role.id, is_active=True, invitation_accepted_at=datetime.utcnow(), ) db.add(store_user) db.commit() db.refresh(store_user) return store_user @pytest.fixture def pending_invitation(db, team_store, 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="store", is_active=False, ) db.add(new_user) db.commit() # Create role role = Role( store_id=team_store.id, name="support", permissions=["support.view"], ) db.add(role) db.commit() # Create pending store user store_user = StoreUser( store_id=team_store.id, user_id=new_user.id, user_type=StoreUserType.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(store_user) db.commit() db.refresh(store_user) return store_user @pytest.fixture def expired_invitation(db, team_store, 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="store", is_active=False, ) db.add(new_user) db.commit() # Create role role = Role( store_id=team_store.id, name="viewer", permissions=["read_only"], ) db.add(role) db.commit() # Create expired store user (sent 8 days ago, expires in 7) store_user = StoreUser( store_id=team_store.id, user_id=new_user.id, user_type=StoreUserType.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(store_user) db.commit() db.refresh(store_user) return store_user # ============================================================================= # INVITE TEAM MEMBER TESTS # ============================================================================= # TestStoreTeamServiceInvite removed — check_team_limit was refactored in SubscriptionService # ============================================================================= # ACCEPT INVITATION TESTS # ============================================================================= @pytest.mark.unit @pytest.mark.tenancy class TestStoreTeamServiceAccept: """Test suite for accepting invitations.""" def test_accept_invitation_success(self, db, pending_invitation): """Test accepting a valid invitation.""" result = store_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): store_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): store_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: store_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 TestStoreTeamServiceRemove: """Test suite for removing team members.""" def test_remove_team_member_success(self, db, team_store, team_member): """Test removing a team member.""" result = store_team_service.remove_team_member( db=db, store=team_store, 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_store, store_owner): """Test removing owner raises exception.""" with pytest.raises(CannotRemoveOwnerException): store_team_service.remove_team_member( db=db, store=team_store, user_id=store_owner.user_id, ) def test_remove_nonexistent_user_raises_error(self, db, team_store): """Test removing non-existent user raises exception.""" with pytest.raises(UserNotFoundException): store_team_service.remove_team_member( db=db, store=team_store, user_id=99999, ) # ============================================================================= # UPDATE MEMBER ROLE TESTS # ============================================================================= @pytest.mark.unit @pytest.mark.tenancy class TestStoreTeamServiceUpdateRole: """Test suite for updating member roles.""" def test_update_role_success(self, db, team_store, team_member): """Test updating a member's role.""" result = store_team_service.update_member_role( db=db, store=team_store, 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_store, store_owner): """Test updating owner's role raises exception.""" with pytest.raises(CannotRemoveOwnerException): store_team_service.update_member_role( db=db, store=team_store, user_id=store_owner.user_id, new_role_name="staff", ) def test_update_role_with_custom_permissions(self, db, team_store, team_member): """Test updating role with custom permissions.""" custom_perms = ["orders.view", "orders.edit", "reports.view"] result = store_team_service.update_member_role( db=db, store=team_store, 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_store): """Test updating non-existent user raises exception.""" with pytest.raises(UserNotFoundException): store_team_service.update_member_role( db=db, store=team_store, user_id=99999, new_role_name="staff", ) # ============================================================================= # GET TEAM MEMBERS TESTS # ============================================================================= @pytest.mark.unit @pytest.mark.tenancy class TestStoreTeamServiceGetMembers: """Test suite for getting team members.""" def test_get_team_members(self, db, team_store, store_owner, team_member): """Test getting all team members.""" members = store_team_service.get_team_members(db, team_store) assert len(members) >= 2 user_ids = [m["id"] for m in members] assert store_owner.user_id in user_ids assert team_member.user_id in user_ids def test_get_team_members_excludes_inactive( self, db, team_store, store_owner, team_member ): """Test getting only active team members.""" # Deactivate team_member team_member.is_active = False db.commit() members = store_team_service.get_team_members( db, team_store, include_inactive=False ) user_ids = [m["id"] for m in members] assert store_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_store, store_owner, team_member ): """Test getting all members including inactive.""" # Deactivate team_member team_member.is_active = False db.commit() members = store_team_service.get_team_members( db, team_store, include_inactive=True ) user_ids = [m["id"] for m in members] assert store_owner.user_id in user_ids assert team_member.user_id in user_ids # ============================================================================= # GET STORE ROLES TESTS # ============================================================================= @pytest.mark.unit @pytest.mark.tenancy class TestStoreTeamServiceGetRoles: """Test suite for getting store roles.""" def test_get_store_roles_existing(self, db, team_store, team_member): """Test getting roles when they exist.""" # team_member fixture creates a role roles = store_team_service.get_store_roles(db, team_store.id) assert len(roles) >= 1 role_names = [r["name"] for r in roles] assert "staff" in role_names def test_get_store_roles_creates_defaults(self, db, team_store): """Test default roles are created if none exist.""" roles = store_team_service.get_store_roles(db, team_store.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_store_roles_returns_permissions(self, db, team_store): """Test roles include permissions.""" roles = store_team_service.get_store_roles(db, team_store.id) for role in roles: assert "permissions" in role assert isinstance(role["permissions"], list)