# tests/unit/services/test_admin_service.py import pytest from app.exceptions import ValidationException from app.modules.analytics.services.stats_service import stats_service from app.modules.tenancy.exceptions import ( AdminOperationException, CannotModifySelfException, MerchantNotFoundException, StoreAlreadyExistsException, StoreNotFoundException, UserNotFoundException, UserStatusChangeException, ) from app.modules.tenancy.schemas.store import StoreCreate from app.modules.tenancy.services.admin_service import AdminService @pytest.mark.unit @pytest.mark.admin class TestAdminService: """Test suite for AdminService following the application's testing patterns""" def setup_method(self): """Setup method following the same pattern as product service tests""" self.service = AdminService() # User Management Tests def test_get_all_users(self, db, test_user, test_admin): """Test getting all users with pagination""" users = self.service.get_all_users(db, skip=0, limit=10) assert len(users) >= 2 # test_user + test_admin user_ids = [user.id for user in users] assert test_user.id in user_ids assert test_admin.id in user_ids def test_get_all_users_with_pagination(self, db, test_user, test_admin): """Test user pagination works correctly""" users = self.service.get_all_users(db, skip=0, limit=1) assert len(users) == 1 users_second_page = self.service.get_all_users(db, skip=1, limit=1) assert len(users_second_page) == 1 assert users[0].id != users_second_page[0].id def test_toggle_user_status_deactivate(self, db, test_user, test_admin): """Test deactivating a user""" assert test_user.is_active is True user, message = self.service.toggle_user_status(db, test_user.id, test_admin.id) assert user.id == test_user.id assert user.is_active is False assert test_user.username in message assert "deactivated" in message def test_toggle_user_status_activate(self, db, test_user, test_admin): """Test activating a user""" from app.modules.tenancy.models import User # Re-query user to get fresh instance user_to_deactivate = db.query(User).filter(User.id == test_user.id).first() user_to_deactivate.is_active = False db.commit() user, message = self.service.toggle_user_status(db, test_user.id, test_admin.id) assert user.id == test_user.id assert user.is_active is True assert "activated" in message def test_toggle_user_status_user_not_found(self, db, test_admin): """Test toggle user status when user not found""" with pytest.raises(UserNotFoundException) as exc_info: self.service.toggle_user_status(db, 99999, test_admin.id) exception = exc_info.value assert exception.error_code == "USER_NOT_FOUND" assert "99999" in exception.message def test_toggle_user_status_cannot_modify_self(self, db, test_admin): """Test that admin cannot modify their own account""" with pytest.raises(CannotModifySelfException) as exc_info: self.service.toggle_user_status(db, test_admin.id, test_admin.id) exception = exc_info.value assert exception.error_code == "CANNOT_MODIFY_SELF" assert "deactivate account" in exception.message def test_toggle_user_status_cannot_modify_admin( self, db, test_admin, another_admin ): """Test that admin cannot modify another admin""" with pytest.raises(UserStatusChangeException) as exc_info: self.service.toggle_user_status(db, another_admin.id, test_admin.id) exception = exc_info.value assert exception.error_code == "USER_STATUS_CHANGE_FAILED" assert "Cannot modify another admin user" in exception.message # Store Management Tests def test_get_all_stores(self, db, test_store): """Test getting all stores with total count""" stores, total = self.service.get_all_stores(db, skip=0, limit=10) assert total >= 1 assert len(stores) >= 1 store_codes = [store.store_code for store in stores] assert test_store.store_code in store_codes def test_get_all_stores_with_pagination(self, db, test_store, verified_store): """Test store pagination works correctly""" stores, total = self.service.get_all_stores(db, skip=0, limit=1) assert total >= 2 assert len(stores) == 1 stores_second_page, _ = self.service.get_all_stores(db, skip=1, limit=1) assert len(stores_second_page) >= 0 if len(stores_second_page) > 0: assert stores[0].id != stores_second_page[0].id def test_verify_store_mark_verified(self, db, test_store): """Test marking store as verified""" from app.modules.tenancy.models import Store # Re-query store to get fresh instance store_to_unverify = ( db.query(Store).filter(Store.id == test_store.id).first() ) store_to_unverify.is_verified = False db.commit() store, message = self.service.verify_store(db, test_store.id) assert store.id == test_store.id assert store.is_verified is True assert "verified" in message def test_verify_store_mark_unverified(self, db, verified_store): """Test marking verified store as unverified""" store, message = self.service.verify_store(db, verified_store.id) assert store.id == verified_store.id assert store.is_verified is False assert verified_store.store_code in message assert "unverified" in message def test_verify_store_not_found(self, db): """Test verify store when store not found""" with pytest.raises(StoreNotFoundException) as exc_info: self.service.verify_store(db, 99999) exception = exc_info.value assert exception.error_code == "STORE_NOT_FOUND" assert "99999" in exception.message def test_toggle_store_status_deactivate(self, db, test_store): """Test deactivating a store""" original_status = test_store.is_active store, message = self.service.toggle_store_status(db, test_store.id) assert store.id == test_store.id assert store.is_active != original_status assert test_store.store_code in message if original_status: assert "deactivated" in message else: assert "activated" in message def test_toggle_store_status_not_found(self, db): """Test toggle store status when store not found""" with pytest.raises(StoreNotFoundException) as exc_info: self.service.toggle_store_status(db, 99999) exception = exc_info.value assert exception.error_code == "STORE_NOT_FOUND" # NOTE: Marketplace Import Jobs tests have been moved to the marketplace module. # See tests/unit/services/test_marketplace_import_job_service.py # Statistics Tests def test_get_user_statistics(self, db, test_user, test_admin): """Test getting user statistics""" stats = stats_service.get_user_statistics(db) assert "total_users" in stats assert "active_users" in stats assert "inactive_users" in stats assert "activation_rate" in stats assert isinstance(stats["total_users"], int) assert isinstance(stats["active_users"], int) assert isinstance(stats["inactive_users"], int) assert isinstance(stats["activation_rate"], int | float) assert stats["total_users"] >= 2 # test_user + test_admin assert stats["active_users"] + stats["inactive_users"] == stats["total_users"] def test_get_store_statistics(self, db, test_store): """Test getting store statistics""" stats = stats_service.get_store_statistics(db) assert "total" in stats assert "verified" in stats assert "pending" in stats assert "inactive" in stats assert "verification_rate" in stats assert isinstance(stats["total"], int) assert isinstance(stats["verified"], int) assert isinstance(stats["verification_rate"], int | float) assert stats["total"] >= 1 # Error Handling Tests def test_get_all_users_database_error(self, db_with_error, test_admin): """Test handling database errors in get_all_users""" with pytest.raises(AdminOperationException) as exc_info: self.service.get_all_users(db_with_error, skip=0, limit=10) exception = exc_info.value assert exception.error_code == "ADMIN_OPERATION_FAILED" assert "get_all_users" in exception.message def test_get_all_stores_database_error(self, db_with_error): """Test handling database errors in get_all_stores""" with pytest.raises(AdminOperationException) as exc_info: self.service.get_all_stores(db_with_error, skip=0, limit=10) exception = exc_info.value assert exception.error_code == "ADMIN_OPERATION_FAILED" assert "get_all_stores" in exception.message # Edge Cases def test_get_all_users_empty_database(self, empty_db): """Test getting users when database is empty""" users = self.service.get_all_users(empty_db, skip=0, limit=10) assert len(users) == 0 def test_get_all_stores_empty_database(self, empty_db): """Test getting stores when database is empty""" stores, total = self.service.get_all_stores(empty_db, skip=0, limit=10) assert len(stores) == 0 assert total == 0 def test_user_statistics_empty_database(self, empty_db): """Test user statistics when no users exist""" stats = stats_service.get_user_statistics(empty_db) assert stats["total_users"] == 0 assert stats["active_users"] == 0 assert stats["inactive_users"] == 0 assert stats["activation_rate"] == 0 def test_store_statistics_empty_database(self, empty_db): """Test store statistics when no stores exist""" stats = stats_service.get_store_statistics(empty_db) assert stats["total"] == 0 assert stats["verified"] == 0 assert stats["inactive"] == 0 assert stats["verification_rate"] == 0 @pytest.mark.unit @pytest.mark.admin class TestAdminServiceStoreCreation: """Test suite for AdminService.create_store with platform assignments.""" def setup_method(self): """Setup method following the same pattern as other tests.""" self.service = AdminService() def test_create_store_without_platforms(self, db, test_merchant): """Test creating a store without platform assignments.""" import uuid unique_id = str(uuid.uuid4())[:8] store_data = StoreCreate( merchant_id=test_merchant.id, store_code=f"NOPLATFORM_{unique_id}", subdomain=f"noplatform{unique_id}", name=f"No Platform Store {unique_id}", ) store = self.service.create_store(db, store_data) db.commit() assert store is not None assert store.store_code == store_data.store_code.upper() assert store.merchant_id == test_merchant.id assert store.is_active is True def test_create_store_with_single_platform( self, db, test_merchant, test_platform ): """Test creating a store with one platform assignment.""" import uuid unique_id = str(uuid.uuid4())[:8] store_data = StoreCreate( merchant_id=test_merchant.id, store_code=f"SINGLEPLAT_{unique_id}", subdomain=f"singleplat{unique_id}", name=f"Single Platform Store {unique_id}", platform_ids=[test_platform.id], ) store = self.service.create_store(db, store_data) db.commit() db.refresh(store) assert store is not None assert store.store_code == store_data.store_code.upper() # Verify platform assignment from app.modules.tenancy.models import StorePlatform assignment = ( db.query(StorePlatform) .filter( StorePlatform.store_id == store.id, StorePlatform.platform_id == test_platform.id, ) .first() ) assert assignment is not None assert assignment.is_active is True def test_create_store_with_multiple_platforms( self, db, test_merchant, test_platform, another_platform ): """Test creating a store with multiple platform assignments.""" import uuid unique_id = str(uuid.uuid4())[:8] store_data = StoreCreate( merchant_id=test_merchant.id, store_code=f"MULTIPLAT_{unique_id}", subdomain=f"multiplat{unique_id}", name=f"Multi Platform Store {unique_id}", platform_ids=[test_platform.id, another_platform.id], ) store = self.service.create_store(db, store_data) db.commit() db.refresh(store) assert store is not None # Verify both platform assignments from app.modules.tenancy.models import StorePlatform assignments = ( db.query(StorePlatform) .filter(StorePlatform.store_id == store.id) .all() ) assert len(assignments) == 2 platform_ids = [a.platform_id for a in assignments] assert test_platform.id in platform_ids assert another_platform.id in platform_ids def test_create_store_with_invalid_platform_id(self, db, test_merchant): """Test creating a store with non-existent platform ID (should ignore).""" import uuid unique_id = str(uuid.uuid4())[:8] store_data = StoreCreate( merchant_id=test_merchant.id, store_code=f"INVALIDPLAT_{unique_id}", subdomain=f"invalidplat{unique_id}", name=f"Invalid Platform Store {unique_id}", platform_ids=[99999], # Non-existent platform ) # Should succeed but not create assignment for invalid platform store = self.service.create_store(db, store_data) db.commit() db.refresh(store) assert store is not None # Verify no platform assignments created from app.modules.tenancy.models import StorePlatform assignments = ( db.query(StorePlatform) .filter(StorePlatform.store_id == store.id) .all() ) assert len(assignments) == 0 def test_create_store_duplicate_code_fails(self, db, test_merchant, test_store): """Test creating a store with duplicate store code fails.""" store_data = StoreCreate( merchant_id=test_merchant.id, store_code=test_store.store_code, # Duplicate subdomain="uniquesubdomain", name="Duplicate Code Store", ) with pytest.raises(StoreAlreadyExistsException): self.service.create_store(db, store_data) def test_create_store_duplicate_subdomain_fails(self, db, test_merchant, test_store): """Test creating a store with duplicate subdomain fails.""" import uuid unique_id = str(uuid.uuid4())[:8] store_data = StoreCreate( merchant_id=test_merchant.id, store_code=f"UNIQUECODE_{unique_id}", subdomain=test_store.subdomain, # Duplicate name="Duplicate Subdomain Store", ) with pytest.raises(ValidationException) as exc_info: self.service.create_store(db, store_data) assert "already taken" in str(exc_info.value) def test_create_store_invalid_merchant_fails(self, db): """Test creating a store with non-existent merchant fails.""" import uuid unique_id = str(uuid.uuid4())[:8] store_data = StoreCreate( merchant_id=99999, # Non-existent store_code=f"NOMERCHANT_{unique_id}", subdomain=f"nomerchant{unique_id}", name="No Merchant Store", ) with pytest.raises(MerchantNotFoundException) as exc_info: self.service.create_store(db, store_data) assert "not found" in str(exc_info.value)