feat: loyalty feature provider, admin data fixes, storefront mobile menu
- Add LoyaltyFeatureProvider with 11 BINARY/MERCHANT features for billing feature gating, wired into loyalty module definition - Fix subscription-tiers admin page showing 0 features by populating feature_codes from tier relationship in all admin tier endpoints - Fix merchants admin page showing 0 stores and N/A owner by adding store_count and owner_email to MerchantResponse and eager-loading owner - Add "no tiers" warning with link in subscription creation modal when platform has no configured tiers - Add missing mobile menu panel to storefront base template so hamburger toggle actually shows navigation links Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
0
tests/unit/modules/loyalty/__init__.py
Normal file
0
tests/unit/modules/loyalty/__init__.py
Normal file
92
tests/unit/modules/loyalty/test_loyalty_features.py
Normal file
92
tests/unit/modules/loyalty/test_loyalty_features.py
Normal file
@@ -0,0 +1,92 @@
|
||||
# tests/unit/modules/loyalty/test_loyalty_features.py
|
||||
"""
|
||||
Unit tests for the loyalty feature provider.
|
||||
|
||||
Tests cover:
|
||||
- Feature category
|
||||
- Feature declarations (count, codes, types, scopes)
|
||||
- Usage methods return empty lists
|
||||
- Protocol compliance
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.contracts.features import (
|
||||
FeatureProviderProtocol,
|
||||
FeatureScope,
|
||||
FeatureType,
|
||||
)
|
||||
from app.modules.loyalty.definition import loyalty_module
|
||||
from app.modules.loyalty.services.loyalty_features import (
|
||||
LoyaltyFeatureProvider,
|
||||
loyalty_feature_provider,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestLoyaltyFeatureProvider:
|
||||
"""Tests for LoyaltyFeatureProvider."""
|
||||
|
||||
def test_feature_category(self):
|
||||
"""Feature category should be 'loyalty'."""
|
||||
assert loyalty_feature_provider.feature_category == "loyalty"
|
||||
|
||||
def test_get_feature_declarations_count(self):
|
||||
"""Should return exactly 11 feature declarations."""
|
||||
declarations = loyalty_feature_provider.get_feature_declarations()
|
||||
assert len(declarations) == 11
|
||||
|
||||
def test_feature_codes_match_module_definition(self):
|
||||
"""All declared feature codes should match loyalty_module.features."""
|
||||
declarations = loyalty_feature_provider.get_feature_declarations()
|
||||
declared_codes = {d.code for d in declarations}
|
||||
module_features = set(loyalty_module.features)
|
||||
assert declared_codes == module_features
|
||||
|
||||
def test_all_features_are_binary(self):
|
||||
"""All loyalty features should be BINARY type."""
|
||||
declarations = loyalty_feature_provider.get_feature_declarations()
|
||||
for decl in declarations:
|
||||
assert decl.feature_type == FeatureType.BINARY, (
|
||||
f"Feature {decl.code} should be BINARY, got {decl.feature_type}"
|
||||
)
|
||||
|
||||
def test_all_features_are_merchant_scoped(self):
|
||||
"""All loyalty features should be MERCHANT scoped."""
|
||||
declarations = loyalty_feature_provider.get_feature_declarations()
|
||||
for decl in declarations:
|
||||
assert decl.scope == FeatureScope.MERCHANT, (
|
||||
f"Feature {decl.code} should be MERCHANT scoped, got {decl.scope}"
|
||||
)
|
||||
|
||||
def test_all_features_have_name_key(self):
|
||||
"""All features should have a non-empty name_key."""
|
||||
declarations = loyalty_feature_provider.get_feature_declarations()
|
||||
for decl in declarations:
|
||||
assert decl.name_key, f"Feature {decl.code} missing name_key"
|
||||
|
||||
def test_all_features_have_description_key(self):
|
||||
"""All features should have a non-empty description_key."""
|
||||
declarations = loyalty_feature_provider.get_feature_declarations()
|
||||
for decl in declarations:
|
||||
assert decl.description_key, f"Feature {decl.code} missing description_key"
|
||||
|
||||
def test_get_store_usage_returns_empty(self):
|
||||
"""get_store_usage should return an empty list."""
|
||||
result = loyalty_feature_provider.get_store_usage(db=None, store_id=1)
|
||||
assert result == []
|
||||
|
||||
def test_get_merchant_usage_returns_empty(self):
|
||||
"""get_merchant_usage should return an empty list."""
|
||||
result = loyalty_feature_provider.get_merchant_usage(
|
||||
db=None, merchant_id=1, platform_id=1
|
||||
)
|
||||
assert result == []
|
||||
|
||||
def test_satisfies_feature_provider_protocol(self):
|
||||
"""Provider should satisfy FeatureProviderProtocol."""
|
||||
assert isinstance(loyalty_feature_provider, FeatureProviderProtocol)
|
||||
|
||||
def test_singleton_is_correct_type(self):
|
||||
"""Singleton instance should be a LoyaltyFeatureProvider."""
|
||||
assert isinstance(loyalty_feature_provider, LoyaltyFeatureProvider)
|
||||
Reference in New Issue
Block a user