Files
orion/tests/unit/middleware/test_merchant_domain_resolution.py
Samir Boulahtit 0984ff7d17 feat(tenancy): add merchant-level domain with store override
Merchants can now register domains (e.g., myloyaltyprogram.lu) that all
their stores inherit. Individual stores can override with their own custom
domain. Resolution priority: StoreDomain > MerchantDomain > subdomain.

- Add MerchantDomain model, schema, service, and admin API endpoints
- Add merchant domain fallback in platform and store context middleware
- Add Merchant.primary_domain and Store.effective_domain properties
- Add Alembic migration for merchant_domains table
- Update loyalty user journey docs with subscription & domain setup flow
- Add unit tests (50 passing) and integration tests (15 passing)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 22:04:49 +01:00

290 lines
9.8 KiB
Python

# tests/unit/middleware/test_merchant_domain_resolution.py
"""
Unit tests for merchant domain resolution in platform and store context middleware.
Tests cover:
- PlatformContextManager.get_platform_from_context() with merchant domain
- StoreContextManager.get_store_from_context() with merchant domain
- Priority: StoreDomain > MerchantDomain
- Fallthrough when MerchantDomain not found or inactive/unverified
"""
import uuid
from datetime import UTC, datetime
from unittest.mock import Mock
import pytest
from app.modules.tenancy.models import Platform, Store, StoreDomain
from app.modules.tenancy.models.merchant_domain import MerchantDomain
from middleware.platform_context import PlatformContextManager
from middleware.store_context import StoreContextManager
# =============================================================================
# PLATFORM CONTEXT - MERCHANT DOMAIN RESOLUTION
# =============================================================================
@pytest.mark.unit
@pytest.mark.middleware
class TestPlatformContextMerchantDomain:
"""Test PlatformContextManager.get_platform_from_context() with merchant domains."""
def test_resolves_platform_from_merchant_domain(self, db, test_merchant, test_platform):
"""Test that platform is resolved from MerchantDomain.platform_id."""
unique_id = str(uuid.uuid4())[:8]
md = MerchantDomain(
merchant_id=test_merchant.id,
platform_id=test_platform.id,
domain=f"mplatform{unique_id}.lu",
is_primary=True,
is_active=True,
is_verified=True,
verified_at=datetime.now(UTC),
verification_token=f"mpt_{unique_id}",
)
db.add(md)
db.commit()
context = {
"domain": f"mplatform{unique_id}.lu",
"detection_method": "domain",
"host": f"mplatform{unique_id}.lu",
"original_path": "/",
}
platform = PlatformContextManager.get_platform_from_context(db, context)
assert platform is not None
assert platform.id == test_platform.id
def test_falls_through_when_merchant_domain_not_found(self, db):
"""Test that None is returned when no MerchantDomain matches."""
context = {
"domain": "nonexistent.lu",
"detection_method": "domain",
"host": "nonexistent.lu",
"original_path": "/",
}
platform = PlatformContextManager.get_platform_from_context(db, context)
assert platform is None
def test_falls_through_when_merchant_domain_inactive(self, db, test_merchant, test_platform):
"""Test that inactive MerchantDomain is skipped."""
unique_id = str(uuid.uuid4())[:8]
md = MerchantDomain(
merchant_id=test_merchant.id,
platform_id=test_platform.id,
domain=f"inactive{unique_id}.lu",
is_primary=True,
is_active=False,
is_verified=True,
verified_at=datetime.now(UTC),
verification_token=f"ipt_{unique_id}",
)
db.add(md)
db.commit()
context = {
"domain": f"inactive{unique_id}.lu",
"detection_method": "domain",
"host": f"inactive{unique_id}.lu",
"original_path": "/",
}
platform = PlatformContextManager.get_platform_from_context(db, context)
assert platform is None
def test_falls_through_when_merchant_domain_unverified(self, db, test_merchant, test_platform):
"""Test that unverified MerchantDomain is skipped."""
unique_id = str(uuid.uuid4())[:8]
md = MerchantDomain(
merchant_id=test_merchant.id,
platform_id=test_platform.id,
domain=f"unverified{unique_id}.lu",
is_primary=True,
is_active=True,
is_verified=False,
verification_token=f"upt_{unique_id}",
)
db.add(md)
db.commit()
context = {
"domain": f"unverified{unique_id}.lu",
"detection_method": "domain",
"host": f"unverified{unique_id}.lu",
"original_path": "/",
}
platform = PlatformContextManager.get_platform_from_context(db, context)
assert platform is None
def test_store_domain_takes_priority_over_merchant_domain(
self, db, test_store, test_merchant, test_platform
):
"""Test that StoreDomain is checked before MerchantDomain."""
unique_id = str(uuid.uuid4())[:8]
domain_name = f"priority{unique_id}.lu"
# Create a StoreDomain with this domain
sd = StoreDomain(
store_id=test_store.id,
platform_id=test_platform.id,
domain=domain_name,
is_primary=True,
is_active=True,
is_verified=True,
verified_at=datetime.now(UTC),
verification_token=f"sdp_{unique_id}",
)
db.add(sd)
db.commit()
context = {
"domain": domain_name,
"detection_method": "domain",
"host": domain_name,
"original_path": "/",
}
platform = PlatformContextManager.get_platform_from_context(db, context)
assert platform is not None
assert platform.id == test_platform.id
# =============================================================================
# STORE CONTEXT - MERCHANT DOMAIN RESOLUTION
# =============================================================================
@pytest.mark.unit
@pytest.mark.middleware
class TestStoreContextMerchantDomain:
"""Test StoreContextManager.get_store_from_context() with merchant domains."""
def test_resolves_to_merchants_first_active_store(
self, db, test_merchant, test_store
):
"""Test that merchant domain resolves to merchant's first active store."""
unique_id = str(uuid.uuid4())[:8]
md = MerchantDomain(
merchant_id=test_merchant.id,
domain=f"mstore{unique_id}.lu",
is_primary=True,
is_active=True,
is_verified=True,
verified_at=datetime.now(UTC),
verification_token=f"mst_{unique_id}",
)
db.add(md)
db.commit()
context = {
"domain": f"mstore{unique_id}.lu",
"detection_method": "custom_domain",
"host": f"mstore{unique_id}.lu",
"original_host": f"mstore{unique_id}.lu",
}
store = StoreContextManager.get_store_from_context(db, context)
assert store is not None
assert store.merchant_id == test_merchant.id
assert context.get("merchant_domain") is True
assert context.get("merchant_id") == test_merchant.id
def test_store_domain_takes_priority_over_merchant_domain(
self, db, test_store, test_merchant
):
"""Test that StoreDomain takes priority over MerchantDomain."""
unique_id = str(uuid.uuid4())[:8]
domain_name = f"storepri{unique_id}.lu"
# Create StoreDomain for this store
sd = StoreDomain(
store_id=test_store.id,
domain=domain_name,
is_primary=True,
is_active=True,
is_verified=True,
verified_at=datetime.now(UTC),
verification_token=f"sdpri_{unique_id}",
)
db.add(sd)
db.commit()
context = {
"domain": domain_name,
"detection_method": "custom_domain",
"host": domain_name,
"original_host": domain_name,
}
store = StoreContextManager.get_store_from_context(db, context)
assert store is not None
assert store.id == test_store.id
# merchant_domain should NOT be set because StoreDomain resolved first
assert context.get("merchant_domain") is None
def test_falls_through_when_no_active_stores(self, db, other_merchant):
"""Test that None is returned when merchant has no active stores."""
unique_id = str(uuid.uuid4())[:8]
# Create inactive store for the merchant
inactive = Store(
merchant_id=other_merchant.id,
store_code=f"INACTIVE_{unique_id.upper()}",
subdomain=f"inactive{unique_id.lower()}",
name="Inactive Store",
is_active=False,
)
db.add(inactive)
md = MerchantDomain(
merchant_id=other_merchant.id,
domain=f"noactive{unique_id}.lu",
is_primary=True,
is_active=True,
is_verified=True,
verified_at=datetime.now(UTC),
verification_token=f"nat_{unique_id}",
)
db.add(md)
db.commit()
context = {
"domain": f"noactive{unique_id}.lu",
"detection_method": "custom_domain",
"host": f"noactive{unique_id}.lu",
"original_host": f"noactive{unique_id}.lu",
}
store = StoreContextManager.get_store_from_context(db, context)
assert store is None
def test_falls_through_when_merchant_domain_inactive(self, db, test_merchant):
"""Test that inactive MerchantDomain is not resolved to a store."""
unique_id = str(uuid.uuid4())[:8]
md = MerchantDomain(
merchant_id=test_merchant.id,
domain=f"inactivem{unique_id}.lu",
is_primary=True,
is_active=False,
is_verified=True,
verified_at=datetime.now(UTC),
verification_token=f"im_{unique_id}",
)
db.add(md)
db.commit()
context = {
"domain": f"inactivem{unique_id}.lu",
"detection_method": "custom_domain",
"host": f"inactivem{unique_id}.lu",
"original_host": f"inactivem{unique_id}.lu",
}
store = StoreContextManager.get_store_from_context(db, context)
assert store is None