Files
orion/tests/unit/services/test_feature_service.py
Samir Boulahtit aa4b5a4c63 test: add service tests and fix architecture violations
- Add comprehensive unit tests for FeatureService (24 tests)
- Add comprehensive unit tests for UsageService (11 tests)
- Fix API-002/API-003 architecture violations in feature/usage APIs
- Move database queries from API layer to service layer
- Create UsageService for usage and limits management
- Create custom exceptions (FeatureNotFoundError, TierNotFoundError)
- Fix ValidationException usage in content_pages.py
- Refactor vendor features API to use proper response models
- All 35 new tests passing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 18:48:59 +01:00

368 lines
13 KiB
Python

# 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