# tests/test_vendor_service.py (updated to use custom exceptions) import pytest from app.exceptions import ( InvalidVendorDataException, MarketplaceProductNotFoundException, MaxVendorsReachedException, ProductAlreadyExistsException, UnauthorizedVendorAccessException, ValidationException, VendorAlreadyExistsException, VendorNotFoundException, ) from app.services.vendor_service import VendorService from models.schema.product import ProductCreate from models.schema.vendor import VendorCreate @pytest.mark.unit @pytest.mark.vendors class TestVendorService: """Test suite for VendorService following the application's exception patterns""" def setup_method(self): """Setup method following the same pattern as admin service tests""" self.service = VendorService() def test_create_vendor_success(self, db, test_user, vendor_factory): """Test successful vendor creation""" vendor_data = VendorCreate( vendor_code="NEWVENDOR", vendor_name="New Test Vendor", description="A new test vendor ", ) vendor = self.service.create_vendor(db, vendor_data, test_user) assert vendor is not None assert vendor.vendor_code == "NEWVENDOR" assert vendor.owner_user_id == test_user.id assert vendor.is_verified is False # Regular user creates unverified vendor def test_create_vendor_admin_auto_verify(self, db, test_admin, vendor_factory): """Test admin creates verified vendor automatically""" vendor_data = VendorCreate( vendor_code="ADMINVENDOR", vendor_name="Admin Test Vendor" ) vendor = self.service.create_vendor(db, vendor_data, test_admin) assert vendor.is_verified is True # Admin creates verified vendor def test_create_vendor_duplicate_code(self, db, test_user, test_vendor): """Test vendor creation fails with duplicate vendor code""" vendor_data = VendorCreate( vendor_code=test_vendor.vendor_code, vendor_name=test_vendor.name ) with pytest.raises(VendorAlreadyExistsException) as exc_info: self.service.create_vendor(db, vendor_data, test_user) exception = exc_info.value assert exception.status_code == 409 assert exception.error_code == "VENDOR_ALREADY_EXISTS" assert test_vendor.vendor_code.upper() in exception.message assert "vendor_code" in exception.details def test_create_vendor_invalid_data_empty_code(self, db, test_user): """Test vendor creation fails with empty vendor code""" vendor_data = VendorCreate(vendor_code="", vendor_name="Test Vendor") with pytest.raises(InvalidVendorDataException) as exc_info: self.service.create_vendor(db, vendor_data, test_user) exception = exc_info.value assert exception.status_code == 422 assert exception.error_code == "INVALID_VENDOR_DATA" assert exception.details["field"] == "vendor_code" def test_create_vendor_invalid_data_empty_name(self, db, test_user): """Test vendor creation fails with empty vendor name""" vendor_data = VendorCreate(vendor_code="VALIDCODE", vendor_name="") with pytest.raises(InvalidVendorDataException) as exc_info: self.service.create_vendor(db, vendor_data, test_user) exception = exc_info.value assert exception.error_code == "INVALID_VENDOR_DATA" assert exception.details["field"] == "name" def test_create_vendor_invalid_code_format(self, db, test_user): """Test vendor creation fails with invalid vendor code format""" vendor_data = VendorCreate( vendor_code="INVALID@CODE!", vendor_name="Test Vendor" ) with pytest.raises(InvalidVendorDataException) as exc_info: self.service.create_vendor(db, vendor_data, test_user) exception = exc_info.value assert exception.error_code == "INVALID_VENDOR_DATA" assert exception.details["field"] == "vendor_code" assert "letters, numbers, underscores, and hyphens" in exception.message def test_create_vendor_max_vendors_reached(self, db, test_user, monkeypatch): """Test vendor creation fails when user reaches maximum vendors""" # Mock the vendor count check to simulate user at limit def mock_check_vendor_limit(self, db, user): raise MaxVendorsReachedException(max_vendors=5, user_id=user.id) monkeypatch.setattr( VendorService, "_check_vendor_limit", mock_check_vendor_limit ) vendor_data = VendorCreate(vendor_code="NEWVENDOR", vendor_name="New Vendor") with pytest.raises(MaxVendorsReachedException) as exc_info: self.service.create_vendor(db, vendor_data, test_user) exception = exc_info.value assert exception.status_code == 400 assert exception.error_code == "MAX_VENDORS_REACHED" assert exception.details["max_vendors"] == 5 assert exception.details["user_id"] == test_user.id def test_get_vendors_regular_user( self, db, test_user, test_vendor, inactive_vendor ): """Test regular user can only see active verified vendors and own vendors""" vendors, total = self.service.get_vendors(db, test_user, skip=0, limit=10) vendor_codes = [vendor.vendor_code for vendor in vendors] assert test_vendor.vendor_code in vendor_codes assert inactive_vendor.vendor_code not in vendor_codes def test_get_vendors_admin_user( self, db, test_admin, test_vendor, inactive_vendor, verified_vendor ): """Test admin user can see all vendors with filters""" vendors, total = self.service.get_vendors( db, test_admin, active_only=False, verified_only=False ) vendor_codes = [vendor.vendor_code for vendor in vendors] assert test_vendor.vendor_code in vendor_codes assert inactive_vendor.vendor_code in vendor_codes assert verified_vendor.vendor_code in vendor_codes def test_get_vendor_by_code_owner_access(self, db, test_user, test_vendor): """Test vendor owner can access their own vendor""" vendor = self.service.get_vendor_by_code( db, test_vendor.vendor_code.lower(), test_user ) assert vendor is not None assert vendor.id == test_vendor.id def test_get_vendor_by_code_admin_access(self, db, test_admin, test_vendor): """Test admin can access any vendor""" vendor = self.service.get_vendor_by_code( db, test_vendor.vendor_code.lower(), test_admin ) assert vendor is not None assert vendor.id == test_vendor.id def test_get_vendor_by_code_not_found(self, db, test_user): """Test vendor not found raises proper exception""" with pytest.raises(VendorNotFoundException) as exc_info: self.service.get_vendor_by_code(db, "NONEXISTENT", test_user) exception = exc_info.value assert exception.status_code == 404 assert exception.error_code == "VENDOR_NOT_FOUND" assert exception.details["resource_type"] == "Vendor" assert exception.details["identifier"] == "NONEXISTENT" def test_get_vendor_by_code_access_denied(self, db, test_user, inactive_vendor): """Test regular user cannot access unverified vendor they don't own""" with pytest.raises(UnauthorizedVendorAccessException) as exc_info: self.service.get_vendor_by_code(db, inactive_vendor.vendor_code, test_user) exception = exc_info.value assert exception.status_code == 403 assert exception.error_code == "UNAUTHORIZED_VENDOR_ACCESS" assert exception.details["vendor_code"] == inactive_vendor.vendor_code assert exception.details["user_id"] == test_user.id def test_add_product_to_vendor_success(self, db, test_vendor, unique_product): """Test successfully adding product to vendor""" product_data = ProductCreate( marketplace_product_id=unique_product.marketplace_product_id, price="15.99", is_featured=True, ) product = self.service.add_product_to_catalog(db, test_vendor, product_data) assert product is not None assert product.vendor_id == test_vendor.id assert product.marketplace_product_id == unique_product.id def test_add_product_to_vendor_product_not_found(self, db, test_vendor): """Test adding non-existent product to vendor fails""" product_data = ProductCreate( marketplace_product_id="NONEXISTENT", price="15.99" ) with pytest.raises(MarketplaceProductNotFoundException) as exc_info: self.service.add_product_to_catalog(db, test_vendor, product_data) exception = exc_info.value assert exception.status_code == 404 assert exception.error_code == "PRODUCT_NOT_FOUND" assert exception.details["resource_type"] == "MarketplaceProduct" assert exception.details["identifier"] == "NONEXISTENT" def test_add_product_to_vendor_already_exists(self, db, test_vendor, test_product): """Test adding product that's already in vendor fails""" product_data = ProductCreate( marketplace_product_id=test_product.marketplace_product.marketplace_product_id, price="15.99", ) with pytest.raises(ProductAlreadyExistsException) as exc_info: self.service.add_product_to_catalog(db, test_vendor, product_data) exception = exc_info.value assert exception.status_code == 409 assert exception.error_code == "PRODUCT_ALREADY_EXISTS" assert exception.details["vendor_code"] == test_vendor.vendor_code assert ( exception.details["marketplace_product_id"] == test_product.marketplace_product.marketplace_product_id ) def test_get_products_owner_access(self, db, test_user, test_vendor, test_product): """Test vendor owner can get vendor products""" products, total = self.service.get_products(db, test_vendor, test_user) assert total >= 1 assert len(products) >= 1 product_ids = [p.marketplace_product_id for p in products] assert test_product.marketplace_product_id in product_ids def test_get_products_access_denied(self, db, test_user, inactive_vendor): """Test non-owner cannot access unverified vendor products""" with pytest.raises(UnauthorizedVendorAccessException) as exc_info: self.service.get_products(db, inactive_vendor, test_user) exception = exc_info.value assert exception.status_code == 403 assert exception.error_code == "UNAUTHORIZED_VENDOR_ACCESS" assert exception.details["vendor_code"] == inactive_vendor.vendor_code assert exception.details["user_id"] == test_user.id def test_get_products_with_filters(self, db, test_user, test_vendor, test_product): """Test getting vendor products with various filters""" # Test active only filter products, total = self.service.get_products( db, test_vendor, test_user, active_only=True ) assert all(p.is_active for p in products) # Test featured only filter products, total = self.service.get_products( db, test_vendor, test_user, featured_only=True ) assert all(p.is_featured for p in products) # Test exception handling for generic errors def test_create_vendor_database_error(self, db, test_user, monkeypatch): """Test vendor creation handles database errors gracefully""" def mock_commit(): raise Exception("Database connection failed") monkeypatch.setattr(db, "commit", mock_commit) vendor_data = VendorCreate(vendor_code="NEWVENDOR", vendor_name="Test Vendor") with pytest.raises(ValidationException) as exc_info: self.service.create_vendor(db, vendor_data, test_user) exception = exc_info.value assert exception.status_code == 422 assert exception.error_code == "VALIDATION_ERROR" assert "Failed to create vendor " in exception.message def test_get_vendors_database_error(self, db, test_user, monkeypatch): """Test get vendors 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_vendors(db, test_user) exception = exc_info.value assert exception.error_code == "VALIDATION_ERROR" assert "Failed to retrieve vendors" in exception.message def test_add_product_database_error( self, db, test_vendor, unique_product, monkeypatch ): """Test add product handles database errors gracefully""" def mock_commit(): raise Exception("Database commit failed") monkeypatch.setattr(db, "commit", mock_commit) product_data = ProductCreate( marketplace_product_id=unique_product.marketplace_product_id, price="15.99" ) with pytest.raises(ValidationException) as exc_info: self.service.add_product_to_catalog(db, test_vendor, product_data) exception = exc_info.value assert exception.error_code == "VALIDATION_ERROR" assert "Failed to add product to vendor " in exception.message @pytest.mark.unit @pytest.mark.vendors class TestVendorServiceExceptionDetails: """Additional tests focusing specifically on exception structure and details""" def setup_method(self): self.service = VendorService() def test_exception_to_dict_structure(self, db, test_user, test_vendor): """Test that exceptions can be properly serialized to dict for API responses""" vendor_data = VendorCreate( vendor_code=test_vendor.vendor_code, vendor_name="Duplicate" ) with pytest.raises(VendorAlreadyExistsException) as exc_info: self.service.create_vendor(db, vendor_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"] == "VENDOR_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""" vendor_data = VendorCreate(vendor_code="", vendor_name="Test") with pytest.raises(InvalidVendorDataException) as exc_info: self.service.create_vendor(db, vendor_data, test_user) exception = exc_info.value assert exception.details["field"] == "vendor_code" assert exception.status_code == 422 assert "required" in exception.message.lower() def test_authorization_exception_user_details(self, db, test_user, inactive_vendor): """Test authorization exceptions include user context""" with pytest.raises(UnauthorizedVendorAccessException) as exc_info: self.service.get_vendor_by_code(db, inactive_vendor.vendor_code, test_user) exception = exc_info.value assert exception.details["vendor_code"] == inactive_vendor.vendor_code assert exception.details["user_id"] == test_user.id assert "Unauthorized access" in exception.message