# tests/test_shop_service.py (updated to use custom exceptions) import pytest from app.services.shop_service import ShopService from app.exceptions import ( ShopNotFoundException, ShopAlreadyExistsException, UnauthorizedShopAccessException, InvalidShopDataException, ProductNotFoundException, ShopProductAlreadyExistsException, MaxShopsReachedException, ValidationException, ) from models.schemas.shop import ShopCreate, ShopProductCreate @pytest.mark.unit @pytest.mark.shops class TestShopService: """Test suite for ShopService following the application's exception patterns""" def setup_method(self): """Setup method following the same pattern as admin service tests""" self.service = ShopService() def test_create_shop_success(self, db, test_user, shop_factory): """Test successful shop creation""" shop_data = ShopCreate( shop_code="NEWSHOP", shop_name="New Test Shop", description="A new test shop", ) shop = self.service.create_shop(db, shop_data, test_user) assert shop is not None assert shop.shop_code == "NEWSHOP" assert shop.owner_id == test_user.id assert shop.is_verified is False # Regular user creates unverified shop def test_create_shop_admin_auto_verify(self, db, test_admin, shop_factory): """Test admin creates verified shop automatically""" shop_data = ShopCreate(shop_code="ADMINSHOP", shop_name="Admin Test Shop") shop = self.service.create_shop(db, shop_data, test_admin) assert shop.is_verified is True # Admin creates verified shop def test_create_shop_duplicate_code(self, db, test_user, test_shop): """Test shop creation fails with duplicate shop code""" shop_data = ShopCreate( shop_code=test_shop.shop_code, shop_name=test_shop.shop_name ) with pytest.raises(ShopAlreadyExistsException) as exc_info: self.service.create_shop(db, shop_data, test_user) exception = exc_info.value assert exception.status_code == 409 assert exception.error_code == "SHOP_ALREADY_EXISTS" assert test_shop.shop_code.upper() in exception.message assert "shop_code" in exception.details def test_create_shop_invalid_data_empty_code(self, db, test_user): """Test shop creation fails with empty shop code""" shop_data = ShopCreate(shop_code="", shop_name="Test Shop") with pytest.raises(InvalidShopDataException) as exc_info: self.service.create_shop(db, shop_data, test_user) exception = exc_info.value assert exception.status_code == 422 assert exception.error_code == "INVALID_SHOP_DATA" assert exception.details["field"] == "shop_code" def test_create_shop_invalid_data_empty_name(self, db, test_user): """Test shop creation fails with empty shop name""" shop_data = ShopCreate(shop_code="VALIDCODE", shop_name="") with pytest.raises(InvalidShopDataException) as exc_info: self.service.create_shop(db, shop_data, test_user) exception = exc_info.value assert exception.error_code == "INVALID_SHOP_DATA" assert exception.details["field"] == "shop_name" def test_create_shop_invalid_code_format(self, db, test_user): """Test shop creation fails with invalid shop code format""" shop_data = ShopCreate(shop_code="INVALID@CODE!", shop_name="Test Shop") with pytest.raises(InvalidShopDataException) as exc_info: self.service.create_shop(db, shop_data, test_user) exception = exc_info.value assert exception.error_code == "INVALID_SHOP_DATA" assert exception.details["field"] == "shop_code" assert "letters, numbers, underscores, and hyphens" in exception.message def test_create_shop_max_shops_reached(self, db, test_user, monkeypatch): """Test shop creation fails when user reaches maximum shops""" # Mock the shop count check to simulate user at limit def mock_check_shop_limit(self, db, user): raise MaxShopsReachedException(max_shops=5, user_id=user.id) monkeypatch.setattr(ShopService, "_check_shop_limit", mock_check_shop_limit) shop_data = ShopCreate(shop_code="NEWSHOP", shop_name="New Shop") with pytest.raises(MaxShopsReachedException) as exc_info: self.service.create_shop(db, shop_data, test_user) exception = exc_info.value assert exception.status_code == 400 assert exception.error_code == "MAX_SHOPS_REACHED" assert exception.details["max_shops"] == 5 assert exception.details["user_id"] == test_user.id def test_get_shops_regular_user(self, db, test_user, test_shop, inactive_shop): """Test regular user can only see active verified shops and own shops""" shops, total = self.service.get_shops(db, test_user, skip=0, limit=10) shop_codes = [shop.shop_code for shop in shops] assert test_shop.shop_code in shop_codes assert inactive_shop.shop_code not in shop_codes def test_get_shops_admin_user( self, db, test_admin, test_shop, inactive_shop, verified_shop ): """Test admin user can see all shops with filters""" shops, total = self.service.get_shops( db, test_admin, active_only=False, verified_only=False ) shop_codes = [shop.shop_code for shop in shops] assert test_shop.shop_code in shop_codes assert inactive_shop.shop_code in shop_codes assert verified_shop.shop_code in shop_codes def test_get_shop_by_code_owner_access(self, db, test_user, test_shop): """Test shop owner can access their own shop""" shop = self.service.get_shop_by_code(db, test_shop.shop_code.lower(), test_user) assert shop is not None assert shop.id == test_shop.id def test_get_shop_by_code_admin_access(self, db, test_admin, test_shop): """Test admin can access any shop""" shop = self.service.get_shop_by_code( db, test_shop.shop_code.lower(), test_admin ) assert shop is not None assert shop.id == test_shop.id def test_get_shop_by_code_not_found(self, db, test_user): """Test shop not found raises proper exception""" with pytest.raises(ShopNotFoundException) as exc_info: self.service.get_shop_by_code(db, "NONEXISTENT", test_user) exception = exc_info.value assert exception.status_code == 404 assert exception.error_code == "SHOP_NOT_FOUND" assert exception.details["resource_type"] == "Shop" assert exception.details["identifier"] == "NONEXISTENT" def test_get_shop_by_code_access_denied(self, db, test_user, inactive_shop): """Test regular user cannot access unverified shop they don't own""" with pytest.raises(UnauthorizedShopAccessException) as exc_info: self.service.get_shop_by_code(db, inactive_shop.shop_code, test_user) exception = exc_info.value assert exception.status_code == 403 assert exception.error_code == "UNAUTHORIZED_SHOP_ACCESS" assert exception.details["shop_code"] == inactive_shop.shop_code assert exception.details["user_id"] == test_user.id def test_add_product_to_shop_success(self, db, test_shop, unique_product): """Test successfully adding product to shop""" shop_product_data = ShopProductCreate( product_id=unique_product.product_id, price="15.99", is_featured=True, stock_quantity=5, ) shop_product = self.service.add_product_to_shop( db, test_shop, shop_product_data ) assert shop_product is not None assert shop_product.shop_id == test_shop.id assert shop_product.product_id == unique_product.id def test_add_product_to_shop_product_not_found(self, db, test_shop): """Test adding non-existent product to shop fails""" shop_product_data = ShopProductCreate(product_id="NONEXISTENT", price="15.99") with pytest.raises(ProductNotFoundException) as exc_info: self.service.add_product_to_shop(db, test_shop, shop_product_data) exception = exc_info.value assert exception.status_code == 404 assert exception.error_code == "PRODUCT_NOT_FOUND" assert exception.details["resource_type"] == "Product" assert exception.details["identifier"] == "NONEXISTENT" def test_add_product_to_shop_already_exists(self, db, test_shop, shop_product): """Test adding product that's already in shop fails""" shop_product_data = ShopProductCreate( product_id=shop_product.product.product_id, price="15.99" ) with pytest.raises(ShopProductAlreadyExistsException) as exc_info: self.service.add_product_to_shop(db, test_shop, shop_product_data) exception = exc_info.value assert exception.status_code == 409 assert exception.error_code == "SHOP_PRODUCT_ALREADY_EXISTS" assert exception.details["shop_code"] == test_shop.shop_code assert exception.details["product_id"] == shop_product.product.product_id def test_get_shop_products_owner_access( self, db, test_user, test_shop, shop_product ): """Test shop owner can get shop products""" products, total = self.service.get_shop_products(db, test_shop, test_user) assert total >= 1 assert len(products) >= 1 product_ids = [p.product_id for p in products] assert shop_product.product_id in product_ids def test_get_shop_products_access_denied(self, db, test_user, inactive_shop): """Test non-owner cannot access unverified shop products""" with pytest.raises(UnauthorizedShopAccessException) as exc_info: self.service.get_shop_products(db, inactive_shop, test_user) exception = exc_info.value assert exception.status_code == 403 assert exception.error_code == "UNAUTHORIZED_SHOP_ACCESS" assert exception.details["shop_code"] == inactive_shop.shop_code assert exception.details["user_id"] == test_user.id def test_get_shop_products_with_filters(self, db, test_user, test_shop, shop_product): """Test getting shop products with various filters""" # Test active only filter products, total = self.service.get_shop_products( db, test_shop, test_user, active_only=True ) assert all(p.is_active for p in products) # Test featured only filter products, total = self.service.get_shop_products( db, test_shop, test_user, featured_only=True ) assert all(p.is_featured for p in products) # Test exception handling for generic errors def test_create_shop_database_error(self, db, test_user, monkeypatch): """Test shop creation handles database errors gracefully""" def mock_commit(): raise Exception("Database connection failed") monkeypatch.setattr(db, "commit", mock_commit) shop_data = ShopCreate(shop_code="NEWSHOP", shop_name="Test Shop") with pytest.raises(ValidationException) as exc_info: self.service.create_shop(db, shop_data, test_user) exception = exc_info.value assert exception.status_code == 422 assert exception.error_code == "VALIDATION_ERROR" assert "Failed to create shop" in exception.message def test_get_shops_database_error(self, db, test_user, monkeypatch): """Test get shops handles database errors gracefully""" def mock_query(*args): raise Exception("Database query failed") monkeypatch.setattr(db, "query", mock_query) with pytest.raises(ValidationException) as exc_info: self.service.get_shops(db, test_user) exception = exc_info.value assert exception.error_code == "VALIDATION_ERROR" assert "Failed to retrieve shops" in exception.message def test_add_product_database_error(self, db, test_shop, unique_product, monkeypatch): """Test add product handles database errors gracefully""" def mock_commit(): raise Exception("Database commit failed") monkeypatch.setattr(db, "commit", mock_commit) shop_product_data = ShopProductCreate( product_id=unique_product.product_id, price="15.99" ) with pytest.raises(ValidationException) as exc_info: self.service.add_product_to_shop(db, test_shop, shop_product_data) exception = exc_info.value assert exception.error_code == "VALIDATION_ERROR" assert "Failed to add product to shop" in exception.message @pytest.mark.unit @pytest.mark.shops class TestShopServiceExceptionDetails: """Additional tests focusing specifically on exception structure and details""" def setup_method(self): self.service = ShopService() def test_exception_to_dict_structure(self, db, test_user, test_shop): """Test that exceptions can be properly serialized to dict for API responses""" shop_data = ShopCreate( shop_code=test_shop.shop_code, shop_name="Duplicate" ) with pytest.raises(ShopAlreadyExistsException) as exc_info: self.service.create_shop(db, shop_data, test_user) exception = exc_info.value exception_dict = exception.to_dict() # Verify structure matches expected API response format assert "error_code" in exception_dict assert "message" in exception_dict assert "status_code" in exception_dict assert "details" in exception_dict # Verify values assert exception_dict["error_code"] == "SHOP_ALREADY_EXISTS" assert exception_dict["status_code"] == 409 assert isinstance(exception_dict["details"], dict) def test_validation_exception_field_details(self, db, test_user): """Test validation exceptions include field-specific details""" shop_data = ShopCreate(shop_code="", shop_name="Test") with pytest.raises(InvalidShopDataException) as exc_info: self.service.create_shop(db, shop_data, test_user) exception = exc_info.value assert exception.details["field"] == "shop_code" assert exception.status_code == 422 assert "required" in exception.message.lower() def test_authorization_exception_user_details(self, db, test_user, inactive_shop): """Test authorization exceptions include user context""" with pytest.raises(UnauthorizedShopAccessException) as exc_info: self.service.get_shop_by_code(db, inactive_shop.shop_code, test_user) exception = exc_info.value assert exception.details["shop_code"] == inactive_shop.shop_code assert exception.details["user_id"] == test_user.id assert "Unauthorized access" in exception.message