feat(middleware): harden routing with fail-closed policy, custom subdomain management, and perf fixes
- Fix IPv6 host parsing with _strip_port() utility - Remove dangerous StorePlatform→Store.subdomain silent fallback - Close storefront gate bypass when frontend_type is None - Add custom subdomain management UI and API for stores - Add domain health diagnostic tool - Convert db.add() in loops to db.add_all() (24 PERF-006 fixes) - Add tests for all new functionality (18 subdomain service tests) - Add .github templates for validator compliance Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
257
app/modules/tenancy/tests/unit/test_store_subdomain_service.py
Normal file
257
app/modules/tenancy/tests/unit/test_store_subdomain_service.py
Normal file
@@ -0,0 +1,257 @@
|
||||
# tests/unit/services/test_store_subdomain_service.py
|
||||
"""Unit tests for StoreSubdomainService."""
|
||||
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from app.exceptions.base import (
|
||||
ConflictException,
|
||||
ResourceNotFoundException,
|
||||
ValidationException,
|
||||
)
|
||||
from app.modules.tenancy.models import Platform, Store
|
||||
from app.modules.tenancy.models.store_platform import StorePlatform
|
||||
from app.modules.tenancy.services.store_subdomain_service import StoreSubdomainService
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def subdomain_service():
|
||||
return StoreSubdomainService()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sd_merchant(db, test_admin):
|
||||
"""Create a merchant for subdomain tests."""
|
||||
from app.modules.tenancy.models import Merchant
|
||||
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
merchant = Merchant(
|
||||
name=f"SD Merchant {unique_id}",
|
||||
owner_user_id=test_admin.id,
|
||||
contact_email=f"sd{unique_id}@test.com",
|
||||
is_active=True,
|
||||
is_verified=True,
|
||||
)
|
||||
db.add(merchant)
|
||||
db.commit()
|
||||
db.refresh(merchant)
|
||||
return merchant
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sd_platform(db):
|
||||
"""Create a test platform."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
platform = Platform(
|
||||
code=f"sd{unique_id}",
|
||||
name=f"SD Platform {unique_id}",
|
||||
domain=f"sd{unique_id}.example.com",
|
||||
is_active=True,
|
||||
)
|
||||
db.add(platform)
|
||||
db.commit()
|
||||
db.refresh(platform)
|
||||
return platform
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sd_platform_2(db):
|
||||
"""Create a second test platform."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
platform = Platform(
|
||||
code=f"sd2{unique_id}",
|
||||
name=f"SD Platform 2 {unique_id}",
|
||||
domain=f"sd2{unique_id}.example.com",
|
||||
is_active=True,
|
||||
)
|
||||
db.add(platform)
|
||||
db.commit()
|
||||
db.refresh(platform)
|
||||
return platform
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sd_store(db, sd_merchant):
|
||||
"""Create a test store."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
store = Store(
|
||||
store_code=f"SD{unique_id}".upper(),
|
||||
name=f"SD Store {unique_id}",
|
||||
subdomain=f"sdstore{unique_id}",
|
||||
merchant_id=sd_merchant.id,
|
||||
is_active=True,
|
||||
)
|
||||
db.add(store)
|
||||
db.commit()
|
||||
db.refresh(store)
|
||||
return store
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sd_membership(db, sd_store, sd_platform):
|
||||
"""Create a StorePlatform membership."""
|
||||
sp = StorePlatform(
|
||||
store_id=sd_store.id,
|
||||
platform_id=sd_platform.id,
|
||||
is_active=True,
|
||||
)
|
||||
db.add(sp)
|
||||
db.commit()
|
||||
db.refresh(sp)
|
||||
return sp
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.stores
|
||||
class TestGetCustomSubdomains:
|
||||
"""Tests for listing custom subdomains."""
|
||||
|
||||
def test_list_returns_memberships(self, db, subdomain_service, sd_store, sd_membership, sd_platform):
|
||||
results = subdomain_service.get_custom_subdomains(db, sd_store.id)
|
||||
|
||||
assert len(results) == 1
|
||||
assert results[0]["platform_code"] == sd_platform.code
|
||||
assert results[0]["custom_subdomain"] is None
|
||||
assert results[0]["default_subdomain"] == sd_store.subdomain
|
||||
|
||||
def test_list_shows_custom_subdomain(self, db, subdomain_service, sd_store, sd_membership, sd_platform):
|
||||
sd_membership.custom_subdomain = "my-custom"
|
||||
db.commit()
|
||||
|
||||
results = subdomain_service.get_custom_subdomains(db, sd_store.id)
|
||||
|
||||
assert results[0]["custom_subdomain"] == "my-custom"
|
||||
assert results[0]["full_url"] == f"my-custom.{sd_platform.domain}"
|
||||
|
||||
def test_list_nonexistent_store_raises(self, db, subdomain_service):
|
||||
with pytest.raises(ResourceNotFoundException):
|
||||
subdomain_service.get_custom_subdomains(db, 999999)
|
||||
|
||||
def test_list_excludes_inactive_memberships(self, db, subdomain_service, sd_store, sd_membership):
|
||||
sd_membership.is_active = False
|
||||
db.commit()
|
||||
|
||||
results = subdomain_service.get_custom_subdomains(db, sd_store.id)
|
||||
|
||||
assert len(results) == 0
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.stores
|
||||
class TestSetCustomSubdomain:
|
||||
"""Tests for setting a custom subdomain."""
|
||||
|
||||
def test_set_valid_subdomain(self, db, subdomain_service, sd_store, sd_membership, sd_platform):
|
||||
sp = subdomain_service.set_custom_subdomain(db, sd_store.id, sd_platform.id, "my-store")
|
||||
|
||||
assert sp.custom_subdomain == "my-store"
|
||||
|
||||
def test_set_normalizes_to_lowercase(self, db, subdomain_service, sd_store, sd_membership, sd_platform):
|
||||
sp = subdomain_service.set_custom_subdomain(db, sd_store.id, sd_platform.id, "My-Store")
|
||||
|
||||
assert sp.custom_subdomain == "my-store"
|
||||
|
||||
def test_set_strips_whitespace(self, db, subdomain_service, sd_store, sd_membership, sd_platform):
|
||||
sp = subdomain_service.set_custom_subdomain(db, sd_store.id, sd_platform.id, " my-store ")
|
||||
|
||||
assert sp.custom_subdomain == "my-store"
|
||||
|
||||
def test_set_rejects_leading_hyphen(self, db, subdomain_service, sd_store, sd_membership, sd_platform):
|
||||
with pytest.raises(ValidationException):
|
||||
subdomain_service.set_custom_subdomain(db, sd_store.id, sd_platform.id, "-invalid")
|
||||
|
||||
def test_set_rejects_trailing_hyphen(self, db, subdomain_service, sd_store, sd_membership, sd_platform):
|
||||
with pytest.raises(ValidationException):
|
||||
subdomain_service.set_custom_subdomain(db, sd_store.id, sd_platform.id, "invalid-")
|
||||
|
||||
def test_set_rejects_uppercase_special_chars(self, db, subdomain_service, sd_store, sd_membership, sd_platform):
|
||||
with pytest.raises(ValidationException):
|
||||
subdomain_service.set_custom_subdomain(db, sd_store.id, sd_platform.id, "inv@lid")
|
||||
|
||||
def test_set_rejects_too_short(self, db, subdomain_service, sd_store, sd_membership, sd_platform):
|
||||
with pytest.raises(ValidationException):
|
||||
subdomain_service.set_custom_subdomain(db, sd_store.id, sd_platform.id, "ab")
|
||||
|
||||
def test_set_nonexistent_membership_raises(self, db, subdomain_service, sd_store):
|
||||
with pytest.raises(ResourceNotFoundException):
|
||||
subdomain_service.set_custom_subdomain(db, sd_store.id, 999999, "test-sub")
|
||||
|
||||
def test_set_duplicate_on_same_platform_raises(
|
||||
self, db, subdomain_service, sd_store, sd_membership, sd_platform, sd_merchant
|
||||
):
|
||||
# Create another store with same subdomain on same platform
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
other_store = Store(
|
||||
store_code=f"OTHER{unique_id}".upper(),
|
||||
name=f"Other Store {unique_id}",
|
||||
subdomain=f"other{unique_id}",
|
||||
merchant_id=sd_merchant.id,
|
||||
is_active=True,
|
||||
)
|
||||
db.add(other_store)
|
||||
db.flush()
|
||||
|
||||
other_sp = StorePlatform(
|
||||
store_id=other_store.id,
|
||||
platform_id=sd_platform.id,
|
||||
is_active=True,
|
||||
custom_subdomain="taken-name",
|
||||
)
|
||||
db.add(other_sp)
|
||||
db.commit()
|
||||
|
||||
with pytest.raises(ConflictException):
|
||||
subdomain_service.set_custom_subdomain(db, sd_store.id, sd_platform.id, "taken-name")
|
||||
|
||||
def test_set_same_subdomain_different_platform_ok(
|
||||
self, db, subdomain_service, sd_store, sd_membership, sd_platform, sd_platform_2
|
||||
):
|
||||
# Set on first platform
|
||||
subdomain_service.set_custom_subdomain(db, sd_store.id, sd_platform.id, "shared-name")
|
||||
db.commit()
|
||||
|
||||
# Create membership on second platform
|
||||
sp2 = StorePlatform(
|
||||
store_id=sd_store.id,
|
||||
platform_id=sd_platform_2.id,
|
||||
is_active=True,
|
||||
)
|
||||
db.add(sp2)
|
||||
db.commit()
|
||||
|
||||
# Same subdomain on different platform should work
|
||||
result = subdomain_service.set_custom_subdomain(db, sd_store.id, sd_platform_2.id, "shared-name")
|
||||
|
||||
assert result.custom_subdomain == "shared-name"
|
||||
|
||||
def test_update_existing_subdomain(self, db, subdomain_service, sd_store, sd_membership, sd_platform):
|
||||
subdomain_service.set_custom_subdomain(db, sd_store.id, sd_platform.id, "first")
|
||||
db.commit()
|
||||
|
||||
sp = subdomain_service.set_custom_subdomain(db, sd_store.id, sd_platform.id, "second")
|
||||
|
||||
assert sp.custom_subdomain == "second"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.stores
|
||||
class TestClearCustomSubdomain:
|
||||
"""Tests for clearing a custom subdomain."""
|
||||
|
||||
def test_clear_existing_subdomain(self, db, subdomain_service, sd_store, sd_membership, sd_platform):
|
||||
sd_membership.custom_subdomain = "to-clear"
|
||||
db.commit()
|
||||
|
||||
sp = subdomain_service.clear_custom_subdomain(db, sd_store.id, sd_platform.id)
|
||||
|
||||
assert sp.custom_subdomain is None
|
||||
|
||||
def test_clear_already_none_ok(self, db, subdomain_service, sd_store, sd_membership, sd_platform):
|
||||
sp = subdomain_service.clear_custom_subdomain(db, sd_store.id, sd_platform.id)
|
||||
|
||||
assert sp.custom_subdomain is None
|
||||
|
||||
def test_clear_nonexistent_membership_raises(self, db, subdomain_service, sd_store):
|
||||
with pytest.raises(ResourceNotFoundException):
|
||||
subdomain_service.clear_custom_subdomain(db, sd_store.id, 999999)
|
||||
@@ -68,15 +68,14 @@ def metrics_team_members(db, metrics_stores):
|
||||
auth = AuthManager()
|
||||
users = []
|
||||
for i in range(3):
|
||||
u = User(
|
||||
users.append(User(
|
||||
email=f"team_{uuid.uuid4().hex[:8]}@test.com",
|
||||
username=f"team_{uuid.uuid4().hex[:8]}",
|
||||
hashed_password=auth.hash_password("pass123"),
|
||||
role="store_user",
|
||||
is_active=True,
|
||||
)
|
||||
db.add(u)
|
||||
users.append(u)
|
||||
))
|
||||
db.add_all(users)
|
||||
db.flush()
|
||||
|
||||
# User 0 on store 0 and store 1 (should be counted once)
|
||||
|
||||
Reference in New Issue
Block a user