feat: multi-module improvements across merchant, store, i18n, and customer systems
All checks were successful
All checks were successful
- Fix platform-grouped merchant sidebar menu with core items at root level - Add merchant store management (detail page, create store, team page) - Fix store settings 500 error by removing dead stripe/API tab - Move onboarding translations to module-owned locale files - Fix onboarding banner i18n with server-side rendering + context inheritance - Refactor login language selectors to use languageSelector() function (LANG-002) - Move HTTPException handling to global exception handler in merchant routes (API-003) - Add language selector to all login pages and portal headers - Fix customer module: drop order stats from customer model, add to orders module - Fix admin menu config visibility for super admin platform context - Fix storefront auth and layout issues - Add missing i18n translations for onboarding steps (en/fr/de/lb) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
255
app/modules/tenancy/tests/unit/test_merchant_store_service.py
Normal file
255
app/modules/tenancy/tests/unit/test_merchant_store_service.py
Normal file
@@ -0,0 +1,255 @@
|
||||
# tests/unit/test_merchant_store_service.py
|
||||
"""Unit tests for MerchantStoreService."""
|
||||
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.tenancy.exceptions import (
|
||||
MerchantNotFoundException,
|
||||
StoreAlreadyExistsException,
|
||||
StoreNotFoundException,
|
||||
StoreValidationException,
|
||||
)
|
||||
from app.modules.tenancy.models import Merchant, Store
|
||||
from app.modules.tenancy.services.merchant_store_service import MerchantStoreService
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def merchant_owner(db, test_user):
|
||||
"""Create a merchant owned by test_user."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
merchant = Merchant(
|
||||
name=f"Test Merchant {unique_id}",
|
||||
owner_user_id=test_user.id,
|
||||
contact_email=f"merchant{unique_id}@test.com",
|
||||
is_active=True,
|
||||
is_verified=True,
|
||||
)
|
||||
db.add(merchant)
|
||||
db.commit()
|
||||
db.refresh(merchant)
|
||||
return merchant
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def other_merchant_owner(db):
|
||||
"""Create a separate merchant for ownership isolation tests."""
|
||||
from app.modules.tenancy.models.user import User
|
||||
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
user = User(
|
||||
email=f"other{unique_id}@test.com",
|
||||
username=f"other_{unique_id}",
|
||||
hashed_password="fakehash", # noqa: SEC001
|
||||
role="merchant_owner",
|
||||
is_active=True,
|
||||
)
|
||||
db.add(user)
|
||||
db.flush()
|
||||
|
||||
merchant = Merchant(
|
||||
name=f"Other Merchant {unique_id}",
|
||||
owner_user_id=user.id,
|
||||
contact_email=f"otherm{unique_id}@test.com",
|
||||
is_active=True,
|
||||
is_verified=True,
|
||||
)
|
||||
db.add(merchant)
|
||||
db.commit()
|
||||
db.refresh(merchant)
|
||||
return merchant
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def merchant_store(db, merchant_owner):
|
||||
"""Create a store under the merchant."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
store = Store(
|
||||
merchant_id=merchant_owner.id,
|
||||
store_code=f"MS_{unique_id}".upper(),
|
||||
subdomain=f"ms-{unique_id}".lower(),
|
||||
name=f"Merchant Store {unique_id}",
|
||||
description="A test store",
|
||||
is_active=True,
|
||||
is_verified=False,
|
||||
)
|
||||
db.add(store)
|
||||
db.commit()
|
||||
db.refresh(store)
|
||||
return store
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.tenancy
|
||||
class TestMerchantStoreServiceGetDetail:
|
||||
"""Tests for get_store_detail."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = MerchantStoreService()
|
||||
|
||||
def test_get_store_detail_success(self, db, merchant_owner, merchant_store):
|
||||
"""Test getting store detail with valid ownership."""
|
||||
result = self.service.get_store_detail(db, merchant_owner.id, merchant_store.id)
|
||||
|
||||
assert result["id"] == merchant_store.id
|
||||
assert result["store_code"] == merchant_store.store_code
|
||||
assert result["name"] == merchant_store.name
|
||||
assert result["is_active"] is True
|
||||
assert "platforms" in result
|
||||
|
||||
def test_get_store_detail_wrong_merchant(
|
||||
self, db, other_merchant_owner, merchant_store
|
||||
):
|
||||
"""Test that accessing another merchant's store raises not found."""
|
||||
with pytest.raises(StoreNotFoundException):
|
||||
self.service.get_store_detail(
|
||||
db, other_merchant_owner.id, merchant_store.id
|
||||
)
|
||||
|
||||
def test_get_store_detail_nonexistent(self, db, merchant_owner):
|
||||
"""Test getting a non-existent store."""
|
||||
with pytest.raises(StoreNotFoundException):
|
||||
self.service.get_store_detail(db, merchant_owner.id, 99999)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.tenancy
|
||||
class TestMerchantStoreServiceUpdate:
|
||||
"""Tests for update_store."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = MerchantStoreService()
|
||||
|
||||
def test_update_store_success(self, db, merchant_owner, merchant_store):
|
||||
"""Test updating store fields."""
|
||||
result = self.service.update_store(
|
||||
db,
|
||||
merchant_owner.id,
|
||||
merchant_store.id,
|
||||
{"name": "Updated Name", "contact_email": "new@test.com"},
|
||||
)
|
||||
db.commit()
|
||||
|
||||
assert result["name"] == "Updated Name"
|
||||
assert result["contact_email"] == "new@test.com"
|
||||
|
||||
def test_update_store_ignores_disallowed_fields(
|
||||
self, db, merchant_owner, merchant_store
|
||||
):
|
||||
"""Test that admin-only fields are ignored."""
|
||||
result = self.service.update_store(
|
||||
db,
|
||||
merchant_owner.id,
|
||||
merchant_store.id,
|
||||
{"is_active": False, "is_verified": True, "name": "OK Name"},
|
||||
)
|
||||
db.commit()
|
||||
|
||||
# is_active and is_verified should NOT change
|
||||
assert result["is_active"] is True
|
||||
assert result["is_verified"] is False
|
||||
assert result["name"] == "OK Name"
|
||||
|
||||
def test_update_store_wrong_merchant(
|
||||
self, db, other_merchant_owner, merchant_store
|
||||
):
|
||||
"""Test that updating another merchant's store raises not found."""
|
||||
with pytest.raises(StoreNotFoundException):
|
||||
self.service.update_store(
|
||||
db,
|
||||
other_merchant_owner.id,
|
||||
merchant_store.id,
|
||||
{"name": "Hacked"},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.tenancy
|
||||
class TestMerchantStoreServiceCreate:
|
||||
"""Tests for create_store."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = MerchantStoreService()
|
||||
|
||||
def test_create_store_success(self, db, merchant_owner):
|
||||
"""Test successful store creation."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
store_data = {
|
||||
"name": f"New Store {unique_id}",
|
||||
"store_code": f"NEW_{unique_id}",
|
||||
"subdomain": f"new-{unique_id}".lower(),
|
||||
"description": "Test description",
|
||||
"platform_ids": [],
|
||||
}
|
||||
|
||||
result = self.service.create_store(db, merchant_owner.id, store_data)
|
||||
db.commit()
|
||||
|
||||
assert result["name"] == store_data["name"]
|
||||
assert result["store_code"] == store_data["store_code"].upper()
|
||||
assert result["is_active"] is True
|
||||
assert result["is_verified"] is False
|
||||
|
||||
def test_create_store_duplicate_code(self, db, merchant_owner, merchant_store):
|
||||
"""Test creating store with duplicate store code."""
|
||||
store_data = {
|
||||
"name": "Another Store",
|
||||
"store_code": merchant_store.store_code,
|
||||
"subdomain": f"another-{uuid.uuid4().hex[:8]}",
|
||||
"description": None,
|
||||
"platform_ids": [],
|
||||
}
|
||||
|
||||
with pytest.raises(StoreAlreadyExistsException):
|
||||
self.service.create_store(db, merchant_owner.id, store_data)
|
||||
|
||||
def test_create_store_duplicate_subdomain(self, db, merchant_owner, merchant_store):
|
||||
"""Test creating store with duplicate subdomain."""
|
||||
store_data = {
|
||||
"name": "Another Store",
|
||||
"store_code": f"UNIQUE_{uuid.uuid4().hex[:8]}",
|
||||
"subdomain": merchant_store.subdomain,
|
||||
"description": None,
|
||||
"platform_ids": [],
|
||||
}
|
||||
|
||||
with pytest.raises(StoreValidationException):
|
||||
self.service.create_store(db, merchant_owner.id, store_data)
|
||||
|
||||
def test_create_store_nonexistent_merchant(self, db):
|
||||
"""Test creating store for non-existent merchant."""
|
||||
store_data = {
|
||||
"name": "No Merchant Store",
|
||||
"store_code": f"NM_{uuid.uuid4().hex[:8]}",
|
||||
"subdomain": f"nm-{uuid.uuid4().hex[:8]}",
|
||||
"description": None,
|
||||
"platform_ids": [],
|
||||
}
|
||||
|
||||
with pytest.raises(MerchantNotFoundException):
|
||||
self.service.create_store(db, 99999, store_data)
|
||||
|
||||
def test_create_store_creates_default_roles(self, db, merchant_owner):
|
||||
"""Test that default roles are created for new store."""
|
||||
from app.modules.tenancy.models.store import Role
|
||||
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
store_data = {
|
||||
"name": f"Roles Store {unique_id}",
|
||||
"store_code": f"ROLE_{unique_id}",
|
||||
"subdomain": f"role-{unique_id}".lower(),
|
||||
"description": None,
|
||||
"platform_ids": [],
|
||||
}
|
||||
|
||||
result = self.service.create_store(db, merchant_owner.id, store_data)
|
||||
db.commit()
|
||||
|
||||
roles = db.query(Role).filter(Role.store_id == result["id"]).all()
|
||||
role_names = {r.name for r in roles}
|
||||
|
||||
assert "Owner" in role_names
|
||||
assert "Manager" in role_names
|
||||
assert "Editor" in role_names
|
||||
assert "Viewer" in role_names
|
||||
Reference in New Issue
Block a user