# tests/unit/services/test_feature_service.py """Unit tests for FeatureService.""" import pytest from app.exceptions import FeatureNotFoundError, InvalidFeatureCodesError, TierNotFoundError from app.services.feature_service import FeatureService, feature_service from models.database.feature import Feature from models.database.subscription import SubscriptionTier, VendorSubscription @pytest.mark.unit @pytest.mark.features class TestFeatureServiceAvailability: """Test suite for feature availability checking.""" def setup_method(self): """Initialize service instance before each test.""" self.service = FeatureService() def test_has_feature_true(self, db, test_vendor_with_subscription): """Test has_feature returns True for available feature.""" vendor_id = test_vendor_with_subscription.id result = self.service.has_feature(db, vendor_id, "basic_reports") assert result is True def test_has_feature_false(self, db, test_vendor_with_subscription): """Test has_feature returns False for unavailable feature.""" vendor_id = test_vendor_with_subscription.id result = self.service.has_feature(db, vendor_id, "api_access") assert result is False def test_has_feature_no_subscription(self, db, test_vendor): """Test has_feature returns False for vendor without subscription.""" result = self.service.has_feature(db, test_vendor.id, "basic_reports") assert result is False def test_get_vendor_feature_codes(self, db, test_vendor_with_subscription): """Test getting all feature codes for vendor.""" vendor_id = test_vendor_with_subscription.id features = self.service.get_vendor_feature_codes(db, vendor_id) assert isinstance(features, set) assert "basic_reports" in features assert "api_access" not in features @pytest.mark.unit @pytest.mark.features class TestFeatureServiceListing: """Test suite for feature listing operations.""" def setup_method(self): """Initialize service instance before each test.""" self.service = FeatureService() def test_get_vendor_features(self, db, test_vendor_with_subscription, test_features): """Test getting all features with availability.""" vendor_id = test_vendor_with_subscription.id features = self.service.get_vendor_features(db, vendor_id) assert len(features) > 0 basic_reports = next((f for f in features if f.code == "basic_reports"), None) assert basic_reports is not None assert basic_reports.is_available is True api_access = next((f for f in features if f.code == "api_access"), None) assert api_access is not None assert api_access.is_available is False def test_get_vendor_features_by_category( self, db, test_vendor_with_subscription, test_features ): """Test filtering features by category.""" vendor_id = test_vendor_with_subscription.id features = self.service.get_vendor_features(db, vendor_id, category="analytics") assert all(f.category == "analytics" for f in features) def test_get_vendor_features_available_only( self, db, test_vendor_with_subscription, test_features ): """Test getting only available features.""" vendor_id = test_vendor_with_subscription.id features = self.service.get_vendor_features( db, vendor_id, include_unavailable=False ) assert all(f.is_available for f in features) def test_get_available_feature_codes(self, db, test_vendor_with_subscription): """Test getting simple list of available codes.""" vendor_id = test_vendor_with_subscription.id codes = self.service.get_available_feature_codes(db, vendor_id) assert isinstance(codes, list) assert "basic_reports" in codes @pytest.mark.unit @pytest.mark.features class TestFeatureServiceMetadata: """Test suite for feature metadata operations.""" def setup_method(self): """Initialize service instance before each test.""" self.service = FeatureService() def test_get_feature_by_code(self, db, test_features): """Test getting feature by code.""" feature = self.service.get_feature_by_code(db, "basic_reports") assert feature is not None assert feature.code == "basic_reports" assert feature.name == "Basic Reports" def test_get_feature_by_code_not_found(self, db, test_features): """Test getting non-existent feature returns None.""" feature = self.service.get_feature_by_code(db, "nonexistent") assert feature is None def test_get_feature_upgrade_info(self, db, test_features, test_subscription_tiers): """Test getting upgrade info for locked feature.""" info = self.service.get_feature_upgrade_info(db, "api_access") assert info is not None assert info.feature_code == "api_access" assert info.required_tier_code == "professional" def test_get_feature_upgrade_info_no_minimum_tier(self, db, test_features): """Test upgrade info for feature without minimum tier.""" # basic_reports has no minimum tier in fixtures info = self.service.get_feature_upgrade_info(db, "basic_reports") assert info is None def test_get_all_features(self, db, test_features): """Test getting all features for admin.""" features = self.service.get_all_features(db) assert len(features) >= 3 def test_get_all_features_by_category(self, db, test_features): """Test filtering features by category.""" features = self.service.get_all_features(db, category="analytics") assert all(f.category == "analytics" for f in features) def test_get_categories(self, db, test_features): """Test getting unique categories.""" categories = self.service.get_categories(db) assert "analytics" in categories assert "integrations" in categories @pytest.mark.unit @pytest.mark.features class TestFeatureServiceCache: """Test suite for cache operations.""" def setup_method(self): """Initialize service instance before each test.""" self.service = FeatureService() def test_cache_invalidation(self, db, test_vendor_with_subscription): """Test cache invalidation for vendor.""" vendor_id = test_vendor_with_subscription.id # Prime the cache self.service.get_vendor_feature_codes(db, vendor_id) assert self.service._cache.get(vendor_id) is not None # Invalidate self.service.invalidate_vendor_cache(vendor_id) assert self.service._cache.get(vendor_id) is None def test_cache_invalidate_all(self, db, test_vendor_with_subscription): """Test invalidating entire cache.""" vendor_id = test_vendor_with_subscription.id # Prime the cache self.service.get_vendor_feature_codes(db, vendor_id) # Invalidate all self.service.invalidate_all_cache() assert self.service._cache.get(vendor_id) is None @pytest.mark.unit @pytest.mark.features class TestFeatureServiceAdmin: """Test suite for admin operations.""" def setup_method(self): """Initialize service instance before each test.""" self.service = FeatureService() def test_get_all_tiers_with_features(self, db, test_subscription_tiers): """Test getting all tiers.""" tiers = self.service.get_all_tiers_with_features(db) assert len(tiers) == 2 assert tiers[0].code == "essential" assert tiers[1].code == "professional" def test_update_tier_features(self, db, test_subscription_tiers, test_features): """Test updating tier features.""" tier = self.service.update_tier_features( db, "essential", ["basic_reports", "api_access"] ) db.commit() assert "api_access" in tier.features def test_update_tier_features_invalid_codes( self, db, test_subscription_tiers, test_features ): """Test updating tier with invalid feature codes.""" with pytest.raises(InvalidFeatureCodesError) as exc_info: self.service.update_tier_features( db, "essential", ["basic_reports", "nonexistent_feature"] ) assert "nonexistent_feature" in exc_info.value.invalid_codes def test_update_tier_features_tier_not_found(self, db, test_features): """Test updating non-existent tier.""" with pytest.raises(TierNotFoundError) as exc_info: self.service.update_tier_features(db, "nonexistent", ["basic_reports"]) assert exc_info.value.tier_code == "nonexistent" def test_update_feature(self, db, test_features): """Test updating feature metadata.""" feature = self.service.update_feature( db, "basic_reports", name="Updated Reports", description="New description" ) db.commit() assert feature.name == "Updated Reports" assert feature.description == "New description" def test_update_feature_not_found(self, db, test_features): """Test updating non-existent feature.""" with pytest.raises(FeatureNotFoundError) as exc_info: self.service.update_feature(db, "nonexistent", name="Test") assert exc_info.value.feature_code == "nonexistent" def test_update_feature_minimum_tier( self, db, test_features, test_subscription_tiers ): """Test updating feature minimum tier.""" feature = self.service.update_feature( db, "basic_reports", minimum_tier_code="professional" ) db.commit() db.refresh(feature) assert feature.minimum_tier is not None assert feature.minimum_tier.code == "professional" # ==================== Fixtures ==================== @pytest.fixture def test_subscription_tiers(db): """Create multiple subscription tiers.""" tiers = [ SubscriptionTier( code="essential", name="Essential", description="Essential plan", price_monthly_cents=4900, price_annual_cents=49000, orders_per_month=100, products_limit=500, team_members=2, features=["basic_reports"], is_active=True, display_order=1, ), SubscriptionTier( code="professional", name="Professional", description="Professional plan", price_monthly_cents=9900, price_annual_cents=99000, orders_per_month=500, products_limit=2000, team_members=5, features=["basic_reports", "api_access", "analytics_dashboard"], is_active=True, display_order=2, ), ] for tier in tiers: db.add(tier) db.commit() for tier in tiers: db.refresh(tier) return tiers @pytest.fixture def test_vendor_with_subscription(db, test_vendor, test_subscription_tiers): """Create a vendor with an active subscription.""" from datetime import datetime, timezone essential_tier = test_subscription_tiers[0] # Use the essential tier from tiers list now = datetime.now(timezone.utc) subscription = VendorSubscription( vendor_id=test_vendor.id, tier="essential", tier_id=essential_tier.id, status="active", period_start=now, period_end=now.replace(month=now.month + 1 if now.month < 12 else 1), orders_this_period=10, ) db.add(subscription) db.commit() db.refresh(test_vendor) return test_vendor @pytest.fixture def test_features(db, test_subscription_tiers): """Create test features.""" features = [ Feature( code="basic_reports", name="Basic Reports", description="View basic analytics reports", category="analytics", ui_location="sidebar", ui_icon="chart-bar", is_active=True, display_order=1, ), Feature( code="api_access", name="API Access", description="Access the REST API", category="integrations", ui_location="settings", ui_icon="code", minimum_tier_id=test_subscription_tiers[1].id, # Professional is_active=True, display_order=2, ), Feature( code="analytics_dashboard", name="Analytics Dashboard", description="Advanced analytics dashboard", category="analytics", ui_location="sidebar", ui_icon="presentation-chart-line", minimum_tier_id=test_subscription_tiers[1].id, is_active=True, display_order=3, ), ] for feature in features: db.add(feature) db.commit() for feature in features: db.refresh(feature) return features