Add OnboardingProviderProtocol so modules declare their own post-signup onboarding steps. The core OnboardingAggregator discovers enabled providers and exposes a dashboard API (GET /dashboard/onboarding). A session-scoped banner on the store dashboard shows a checklist that guides merchants through setup without blocking signup. Signup is simplified from 4 steps to 3 (Plan → Account → Payment): store creation is merged into account creation, store language is captured from the user's browsing language, and platform-specific template branching is removed. Includes 47 unit and integration tests covering all new providers, the aggregator, the API endpoint, and the signup service changes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
116 lines
3.8 KiB
Python
116 lines
3.8 KiB
Python
# app/modules/tenancy/tests/unit/test_tenancy_onboarding.py
|
|
"""Unit tests for TenancyOnboardingProvider."""
|
|
|
|
import uuid
|
|
|
|
import pytest
|
|
|
|
from app.modules.tenancy.models import Merchant, Store, User
|
|
from app.modules.tenancy.services.tenancy_onboarding import TenancyOnboardingProvider
|
|
|
|
|
|
@pytest.fixture
|
|
def onb_owner(db):
|
|
"""Create a store owner for onboarding tests."""
|
|
from middleware.auth import AuthManager
|
|
|
|
auth = AuthManager()
|
|
user = User(
|
|
email=f"onbowner_{uuid.uuid4().hex[:8]}@test.com",
|
|
username=f"onbowner_{uuid.uuid4().hex[:8]}",
|
|
hashed_password=auth.hash_password("pass123"),
|
|
role="merchant_owner",
|
|
is_active=True,
|
|
)
|
|
db.add(user)
|
|
db.commit()
|
|
db.refresh(user)
|
|
return user
|
|
|
|
|
|
@pytest.fixture
|
|
def onb_merchant(db, onb_owner):
|
|
"""Create a merchant for onboarding tests."""
|
|
merchant = Merchant(
|
|
name="Onboarding Test Merchant",
|
|
owner_user_id=onb_owner.id,
|
|
contact_email=onb_owner.email,
|
|
is_active=True,
|
|
is_verified=True,
|
|
)
|
|
db.add(merchant)
|
|
db.commit()
|
|
db.refresh(merchant)
|
|
return merchant
|
|
|
|
|
|
@pytest.fixture
|
|
def onb_store(db, onb_merchant):
|
|
"""Create a store with no description and no logo."""
|
|
uid = uuid.uuid4().hex[:8]
|
|
store = Store(
|
|
merchant_id=onb_merchant.id,
|
|
store_code=f"ONBSTORE_{uid.upper()}",
|
|
subdomain=f"onbstore{uid.lower()}",
|
|
name="Onboarding Test Store",
|
|
is_active=True,
|
|
is_verified=True,
|
|
)
|
|
db.add(store)
|
|
db.commit()
|
|
db.refresh(store)
|
|
return store
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.tenancy
|
|
class TestTenancyOnboardingProvider:
|
|
"""Tests for TenancyOnboardingProvider."""
|
|
|
|
def setup_method(self):
|
|
self.provider = TenancyOnboardingProvider()
|
|
|
|
def test_category(self):
|
|
"""Returns 'tenancy' as the onboarding category."""
|
|
assert self.provider.onboarding_category == "tenancy"
|
|
|
|
def test_get_onboarding_steps_returns_one_step(self):
|
|
"""Returns exactly one step: customize_store."""
|
|
steps = self.provider.get_onboarding_steps()
|
|
assert len(steps) == 1
|
|
assert steps[0].key == "tenancy.customize_store"
|
|
assert steps[0].route_template == "/store/{store_code}/settings"
|
|
assert steps[0].order == 100
|
|
|
|
def test_incomplete_when_no_description_no_logo(self, db, onb_store):
|
|
"""Step is not completed when store has no description and no logo."""
|
|
assert onb_store.description is None or onb_store.description.strip() == ""
|
|
result = self.provider.is_step_completed(db, onb_store.id, "tenancy.customize_store")
|
|
assert result is False
|
|
|
|
def test_completed_when_store_has_description(self, db, onb_store):
|
|
"""Step is completed when store has a non-empty description."""
|
|
onb_store.description = "We sell great coffee!"
|
|
db.commit()
|
|
|
|
result = self.provider.is_step_completed(db, onb_store.id, "tenancy.customize_store")
|
|
assert result is True
|
|
|
|
def test_incomplete_when_description_is_whitespace(self, db, onb_store):
|
|
"""Step is not completed when description is whitespace only."""
|
|
onb_store.description = " "
|
|
db.commit()
|
|
|
|
result = self.provider.is_step_completed(db, onb_store.id, "tenancy.customize_store")
|
|
assert result is False
|
|
|
|
def test_incomplete_for_nonexistent_store(self, db):
|
|
"""Returns False for a store ID that doesn't exist."""
|
|
result = self.provider.is_step_completed(db, 999999, "tenancy.customize_store")
|
|
assert result is False
|
|
|
|
def test_incomplete_for_unknown_step_key(self, db, onb_store):
|
|
"""Returns False for an unrecognized step key."""
|
|
result = self.provider.is_step_completed(db, onb_store.id, "tenancy.unknown_step")
|
|
assert result is False
|