Files
gogo-telefon/tests/conftest.py
Senad Uka e855650f09 M1: backend core + proposal engine
- FastAPI skeleton, SQLAlchemy models (§13), Alembic initial migration
- SchedulingProvider interface with google_calendar (free/busy read-only),
  partner_api (Appendix B client) and mock implementations
- Proposal engine: create → provider-routed delivery → owner actions
  (resolve/confirm+SMS/reject) → expiry + reminders (§9)
- Signed single-use action links, .ics METHOD:REQUEST attachment
- Partner outcome webhook with HMAC verification + polling fallback
- SmsProvider (console) with Bosnian templates (§5.5), EmailProvider (console/SMTP)
- Fake partner API server in tests/ — Appendix B reference implementation
- 43 tests: slot math, proposal lifecycle, action links, partner contract

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 09:45:06 +02:00

166 lines
4.8 KiB
Python

"""Shared fixtures: file-backed SQLite DB per test, console email/SMS capture,
seeded tenant, fake partner wiring."""
from __future__ import annotations
import os
import uuid
os.environ.setdefault("GOGO_ENV", "test")
os.environ.setdefault("GOGO_SECRET_KEY", "test-secret-key-for-tests-only")
os.environ.setdefault("GOGO_BASE_URL", "http://testserver")
import httpx # noqa: E402
import pytest # noqa: E402
import gogo.db as db # noqa: E402
from gogo.config import get_settings # noqa: E402
from gogo.crypto import encrypt # noqa: E402
from gogo.email.sender import ConsoleEmailProvider, set_email_provider # noqa: E402
from gogo.hours import DEFAULT_WORKING_HOURS # noqa: E402
from gogo.models import ProviderConfig, Service, Tenant # noqa: E402
from gogo.scheduling.mock import MockProvider # noqa: E402
from gogo.scheduling.partner_api import PartnerApiProvider # noqa: E402
from gogo.sms.base import ConsoleSmsProvider, set_sms_provider # noqa: E402
from tests import fake_partner # noqa: E402
@pytest.fixture
async def session(tmp_path):
"""Fresh file-backed SQLite DB; the app's get_session sees the same file."""
db_path = tmp_path / "test.db"
os.environ["GOGO_DATABASE_URL"] = f"sqlite+aiosqlite:///{db_path}"
get_settings.cache_clear()
db.reset_engine()
engine = db.get_engine()
async with engine.begin() as conn:
await conn.run_sync(db.Base.metadata.create_all)
async with db.get_sessionmaker()() as s:
yield s
await engine.dispose()
db.reset_engine()
@pytest.fixture
def emails():
provider = ConsoleEmailProvider()
set_email_provider(provider)
yield provider.sent
set_email_provider(None)
@pytest.fixture
def sms():
provider = ConsoleSmsProvider()
set_sms_provider(provider)
yield provider.sent
set_sms_provider(None)
@pytest.fixture
def clean_mock_provider():
MockProvider.busy_blocks = {}
MockProvider.now_override = None
yield MockProvider
MockProvider.busy_blocks = {}
MockProvider.now_override = None
@pytest.fixture
async def tenant(session) -> Tenant:
"""Seeded mock-provider tenant 'Salon Merima' with two services."""
t = Tenant(
name="Salon Merima",
slug="salon-merima",
city="Banja Luka",
working_hours=DEFAULT_WORKING_HOURS,
scheduling_provider="mock",
notify_emails=["merima@example.ba"],
)
session.add(t)
await session.flush()
session.add_all(
[
Service(
tenant_id=t.id, name="Šišanje i feniranje", duration_min=45,
price_min=25, price_max=35,
),
Service(
tenant_id=t.id, name="Pedikir", duration_min=60,
price_min=30, price_max=30, home_visit=True,
),
]
)
await session.commit()
return t
@pytest.fixture
async def partner_tenant(session) -> Tenant:
"""Tenant wired to the fake partner API via ASGI transport."""
t = Tenant(
name="Salon Aida",
slug="salon-aida",
city="Sarajevo",
working_hours=DEFAULT_WORKING_HOURS,
scheduling_provider="partner_api",
notify_emails=[],
)
session.add(t)
await session.flush()
session.add(
ProviderConfig(
tenant_id=t.id,
provider_type="partner_api",
config={
"base_url": "http://partner.test",
"api_key_encrypted": encrypt(fake_partner.API_KEY),
"webhook_secret_encrypted": encrypt(fake_partner.WEBHOOK_SECRET),
"catalog_sync": True,
"email_to_owner": False,
"polling_fallback": False,
},
)
)
session.add(
Service(
tenant_id=t.id, name="Manikir", duration_min=45,
price_min=20, price_max=25, partner_service_id="SRV-12",
)
)
await session.commit()
return t
@pytest.fixture
def wire_fake_partner(monkeypatch):
"""Point PartnerApiProvider's HTTP client at the fake partner ASGI app."""
fake_partner.state.reset()
def factory(timeout):
return httpx.AsyncClient(
transport=httpx.ASGITransport(app=fake_partner.app),
base_url="http://partner.test",
timeout=timeout,
)
monkeypatch.setattr(PartnerApiProvider, "client_factory", staticmethod(factory))
yield fake_partner.state
fake_partner.state.reset()
@pytest.fixture
async def gogo_client(session):
"""HTTP client against the Gogo app (shares the test DB via GOGO_DATABASE_URL)."""
from gogo.main import create_app
app = create_app()
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=app), base_url="http://testserver"
) as client:
yield client
def make_uuid() -> str:
return str(uuid.uuid4())