Files
orion/app/modules/billing/tests/unit/test_feature_service.py
Samir Boulahtit 2833ff1476
Some checks failed
CI / ruff (push) Successful in 10s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled
fix(billing): use tier_id instead of tier_code for feature limit endpoints
Tier codes are not unique across platforms (e.g., "essential" exists for
OMS, marketplace, and loyalty). Using tier_code caused feature limits to
be saved to the wrong tier. Switched to tier_id (unique PK) in routes,
service, and frontend JS. Added comprehensive unit and integration tests
including cross-platform isolation regression tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 13:06:18 +01:00

242 lines
8.3 KiB
Python

"""Unit tests for FeatureService."""
import pytest
from app.modules.billing.models import SubscriptionTier
from app.modules.billing.models.tier_feature_limit import TierFeatureLimit
from app.modules.billing.services.feature_service import FeatureService
from app.modules.tenancy.models import Platform
@pytest.mark.unit
@pytest.mark.billing
class TestFeatureService:
"""Test suite for FeatureService."""
def setup_method(self):
self.service = FeatureService()
def test_service_instantiation(self):
"""Service can be instantiated."""
assert self.service is not None
# ============================================================================
# Fixtures
# ============================================================================
@pytest.fixture
def fs_platform(db):
"""Create a platform for feature service tests."""
platform = Platform(code="fs_test", name="FS Test Platform", is_active=True)
db.add(platform)
db.commit()
db.refresh(platform)
return platform
@pytest.fixture
def fs_second_platform(db):
"""Create a second platform to test cross-platform isolation."""
platform = Platform(code="fs_test2", name="FS Test Platform 2", is_active=True)
db.add(platform)
db.commit()
db.refresh(platform)
return platform
@pytest.fixture
def fs_tier(db, fs_platform):
"""Create a tier for feature service tests."""
tier = SubscriptionTier(
code="essential",
name="Essential",
price_monthly_cents=1000,
display_order=0,
is_active=True,
is_public=True,
platform_id=fs_platform.id,
)
db.add(tier)
db.commit()
db.refresh(tier)
return tier
@pytest.fixture
def fs_same_code_tier(db, fs_second_platform):
"""Create a tier with the SAME code but different platform."""
tier = SubscriptionTier(
code="essential",
name="Essential",
price_monthly_cents=2000,
display_order=0,
is_active=True,
is_public=True,
platform_id=fs_second_platform.id,
)
db.add(tier)
db.commit()
db.refresh(tier)
return tier
@pytest.fixture
def fs_tier_with_features(db, fs_tier):
"""Create a tier with pre-existing feature limits."""
features = [
TierFeatureLimit(tier_id=fs_tier.id, feature_code="feature_a", limit_value=None),
TierFeatureLimit(tier_id=fs_tier.id, feature_code="feature_b", limit_value=100),
TierFeatureLimit(tier_id=fs_tier.id, feature_code="feature_c", limit_value=50),
]
for f in features:
db.add(f)
db.commit()
return features
# ============================================================================
# get_tier_feature_limits
# ============================================================================
@pytest.mark.unit
@pytest.mark.billing
class TestGetTierFeatureLimits:
"""Tests for FeatureService.get_tier_feature_limits."""
def test_returns_limits_for_tier(self, db, fs_tier_with_features, fs_tier):
"""Returns all feature limit rows for the given tier ID."""
service = FeatureService()
rows = service.get_tier_feature_limits(db, fs_tier.id)
assert len(rows) == 3
codes = {r.feature_code for r in rows}
assert codes == {"feature_a", "feature_b", "feature_c"}
def test_returns_empty_for_tier_without_features(self, db, fs_tier):
"""Returns empty list for a tier with no feature limits."""
service = FeatureService()
rows = service.get_tier_feature_limits(db, fs_tier.id)
assert rows == []
def test_returns_empty_for_nonexistent_tier(self, db):
"""Returns empty list for a tier ID that doesn't exist."""
service = FeatureService()
rows = service.get_tier_feature_limits(db, 999999)
assert rows == []
def test_isolates_by_tier_id(self, db, fs_tier, fs_same_code_tier, fs_tier_with_features):
"""Features for one tier don't leak to another with the same code."""
service = FeatureService()
# fs_tier has 3 features
rows_tier1 = service.get_tier_feature_limits(db, fs_tier.id)
assert len(rows_tier1) == 3
# fs_same_code_tier (same code, different platform) has 0
rows_tier2 = service.get_tier_feature_limits(db, fs_same_code_tier.id)
assert len(rows_tier2) == 0
# ============================================================================
# upsert_tier_feature_limits
# ============================================================================
@pytest.mark.unit
@pytest.mark.billing
class TestUpsertTierFeatureLimits:
"""Tests for FeatureService.upsert_tier_feature_limits."""
def test_inserts_new_features(self, db, fs_tier):
"""Creates feature limit rows for a tier."""
service = FeatureService()
entries = [
{"feature_code": "feat_x", "limit_value": None, "enabled": True},
{"feature_code": "feat_y", "limit_value": 200, "enabled": True},
]
rows = service.upsert_tier_feature_limits(db, fs_tier.id, entries)
db.commit()
assert len(rows) == 2
assert {r.feature_code for r in rows} == {"feat_x", "feat_y"}
def test_replaces_existing_features(self, db, fs_tier, fs_tier_with_features):
"""Upsert deletes old features and inserts new ones."""
service = FeatureService()
entries = [
{"feature_code": "new_feature", "limit_value": None, "enabled": True},
]
rows = service.upsert_tier_feature_limits(db, fs_tier.id, entries)
db.commit()
assert len(rows) == 1
assert rows[0].feature_code == "new_feature"
# Old features should be gone
remaining = service.get_tier_feature_limits(db, fs_tier.id)
assert len(remaining) == 1
assert remaining[0].feature_code == "new_feature"
def test_skips_disabled_entries(self, db, fs_tier):
"""Entries with enabled=False are not persisted."""
service = FeatureService()
entries = [
{"feature_code": "enabled_feat", "limit_value": None, "enabled": True},
{"feature_code": "disabled_feat", "limit_value": None, "enabled": False},
]
rows = service.upsert_tier_feature_limits(db, fs_tier.id, entries)
db.commit()
assert len(rows) == 1
assert rows[0].feature_code == "enabled_feat"
def test_saves_to_correct_tier_by_id(self, db, fs_tier, fs_same_code_tier):
"""
Regression test: saving by tier_id targets the exact tier,
not another tier that happens to share the same code.
"""
service = FeatureService()
entries = [
{"feature_code": "platform_specific", "limit_value": None, "enabled": True},
]
# Save to the second tier (same code "essential", different platform)
service.upsert_tier_feature_limits(db, fs_same_code_tier.id, entries)
db.commit()
# First tier should have 0 features
rows_tier1 = service.get_tier_feature_limits(db, fs_tier.id)
assert len(rows_tier1) == 0
# Second tier should have 1 feature
rows_tier2 = service.get_tier_feature_limits(db, fs_same_code_tier.id)
assert len(rows_tier2) == 1
assert rows_tier2[0].feature_code == "platform_specific"
def test_clears_all_features_with_empty_list(self, db, fs_tier, fs_tier_with_features):
"""Passing an empty list removes all features."""
service = FeatureService()
rows = service.upsert_tier_feature_limits(db, fs_tier.id, [])
db.commit()
assert len(rows) == 0
remaining = service.get_tier_feature_limits(db, fs_tier.id)
assert len(remaining) == 0
def test_preserves_limit_values(self, db, fs_tier):
"""Limit values (including None for unlimited) are stored correctly."""
service = FeatureService()
entries = [
{"feature_code": "unlimited", "limit_value": None, "enabled": True},
{"feature_code": "limited", "limit_value": 42, "enabled": True},
]
service.upsert_tier_feature_limits(db, fs_tier.id, entries)
db.commit()
rows = service.get_tier_feature_limits(db, fs_tier.id)
limits = {r.feature_code: r.limit_value for r in rows}
assert limits["unlimited"] is None
assert limits["limited"] == 42