feat(loyalty): wallet debug page, Google Wallet fixes, and module config env_file standardization
Some checks failed
Some checks failed
- Add wallet diagnostics page at /admin/loyalty/wallet-debug (super admin only) with explorer-sidebar pattern: config validation, class status, card inspector, save URL tester, recent enrollments, and Apple Wallet status panels - Fix Google Wallet fat JWT: include both loyaltyClasses and loyaltyObjects in payload, use UNDER_REVIEW instead of DRAFT for class reviewStatus - Fix StorefrontProgramResponse schema: accept google_class_id values while keeping exclude=True (was rejecting non-None values) - Standardize all module configs to read from .env file directly (env_file=".env", extra="ignore") matching core Settings pattern - Add MOD-026 architecture rule enforcing env_file in module configs - Add SVC-005 noqa support in architecture validator - Add test files for dev_tools domain_health and isolation_audit services - Add google_wallet_status.py script for querying Google Wallet API - Use table_wrapper macro in wallet-debug.html (FE-005 compliance) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
# app/modules/dev_tools/tests/unit/test_domain_health_service.py
|
||||
"""Unit tests for domain_health_service."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.dev_tools.services.domain_health_service import _entry
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.dev
|
||||
class TestEntry:
|
||||
"""Tests for the _entry helper function."""
|
||||
|
||||
def test_entry_pass(self):
|
||||
"""Passing entry has status 'pass'."""
|
||||
|
||||
class FakeStore:
|
||||
store_code = "ACME"
|
||||
subdomain = "acme"
|
||||
|
||||
result = _entry(
|
||||
domain="acme.rewardflow.lu",
|
||||
entry_type="custom subdomain",
|
||||
platform_code="RF",
|
||||
expected_store=FakeStore(),
|
||||
resolved_store=FakeStore(),
|
||||
passed=True,
|
||||
note="",
|
||||
)
|
||||
assert result["status"] == "pass"
|
||||
assert result["domain"] == "acme.rewardflow.lu"
|
||||
assert result["type"] == "custom subdomain"
|
||||
assert result["platform_code"] == "RF"
|
||||
assert result["expected_store"] == "ACME"
|
||||
assert result["resolved_store"] == "ACME"
|
||||
|
||||
def test_entry_fail(self):
|
||||
"""Failing entry has status 'fail' and includes note."""
|
||||
|
||||
class FakeStore:
|
||||
store_code = "ACME"
|
||||
subdomain = "acme"
|
||||
|
||||
result = _entry(
|
||||
domain="acme.rewardflow.lu",
|
||||
entry_type="custom subdomain",
|
||||
platform_code="RF",
|
||||
expected_store=FakeStore(),
|
||||
resolved_store=None,
|
||||
passed=False,
|
||||
note="custom_subdomain lookup failed",
|
||||
)
|
||||
assert result["status"] == "fail"
|
||||
assert result["resolved_store"] is None
|
||||
assert result["note"] == "custom_subdomain lookup failed"
|
||||
|
||||
def test_entry_none_stores(self):
|
||||
"""Entry handles None expected/resolved stores."""
|
||||
result = _entry(
|
||||
domain="test.example.com",
|
||||
entry_type="custom domain",
|
||||
platform_code=None,
|
||||
expected_store=None,
|
||||
resolved_store=None,
|
||||
passed=False,
|
||||
note="not found",
|
||||
)
|
||||
assert result["expected_store"] is None
|
||||
assert result["resolved_store"] is None
|
||||
assert result["platform_code"] is None
|
||||
193
app/modules/dev_tools/tests/unit/test_isolation_audit_service.py
Normal file
193
app/modules/dev_tools/tests/unit/test_isolation_audit_service.py
Normal file
@@ -0,0 +1,193 @@
|
||||
# app/modules/dev_tools/tests/unit/test_isolation_audit_service.py
|
||||
"""Unit tests for isolation_audit_service."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.dev_tools.services.isolation_audit_service import (
|
||||
_check_contact_inheritance,
|
||||
_check_language_config,
|
||||
_check_locale_fallback,
|
||||
_check_merchant_active,
|
||||
_check_merchant_domain_primary,
|
||||
_check_theme_fallback,
|
||||
_finding,
|
||||
)
|
||||
|
||||
|
||||
def _make_store(**kwargs):
|
||||
"""Create a minimal fake store for testing."""
|
||||
defaults = {
|
||||
"contact_email": None,
|
||||
"contact_phone": None,
|
||||
"website": None,
|
||||
"business_address": None,
|
||||
"tax_number": None,
|
||||
"merchant": None,
|
||||
"store_platforms": [],
|
||||
"domains": [],
|
||||
"store_theme": None,
|
||||
"storefront_locale": None,
|
||||
"storefront_languages": None,
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
return SimpleNamespace(**defaults)
|
||||
|
||||
|
||||
def _make_merchant(**kwargs):
|
||||
defaults = {"name": "TestMerchant", "is_active": True}
|
||||
defaults.update({k: None for k in ["contact_email", "contact_phone", "website", "business_address", "tax_number"]})
|
||||
defaults.update(kwargs)
|
||||
return SimpleNamespace(**defaults)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.dev
|
||||
class TestFinding:
|
||||
"""Tests for the _finding helper."""
|
||||
|
||||
def test_finding_structure(self):
|
||||
result = _finding(
|
||||
check="test",
|
||||
check_label="Test",
|
||||
risk="high",
|
||||
resolved_value="val",
|
||||
source="store",
|
||||
source_label="Store value",
|
||||
is_explicit=True,
|
||||
note="a note",
|
||||
)
|
||||
assert result["check"] == "test"
|
||||
assert result["risk"] == "high"
|
||||
assert result["is_explicit"] is True
|
||||
assert result["note"] == "a note"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.dev
|
||||
class TestCheckContactInheritance:
|
||||
"""Tests for _check_contact_inheritance."""
|
||||
|
||||
def test_no_findings_when_store_has_values(self):
|
||||
store = _make_store(
|
||||
contact_email="shop@test.com",
|
||||
contact_phone="+352123",
|
||||
website="https://test.com",
|
||||
business_address="123 Main St",
|
||||
tax_number="LU12345",
|
||||
merchant=_make_merchant(),
|
||||
)
|
||||
assert _check_contact_inheritance(store) == []
|
||||
|
||||
def test_critical_when_inheriting_from_merchant(self):
|
||||
merchant = _make_merchant(contact_email="merchant@test.com")
|
||||
store = _make_store(merchant=merchant)
|
||||
findings = _check_contact_inheritance(store)
|
||||
email_findings = [f for f in findings if f["check"] == "contact_email"]
|
||||
assert len(email_findings) == 1
|
||||
assert email_findings[0]["risk"] == "critical"
|
||||
assert email_findings[0]["source"] == "merchant"
|
||||
|
||||
def test_critical_when_no_value_anywhere(self):
|
||||
store = _make_store(merchant=_make_merchant())
|
||||
findings = _check_contact_inheritance(store)
|
||||
email_findings = [f for f in findings if f["check"] == "contact_email"]
|
||||
assert len(email_findings) == 1
|
||||
assert email_findings[0]["source"] == "none"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.dev
|
||||
class TestCheckMerchantActive:
|
||||
"""Tests for _check_merchant_active."""
|
||||
|
||||
def test_no_finding_when_active(self):
|
||||
store = _make_store(merchant=_make_merchant(is_active=True))
|
||||
assert _check_merchant_active(store) == []
|
||||
|
||||
def test_finding_when_inactive(self):
|
||||
store = _make_store(merchant=_make_merchant(is_active=False))
|
||||
findings = _check_merchant_active(store)
|
||||
assert len(findings) == 1
|
||||
assert findings[0]["risk"] == "high"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.dev
|
||||
class TestCheckMerchantDomainPrimary:
|
||||
"""Tests for _check_merchant_domain_primary."""
|
||||
|
||||
def test_no_finding_single_primary(self):
|
||||
domains = [SimpleNamespace(domain="test.com", is_primary=True, is_active=True)]
|
||||
assert _check_merchant_domain_primary(domains) == []
|
||||
|
||||
def test_finding_multiple_primaries(self):
|
||||
domains = [
|
||||
SimpleNamespace(domain="a.com", is_primary=True, is_active=True),
|
||||
SimpleNamespace(domain="b.com", is_primary=True, is_active=True),
|
||||
]
|
||||
findings = _check_merchant_domain_primary(domains)
|
||||
assert len(findings) == 1
|
||||
assert findings[0]["risk"] == "high"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.dev
|
||||
class TestCheckThemeFallback:
|
||||
"""Tests for _check_theme_fallback."""
|
||||
|
||||
def test_no_finding_with_active_theme(self):
|
||||
store = _make_store(store_theme=SimpleNamespace(is_active=True))
|
||||
assert _check_theme_fallback(store) == []
|
||||
|
||||
def test_finding_no_theme(self):
|
||||
store = _make_store(store_theme=None)
|
||||
findings = _check_theme_fallback(store)
|
||||
assert len(findings) == 1
|
||||
assert findings[0]["risk"] == "medium"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.dev
|
||||
class TestCheckLocaleFallback:
|
||||
"""Tests for _check_locale_fallback."""
|
||||
|
||||
def test_no_finding_with_locale(self):
|
||||
store = _make_store(storefront_locale="de-LU")
|
||||
assert _check_locale_fallback(store, "fr-LU") == []
|
||||
|
||||
def test_finding_platform_default(self):
|
||||
store = _make_store(storefront_locale=None)
|
||||
findings = _check_locale_fallback(store, "de-LU")
|
||||
assert len(findings) == 1
|
||||
assert findings[0]["resolved_value"] == "de-LU"
|
||||
assert findings[0]["source"] == "platform_default"
|
||||
|
||||
def test_finding_hardcoded_fallback(self):
|
||||
store = _make_store(storefront_locale=None)
|
||||
findings = _check_locale_fallback(store, None)
|
||||
assert len(findings) == 1
|
||||
assert findings[0]["resolved_value"] == "fr-LU"
|
||||
assert findings[0]["source"] == "hardcoded"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.dev
|
||||
class TestCheckLanguageConfig:
|
||||
"""Tests for _check_language_config."""
|
||||
|
||||
def test_no_finding_custom_languages(self):
|
||||
store = _make_store(storefront_languages=["fr", "en"])
|
||||
assert _check_language_config(store) == []
|
||||
|
||||
def test_finding_default_languages(self):
|
||||
store = _make_store(storefront_languages=["fr", "de", "en", "lb"])
|
||||
findings = _check_language_config(store)
|
||||
assert len(findings) == 1
|
||||
assert findings[0]["risk"] == "medium"
|
||||
|
||||
def test_finding_none_languages(self):
|
||||
store = _make_store(storefront_languages=None)
|
||||
findings = _check_language_config(store)
|
||||
assert len(findings) == 1
|
||||
Reference in New Issue
Block a user