# tests/test_product_service.py import pytest from app.exceptions import ( InvalidMarketplaceProductDataException, MarketplaceProductAlreadyExistsException, MarketplaceProductNotFoundException, MarketplaceProductValidationException, ) from app.services.marketplace_product_service import MarketplaceProductService from models.schema.marketplace_product import ( MarketplaceProductCreate, MarketplaceProductUpdate, ) @pytest.mark.unit @pytest.mark.products class TestProductService: def setup_method(self): self.service = MarketplaceProductService() def test_create_product_success(self, db): """Test successful product creation with valid data""" product_data = MarketplaceProductCreate( marketplace_product_id="SVC001", title="Service Test MarketplaceProduct", gtin="1234567890123", price="19.99", marketplace="TestMarket", ) # Title is passed as separate parameter for translation table product = self.service.create_product( db, product_data, title="Service Test MarketplaceProduct" ) assert product.marketplace_product_id == "SVC001" assert product.get_title() == "Service Test MarketplaceProduct" assert product.gtin == "1234567890123" assert product.marketplace == "TestMarket" assert product.price == "19.99" # Price is stored as string after processing def test_create_product_invalid_gtin(self, db): """Test product creation with invalid GTIN raises InvalidMarketplaceProductDataException""" product_data = MarketplaceProductCreate( marketplace_product_id="SVC002", title="Service Test MarketplaceProduct", gtin="invalid_gtin", price="19.99", ) with pytest.raises(InvalidMarketplaceProductDataException) as exc_info: self.service.create_product(db, product_data) assert exc_info.value.error_code == "INVALID_PRODUCT_DATA" assert "Invalid GTIN format" in str(exc_info.value) assert exc_info.value.status_code == 422 assert exc_info.value.details.get("field") == "gtin" def test_create_product_missing_product_id(self, db): """Test product creation without marketplace_product_id raises MarketplaceProductValidationException""" product_data = MarketplaceProductCreate( marketplace_product_id="", # Empty product ID title="Service Test MarketplaceProduct", price="19.99", ) with pytest.raises(MarketplaceProductValidationException) as exc_info: self.service.create_product(db, product_data) assert exc_info.value.error_code == "PRODUCT_VALIDATION_FAILED" assert "MarketplaceProduct ID is required" in str(exc_info.value) assert exc_info.value.details.get("field") == "marketplace_product_id" def test_create_product_without_title(self, db): """Test product creation without title succeeds (title is optional, stored in translations)""" product_data = MarketplaceProductCreate( marketplace_product_id="SVC003", title="", # Empty title - allowed since translations are optional price="19.99", ) product = self.service.create_product(db, product_data) # Product is created but title returns None since no translation assert product.marketplace_product_id == "SVC003" assert product.get_title() is None # No translation created for empty title def test_create_product_already_exists(self, db, test_marketplace_product): """Test creating product with existing ID raises MarketplaceProductAlreadyExistsException""" # Store the product ID before the exception (session may be invalid after) existing_product_id = test_marketplace_product.marketplace_product_id product_data = MarketplaceProductCreate( marketplace_product_id=existing_product_id, # Use existing product ID title="Duplicate MarketplaceProduct", price="29.99", ) with pytest.raises(MarketplaceProductAlreadyExistsException) as exc_info: self.service.create_product(db, product_data) # Rollback to clear the session's invalid state from IntegrityError db.rollback() assert exc_info.value.error_code == "PRODUCT_ALREADY_EXISTS" assert existing_product_id in str(exc_info.value) assert exc_info.value.status_code == 409 assert ( exc_info.value.details.get("marketplace_product_id") == existing_product_id ) def test_create_product_invalid_price(self, db): """Test product creation with invalid price raises InvalidMarketplaceProductDataException""" product_data = MarketplaceProductCreate( marketplace_product_id="SVC004", title="Service Test MarketplaceProduct", price="invalid_price", ) with pytest.raises(InvalidMarketplaceProductDataException) as exc_info: self.service.create_product(db, product_data) assert exc_info.value.error_code == "INVALID_PRODUCT_DATA" assert "Invalid price format" in str(exc_info.value) assert exc_info.value.details.get("field") == "price" def test_get_product_by_id_or_raise_success(self, db, test_marketplace_product): """Test successful product retrieval by ID""" product = self.service.get_product_by_id_or_raise( db, test_marketplace_product.marketplace_product_id ) assert ( product.marketplace_product_id == test_marketplace_product.marketplace_product_id ) assert product.get_title() == test_marketplace_product.get_title() def test_get_product_by_id_or_raise_not_found(self, db): """Test product retrieval with non-existent ID raises MarketplaceProductNotFoundException""" with pytest.raises(MarketplaceProductNotFoundException) as exc_info: self.service.get_product_by_id_or_raise(db, "NONEXISTENT") assert exc_info.value.error_code == "PRODUCT_NOT_FOUND" assert "NONEXISTENT" in str(exc_info.value) assert exc_info.value.status_code == 404 assert exc_info.value.details.get("resource_type") == "MarketplaceProduct" assert exc_info.value.details.get("identifier") == "NONEXISTENT" def test_get_products_with_filters_success(self, db, test_marketplace_product): """Test getting products with various filters""" products, total = self.service.get_products_with_filters( db, brand=test_marketplace_product.brand ) assert total == 1 assert len(products) == 1 assert products[0].brand == test_marketplace_product.brand def test_get_products_with_search(self, db, test_marketplace_product): """Test getting products with search term""" products, total = self.service.get_products_with_filters( db, search="Test MarketplaceProduct" ) assert total >= 1 assert len(products) >= 1 # Verify search worked by checking that title contains search term found_product = next( ( p for p in products if p.marketplace_product_id == test_marketplace_product.marketplace_product_id ), None, ) assert found_product is not None def test_update_product_success(self, db, test_marketplace_product): """Test successful product update""" update_data = MarketplaceProductUpdate(price="39.99") # Title is passed as separate parameter for translation table updated_product = self.service.update_product( db, test_marketplace_product.marketplace_product_id, update_data, title="Updated MarketplaceProduct Title", ) assert updated_product.get_title() == "Updated MarketplaceProduct Title" assert ( updated_product.price == "39.99" ) # Price is stored as string after processing assert ( updated_product.marketplace_product_id == test_marketplace_product.marketplace_product_id ) # ID unchanged def test_update_product_not_found(self, db): """Test updating non-existent product raises MarketplaceProductNotFoundException""" update_data = MarketplaceProductUpdate(title="Updated Title") with pytest.raises(MarketplaceProductNotFoundException) as exc_info: self.service.update_product(db, "NONEXISTENT", update_data) assert exc_info.value.error_code == "PRODUCT_NOT_FOUND" assert "NONEXISTENT" in str(exc_info.value) def test_update_product_invalid_gtin(self, db, test_marketplace_product): """Test updating product with invalid GTIN raises InvalidMarketplaceProductDataException""" update_data = MarketplaceProductUpdate(gtin="invalid_gtin") with pytest.raises(InvalidMarketplaceProductDataException) as exc_info: self.service.update_product( db, test_marketplace_product.marketplace_product_id, update_data ) assert exc_info.value.error_code == "INVALID_PRODUCT_DATA" assert "Invalid GTIN format" in str(exc_info.value) assert exc_info.value.details.get("field") == "gtin" def test_update_product_empty_title_preserves_existing( self, db, test_marketplace_product ): """Test updating product with empty title preserves existing title in translation""" original_title = test_marketplace_product.get_title() update_data = MarketplaceProductUpdate(title="") updated_product = self.service.update_product( db, test_marketplace_product.marketplace_product_id, update_data ) # Empty title update preserves existing translation title assert updated_product.get_title() == original_title def test_update_product_invalid_price(self, db, test_marketplace_product): """Test updating product with invalid price raises InvalidMarketplaceProductDataException""" update_data = MarketplaceProductUpdate(price="invalid_price") with pytest.raises(InvalidMarketplaceProductDataException) as exc_info: self.service.update_product( db, test_marketplace_product.marketplace_product_id, update_data ) assert exc_info.value.error_code == "INVALID_PRODUCT_DATA" assert "Invalid price format" in str(exc_info.value) assert exc_info.value.details.get("field") == "price" def test_delete_product_success(self, db, test_marketplace_product): """Test successful product deletion""" result = self.service.delete_product( db, test_marketplace_product.marketplace_product_id ) assert result is True # Verify product is deleted deleted_product = self.service.get_product_by_id( db, test_marketplace_product.marketplace_product_id ) assert deleted_product is None def test_delete_product_not_found(self, db): """Test deleting non-existent product raises MarketplaceProductNotFoundException""" with pytest.raises(MarketplaceProductNotFoundException) as exc_info: self.service.delete_product(db, "NONEXISTENT") assert exc_info.value.error_code == "PRODUCT_NOT_FOUND" assert "NONEXISTENT" in str(exc_info.value) def test_get_inventory_info_success( self, db, test_marketplace_product_with_inventory ): """Test getting inventory info for product with inventory.""" marketplace_product = test_marketplace_product_with_inventory[ "marketplace_product" ] inventory = test_marketplace_product_with_inventory["inventory"] inventory_info = self.service.get_inventory_info(db, marketplace_product.gtin) assert inventory_info is not None assert inventory_info.total_quantity == inventory.quantity assert len(inventory_info.locations) >= 1 def test_get_inventory_info_no_inventory(self, db, test_marketplace_product): """Test getting inventory info for product without inventory""" inventory_info = self.service.get_inventory_info( db, test_marketplace_product.gtin or "1234567890123" ) assert inventory_info is None def test_product_exists_true(self, db, test_marketplace_product): """Test product_exists returns True for existing product""" exists = self.service.product_exists( db, test_marketplace_product.marketplace_product_id ) assert exists is True def test_product_exists_false(self, db): """Test product_exists returns False for non-existent product""" exists = self.service.product_exists(db, "NONEXISTENT") assert exists is False def test_generate_csv_export_success(self, db, test_marketplace_product): """Test CSV export generation""" csv_generator = self.service.generate_csv_export(db) # Convert generator to list to test content csv_lines = list(csv_generator) assert len(csv_lines) > 1 # Header + at least one data row assert csv_lines[0].startswith( "marketplace_product_id,title,description" ) # Check header # Check that test product appears in CSV csv_content = "".join(csv_lines) assert test_marketplace_product.marketplace_product_id in csv_content def test_generate_csv_export_with_filters(self, db, test_marketplace_product): """Test CSV export with marketplace filter""" csv_generator = self.service.generate_csv_export( db, marketplace=test_marketplace_product.marketplace ) csv_lines = list(csv_generator) assert len(csv_lines) >= 1 # At least header if len(csv_lines) > 1: # If there's data csv_content = "".join(csv_lines) assert test_marketplace_product.marketplace in csv_content @pytest.mark.unit @pytest.mark.products class TestMarketplaceProductServiceAdmin: """Tests for admin-specific methods in MarketplaceProductService.""" def setup_method(self): self.service = MarketplaceProductService() def test_get_admin_products_success(self, db, test_marketplace_product): """Test getting admin products list.""" products, total = self.service.get_admin_products(db) assert total >= 1 assert len(products) >= 1 # Find our test product in results found = False for p in products: if ( p["marketplace_product_id"] == test_marketplace_product.marketplace_product_id ): found = True assert p["id"] == test_marketplace_product.id assert p["marketplace"] == test_marketplace_product.marketplace break assert found, "Test product not found in results" def test_get_admin_products_with_search(self, db, test_marketplace_product): """Test getting admin products with search filter.""" products, total = self.service.get_admin_products( db, search="Test MarketplaceProduct" ) assert total >= 1 # Should find our test product product_ids = [p["marketplace_product_id"] for p in products] assert test_marketplace_product.marketplace_product_id in product_ids def test_get_admin_products_with_marketplace_filter( self, db, test_marketplace_product ): """Test getting admin products with marketplace filter.""" products, total = self.service.get_admin_products( db, marketplace=test_marketplace_product.marketplace ) assert total >= 1 # All products should be from the filtered marketplace for p in products: assert p["marketplace"] == test_marketplace_product.marketplace def test_get_admin_products_pagination(self, db, multiple_products): """Test admin products pagination.""" # Get first 2 products, total = self.service.get_admin_products(db, skip=0, limit=2) assert total >= 5 # We created 5 products assert len(products) == 2 # Get next 2 products2, _ = self.service.get_admin_products(db, skip=2, limit=2) assert len(products2) == 2 # Make sure they're different ids1 = {p["id"] for p in products} ids2 = {p["id"] for p in products2} assert ids1.isdisjoint(ids2) def test_get_admin_product_stats(self, db, test_marketplace_product): """Test getting admin product statistics.""" stats = self.service.get_admin_product_stats(db) assert "total" in stats assert "active" in stats assert "inactive" in stats assert "digital" in stats assert "physical" in stats assert "by_marketplace" in stats assert stats["total"] >= 1 def test_get_marketplaces_list(self, db, test_marketplace_product): """Test getting list of marketplaces.""" marketplaces = self.service.get_marketplaces_list(db) assert isinstance(marketplaces, list) assert test_marketplace_product.marketplace in marketplaces def test_get_source_vendors_list(self, db, test_marketplace_product): """Test getting list of source vendors.""" vendors = self.service.get_source_vendors_list(db) assert isinstance(vendors, list) assert test_marketplace_product.vendor_name in vendors def test_get_admin_product_detail(self, db, test_marketplace_product): """Test getting admin product detail by ID.""" product = self.service.get_admin_product_detail(db, test_marketplace_product.id) assert product["id"] == test_marketplace_product.id assert ( product["marketplace_product_id"] == test_marketplace_product.marketplace_product_id ) assert product["marketplace"] == test_marketplace_product.marketplace assert "translations" in product def test_get_admin_product_detail_not_found(self, db): """Test getting non-existent product detail raises exception.""" with pytest.raises(MarketplaceProductNotFoundException): self.service.get_admin_product_detail(db, 99999) def test_copy_to_vendor_catalog_success( self, db, test_marketplace_product, test_vendor ): """Test copying products to vendor catalog.""" result = self.service.copy_to_vendor_catalog( db, marketplace_product_ids=[test_marketplace_product.id], vendor_id=test_vendor.id, ) assert result["copied"] == 1 assert result["skipped"] == 0 assert result["failed"] == 0 def test_copy_to_vendor_catalog_skip_existing( self, db, test_marketplace_product, test_vendor ): """Test copying products that already exist skips them.""" # First copy result1 = self.service.copy_to_vendor_catalog( db, marketplace_product_ids=[test_marketplace_product.id], vendor_id=test_vendor.id, ) assert result1["copied"] == 1 # Second copy should skip result2 = self.service.copy_to_vendor_catalog( db, marketplace_product_ids=[test_marketplace_product.id], vendor_id=test_vendor.id, skip_existing=True, ) assert result2["copied"] == 0 assert result2["skipped"] == 1 def test_copy_to_vendor_catalog_invalid_vendor(self, db, test_marketplace_product): """Test copying to non-existent vendor raises exception.""" from app.exceptions import VendorNotFoundException with pytest.raises(VendorNotFoundException): self.service.copy_to_vendor_catalog( db, marketplace_product_ids=[test_marketplace_product.id], vendor_id=99999, ) def test_copy_to_vendor_catalog_invalid_products(self, db, test_vendor): """Test copying non-existent products raises exception.""" with pytest.raises(MarketplaceProductNotFoundException): self.service.copy_to_vendor_catalog( db, marketplace_product_ids=[99999], vendor_id=test_vendor.id, )