feat(middleware): harden routing with fail-closed policy, custom subdomain management, and perf fixes
Some checks failed
CI / pytest (push) Waiting to run
CI / ruff (push) Successful in 12s
CI / validate (push) Successful in 26s
CI / dependency-scanning (push) Successful in 31s
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled

- 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:
2026-03-15 18:13:01 +01:00
parent 07fab01f6a
commit 540205402f
38 changed files with 1827 additions and 134 deletions

View 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)

View File

@@ -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)