Move letzshop-related functionality from tenancy to marketplace module: - Move admin letzshop routes to marketplace/routes/api/admin_letzshop.py - Move letzshop schemas to marketplace/schemas/letzshop.py - Remove letzshop code from tenancy module (admin_vendors, vendor_service) - Update model exports and imports Add comprehensive unit tests for vendor services: - test_company_service.py: Company management operations - test_platform_service.py: Platform management operations - test_vendor_domain_service.py: Vendor domain operations - test_vendor_team_service.py: Vendor team management Update module definitions: - billing, messaging, payments: Minor definition updates Add architecture proposals documentation: - Module dependency redesign session notes - Decouple modules implementation plan - Module decoupling proposal Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
507 lines
16 KiB
Python
507 lines
16 KiB
Python
# 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)
|