# tests/unit/services/test_vendor_service.py """Unit tests for VendorService following the application's exception patterns.""" import uuid import pytest from app.exceptions import ( InvalidVendorDataException, MarketplaceProductNotFoundException, ProductAlreadyExistsException, UnauthorizedVendorAccessException, ValidationException, VendorAlreadyExistsException, VendorNotFoundException, ) from app.services.vendor_service import VendorService from models.database.company import Company from models.database.vendor import Vendor from models.schema.product import ProductCreate from models.schema.vendor import VendorCreate @pytest.fixture def admin_company(db, test_admin): """Create a test company for admin.""" unique_id = str(uuid.uuid4())[:8] company = Company( name=f"Admin Company {unique_id}", owner_user_id=test_admin.id, contact_email=f"admin{unique_id}@company.com", is_active=True, is_verified=True, ) db.add(company) db.commit() db.refresh(company) return company # Note: other_company fixture is defined in tests/fixtures/vendor_fixtures.py @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() # ==================== create_vendor Tests ==================== def test_create_vendor_success(self, db, test_user, test_company): """Test successful vendor creation.""" unique_id = str(uuid.uuid4())[:8] vendor_data = VendorCreate( company_id=test_company.id, vendor_code=f"NEWVENDOR_{unique_id}", subdomain=f"newvendor{unique_id.lower()}", name=f"New Test Vendor {unique_id}", description="A new test vendor", ) vendor = self.service.create_vendor(db, vendor_data, test_user) db.commit() assert vendor is not None assert vendor.vendor_code == f"NEWVENDOR_{unique_id}".upper() assert vendor.company_id == test_company.id assert vendor.is_verified is False # Regular user creates unverified vendor def test_create_vendor_admin_auto_verify(self, db, test_admin, admin_company): """Test admin creates verified vendor automatically.""" unique_id = str(uuid.uuid4())[:8] vendor_data = VendorCreate( company_id=admin_company.id, vendor_code=f"ADMINVENDOR_{unique_id}", subdomain=f"adminvendor{unique_id.lower()}", name=f"Admin Test Vendor {unique_id}", ) vendor = self.service.create_vendor(db, vendor_data, test_admin) db.commit() assert vendor.is_verified is True # Admin creates verified vendor def test_create_vendor_duplicate_code( self, db, test_user, test_company, test_vendor ): """Test vendor creation fails with duplicate vendor code.""" vendor_data = VendorCreate( company_id=test_company.id, vendor_code=test_vendor.vendor_code, subdomain="duplicatesub", name="Duplicate 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 def test_create_vendor_missing_company_id(self, db, test_user): """Test vendor creation fails without company_id.""" # VendorCreate requires company_id, so this should raise ValidationError # from Pydantic before reaching service with pytest.raises(Exception): # Pydantic ValidationError VendorCreate( vendor_code="NOCOMPANY", subdomain="nocompany", name="No Company Vendor", ) def test_create_vendor_unauthorized_user(self, db, test_user, other_company): """Test vendor creation fails when user doesn't own company.""" unique_id = str(uuid.uuid4())[:8] vendor_data = VendorCreate( company_id=other_company.id, # Not owned by test_user vendor_code=f"UNAUTH_{unique_id}", subdomain=f"unauth{unique_id.lower()}", name=f"Unauthorized Vendor {unique_id}", ) with pytest.raises(UnauthorizedVendorAccessException) as exc_info: self.service.create_vendor(db, vendor_data, test_user) exception = exc_info.value assert exception.status_code == 403 assert exception.error_code == "UNAUTHORIZED_VENDOR_ACCESS" def test_create_vendor_invalid_company_id(self, db, test_user): """Test vendor creation fails with non-existent company.""" unique_id = str(uuid.uuid4())[:8] vendor_data = VendorCreate( company_id=99999, # Non-existent company vendor_code=f"BADCOMPANY_{unique_id}", subdomain=f"badcompany{unique_id.lower()}", name=f"Bad Company Vendor {unique_id}", ) 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 "company_id" in exception.details.get("field", "") # ==================== get_vendors Tests ==================== 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=100) vendor_codes = [vendor.vendor_code for vendor in vendors] assert test_vendor.vendor_code in vendor_codes # Inactive vendor should not be visible to regular user 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_vendors_pagination(self, db, test_admin): """Test vendor pagination.""" vendors, total = self.service.get_vendors( db, test_admin, skip=0, limit=5, active_only=False ) assert len(vendors) <= 5 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 # ==================== get_vendor_by_code Tests ==================== 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" 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" # ==================== get_vendor_by_id Tests ==================== def test_get_vendor_by_id_success(self, db, test_vendor): """Test getting vendor by ID.""" vendor = self.service.get_vendor_by_id(db, test_vendor.id) assert vendor is not None assert vendor.id == test_vendor.id assert vendor.vendor_code == test_vendor.vendor_code def test_get_vendor_by_id_not_found(self, db): """Test getting non-existent vendor by ID.""" with pytest.raises(VendorNotFoundException) as exc_info: self.service.get_vendor_by_id(db, 99999) exception = exc_info.value assert exception.status_code == 404 assert exception.error_code == "VENDOR_NOT_FOUND" # ==================== get_active_vendor_by_code Tests ==================== def test_get_active_vendor_by_code_success(self, db, test_vendor): """Test getting active vendor by code (public access).""" vendor = self.service.get_active_vendor_by_code(db, test_vendor.vendor_code) assert vendor is not None assert vendor.id == test_vendor.id assert vendor.is_active is True def test_get_active_vendor_by_code_inactive(self, db, inactive_vendor): """Test getting inactive vendor fails.""" with pytest.raises(VendorNotFoundException): self.service.get_active_vendor_by_code(db, inactive_vendor.vendor_code) def test_get_active_vendor_by_code_not_found(self, db): """Test getting non-existent vendor fails.""" with pytest.raises(VendorNotFoundException): self.service.get_active_vendor_by_code(db, "NONEXISTENT") # ==================== toggle_verification Tests ==================== def test_toggle_verification_verify(self, db, inactive_vendor): """Test toggling verification on.""" original_verified = inactive_vendor.is_verified vendor, message = self.service.toggle_verification(db, inactive_vendor.id) db.commit() assert vendor.is_verified != original_verified assert "verified" in message.lower() def test_toggle_verification_unverify(self, db, verified_vendor): """Test toggling verification off.""" vendor, message = self.service.toggle_verification(db, verified_vendor.id) db.commit() assert vendor.is_verified is False assert "unverified" in message.lower() def test_toggle_verification_not_found(self, db): """Test toggle verification on non-existent vendor.""" with pytest.raises(VendorNotFoundException): self.service.toggle_verification(db, 99999) # ==================== toggle_status Tests ==================== def test_toggle_status_deactivate(self, db, test_vendor): """Test toggling active status off.""" vendor, message = self.service.toggle_status(db, test_vendor.id) db.commit() assert vendor.is_active is False assert "inactive" in message.lower() def test_toggle_status_activate(self, db, inactive_vendor): """Test toggling active status on.""" vendor, message = self.service.toggle_status(db, inactive_vendor.id) db.commit() assert vendor.is_active is True assert "active" in message.lower() def test_toggle_status_not_found(self, db): """Test toggle status on non-existent vendor.""" with pytest.raises(VendorNotFoundException): self.service.toggle_status(db, 99999) # ==================== set_verification / set_status Tests ==================== def test_set_verification_to_true(self, db, inactive_vendor): """Test setting verification to true.""" vendor, message = self.service.set_verification(db, inactive_vendor.id, True) db.commit() assert vendor.is_verified is True def test_set_verification_to_false(self, db, verified_vendor): """Test setting verification to false.""" vendor, message = self.service.set_verification(db, verified_vendor.id, False) db.commit() assert vendor.is_verified is False def test_set_status_to_active(self, db, inactive_vendor): """Test setting status to active.""" vendor, message = self.service.set_status(db, inactive_vendor.id, True) db.commit() assert vendor.is_active is True def test_set_status_to_inactive(self, db, test_vendor): """Test setting status to inactive.""" vendor, message = self.service.set_status(db, test_vendor.id, False) db.commit() assert vendor.is_active is False # ==================== add_product_to_catalog Tests ==================== def test_add_product_to_vendor_success(self, db, test_vendor, unique_product): """Test successfully adding product to vendor.""" from models.database.marketplace_product import MarketplaceProduct # Re-query objects to avoid session issues vendor = db.query(Vendor).filter(Vendor.id == test_vendor.id).first() mp = ( db.query(MarketplaceProduct) .filter(MarketplaceProduct.id == unique_product.id) .first() ) product_data = ProductCreate( marketplace_product_id=mp.id, price=15.99, is_featured=True, ) product = self.service.add_product_to_catalog(db, vendor, product_data) db.commit() assert product is not None assert product.vendor_id == vendor.id assert product.marketplace_product_id == mp.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=99999, # Non-existent ID 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" def test_add_product_to_vendor_already_exists(self, db, test_vendor, test_product): """Test adding product that's already in vendor fails.""" # Re-query to get fresh instances from models.database.product import Product vendor = db.query(Vendor).filter(Vendor.id == test_vendor.id).first() product = db.query(Product).filter(Product.id == test_product.id).first() product_data = ProductCreate( marketplace_product_id=product.marketplace_product_id, price=15.99, ) with pytest.raises(ProductAlreadyExistsException) as exc_info: self.service.add_product_to_catalog(db, vendor, product_data) exception = exc_info.value assert exception.status_code == 409 assert exception.error_code == "PRODUCT_ALREADY_EXISTS" # ==================== get_products Tests ==================== def test_get_products_owner_access(self, db, test_user, test_vendor, test_product): """Test vendor owner can get vendor products.""" # Re-query vendor to get fresh instance vendor = db.query(Vendor).filter(Vendor.id == test_vendor.id).first() products, total = self.service.get_products(db, vendor, test_user) assert total >= 1 assert len(products) >= 1 def test_get_products_access_denied(self, db, test_user, inactive_vendor): """Test non-owner cannot access unverified vendor products.""" # Re-query vendor to get fresh instance vendor = db.query(Vendor).filter(Vendor.id == inactive_vendor.id).first() with pytest.raises(UnauthorizedVendorAccessException) as exc_info: self.service.get_products(db, vendor, test_user) exception = exc_info.value assert exception.status_code == 403 assert exception.error_code == "UNAUTHORIZED_VENDOR_ACCESS" def test_get_products_with_filters(self, db, test_user, test_vendor, test_product): """Test getting vendor products with various filters.""" # Re-query vendor to get fresh instance vendor = db.query(Vendor).filter(Vendor.id == test_vendor.id).first() # Test active only filter products, total = self.service.get_products( db, vendor, test_user, active_only=True ) assert all(p.is_active for p in products) # ==================== Helper Method Tests ==================== def test_vendor_code_exists(self, db, test_vendor): """Test _vendor_code_exists helper method.""" assert self.service._vendor_code_exists(db, test_vendor.vendor_code) is True assert self.service._vendor_code_exists(db, "NONEXISTENT") is False def test_can_access_vendor_admin(self, db, test_admin, test_vendor): """Test admin can always access vendor.""" # Re-query vendor to get fresh instance vendor = db.query(Vendor).filter(Vendor.id == test_vendor.id).first() assert self.service._can_access_vendor(vendor, test_admin) is True def test_can_access_vendor_active_verified(self, db, test_user, verified_vendor): """Test any user can access active verified vendor.""" # Re-query vendor to get fresh instance vendor = db.query(Vendor).filter(Vendor.id == verified_vendor.id).first() assert self.service._can_access_vendor(vendor, test_user) is True @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_company ): """Test that exceptions can be properly serialized to dict for API responses.""" vendor_data = VendorCreate( company_id=test_company.id, vendor_code=test_vendor.vendor_code, subdomain="duplicate", 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_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 def test_not_found_exception_details(self, db, test_user): """Test not found exceptions include identifier details.""" with pytest.raises(VendorNotFoundException) as exc_info: self.service.get_vendor_by_code(db, "NOTEXIST", test_user) exception = exc_info.value assert exception.status_code == 404 assert exception.error_code == "VENDOR_NOT_FOUND"