feat: initial commit — HermesMessages SaaS platform
Backend (FastAPI + Python 3.12): - Multi-tenant auth with JWT: login, register, refresh, Meta OAuth - Business & BusinessConfig management - WhatsApp webhook with HMAC signature verification - Bot engine powered by Claude AI - Calendar availability with Redis caching - Reservations CRUD with status management - Dashboard analytics (stats, agenda, peak hours) - Billing & plan management - Admin panel with platform-wide stats - Async bcrypt via asyncio.to_thread - IntegrityError handling for concurrent registration race conditions Frontend (React 18 + Vite + Tailwind CSS): - Multi-step guided registration form with helper text on every field - Login page with show/hide password toggle - Protected routes with AuthContext - Dashboard with stats cards, bar chart, and daily agenda - Reservations list with search, filters, and inline status actions - Calendar with weekly view, slot availability, and date blocking - Config page: business info, schedules, bot personality - Billing page with plan comparison and usage bar Design system: - Bricolage Grotesque + DM Sans typography - Emerald primary palette with semantic color tokens - scale(0.97) button press feedback, ease-out animations - Skeleton loaders, stagger animations, prefers-reduced-motion support - Accessible: aria-labels, visible focus rings, 4.5:1 contrast Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
115
backend/tests/conftest.py
Normal file
115
backend/tests/conftest.py
Normal file
@ -0,0 +1,115 @@
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from app.core.database import Base, get_db
|
||||
from app.core.redis import get_redis
|
||||
from app.main import app
|
||||
|
||||
# Importar todos los modelos para que Base.metadata los registre
|
||||
from app.modules.auth.models import User # noqa
|
||||
from app.modules.business.models import Business, BusinessConfig # noqa
|
||||
from app.modules.reservations.models import Reservation # noqa
|
||||
|
||||
TEST_DB_URL = "postgresql+asyncpg://postgres:postgres@localhost:5432/hermesmessages_test"
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="session")
|
||||
async def engine():
|
||||
_engine = create_async_engine(TEST_DB_URL)
|
||||
async with _engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
yield _engine
|
||||
async with _engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
await _engine.dispose()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def db(engine) -> AsyncSession:
|
||||
session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
||||
async with session_factory() as session:
|
||||
yield session
|
||||
await session.rollback()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
def redis_mock() -> AsyncMock:
|
||||
mock = AsyncMock()
|
||||
mock.get = AsyncMock(return_value=None)
|
||||
mock.setex = AsyncMock()
|
||||
mock.delete = AsyncMock()
|
||||
mock.aclose = AsyncMock()
|
||||
return mock
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def client(db: AsyncSession, redis_mock: AsyncMock) -> AsyncClient:
|
||||
app.dependency_overrides[get_db] = lambda: db
|
||||
app.dependency_overrides[get_redis] = lambda: redis_mock
|
||||
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
|
||||
yield c
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def client_no_db(redis_mock: AsyncMock) -> AsyncClient:
|
||||
"""Cliente HTTP sin fixture de base de datos — para endpoints que no usan DB."""
|
||||
app.dependency_overrides[get_redis] = lambda: redis_mock
|
||||
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
|
||||
yield c
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
# --- Helpers para crear datos de prueba ---
|
||||
|
||||
async def make_business(db: AsyncSession, **kwargs):
|
||||
from app.modules.business.models import Business, BusinessConfig
|
||||
from datetime import time
|
||||
|
||||
business = Business(
|
||||
name=kwargs.get("name", "Restaurante Test"),
|
||||
type=kwargs.get("type", "restaurant"),
|
||||
timezone="UTC",
|
||||
status=kwargs.get("status", "active"),
|
||||
plan=kwargs.get("plan", "basic"),
|
||||
whatsapp_phone_number_id=kwargs.get("phone_number_id", "12345"),
|
||||
whatsapp_access_token=kwargs.get("access_token", "test-token"),
|
||||
)
|
||||
db.add(business)
|
||||
await db.flush()
|
||||
|
||||
config = BusinessConfig(
|
||||
business_id=business.id,
|
||||
open_days=[0, 1, 2, 3, 4], # lunes a viernes
|
||||
open_time=time(9, 0),
|
||||
close_time=time(18, 0),
|
||||
slot_duration=60,
|
||||
max_per_slot=1,
|
||||
blocked_dates=[],
|
||||
assistant_name="Hermes",
|
||||
tone="formal",
|
||||
)
|
||||
db.add(config)
|
||||
await db.commit()
|
||||
await db.refresh(business)
|
||||
return business
|
||||
|
||||
|
||||
async def make_user(db: AsyncSession, business_id: int, **kwargs):
|
||||
from app.modules.auth.models import User
|
||||
from app.core.security import hash_password
|
||||
|
||||
user = User(
|
||||
business_id=business_id,
|
||||
email=kwargs.get("email", "owner@test.com"),
|
||||
hashed_password=hash_password(kwargs.get("password", "secret123")),
|
||||
role=kwargs.get("role", "owner"),
|
||||
)
|
||||
db.add(user)
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
return user
|
||||
Reference in New Issue
Block a user