feat(backend): FastAPI + libSQL + auth register/login/me + tests (TDD)
- FastAPI app with lifespan, CORS, health endpoint - SQLAlchemy 2.0 async with aiosqlite, Base/UUIDMixin/TenantMixin/TimestampMixin - Tenant and User models with multi-tenant isolation - Auth: register (creates tenant+user), login, /me endpoint - JWT HS256 tokens, bcrypt password hashing - Alembic async setup with initial migration - 6 passing tests (register, login, wrong password, me, no token, health) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
0
backend/tests/__init__.py
Normal file
0
backend/tests/__init__.py
Normal file
24
backend/tests/conftest.py
Normal file
24
backend/tests/conftest.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||
|
||||
from app.db.base import Base
|
||||
from app.db.session import get_db
|
||||
from app.main import app
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(autouse=True)
|
||||
async def setup_test_db():
|
||||
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
async def override_db():
|
||||
async with session_factory() as s:
|
||||
yield s
|
||||
|
||||
app.dependency_overrides[get_db] = override_db
|
||||
yield
|
||||
app.dependency_overrides.clear()
|
||||
await engine.dispose()
|
||||
105
backend/tests/test_auth.py
Normal file
105
backend/tests/test_auth.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import pytest
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
|
||||
from app.main import app
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_creates_tenant():
|
||||
async with AsyncClient(
|
||||
transport=ASGITransport(app=app), base_url="http://test"
|
||||
) as c:
|
||||
r = await c.post(
|
||||
"/api/auth/register",
|
||||
json={
|
||||
"email": "owner@service.ro",
|
||||
"password": "parola123",
|
||||
"tenant_name": "Service Ionescu",
|
||||
"telefon": "0722000000",
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert "access_token" in data
|
||||
assert data["plan"] == "trial"
|
||||
assert data["token_type"] == "bearer"
|
||||
assert data["tenant_id"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_returns_token():
|
||||
async with AsyncClient(
|
||||
transport=ASGITransport(app=app), base_url="http://test"
|
||||
) as c:
|
||||
await c.post(
|
||||
"/api/auth/register",
|
||||
json={
|
||||
"email": "test@s.ro",
|
||||
"password": "abc123",
|
||||
"tenant_name": "Test",
|
||||
"telefon": "0722",
|
||||
},
|
||||
)
|
||||
r = await c.post(
|
||||
"/api/auth/login",
|
||||
json={"email": "test@s.ro", "password": "abc123"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert "access_token" in r.json()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_wrong_password_returns_401():
|
||||
async with AsyncClient(
|
||||
transport=ASGITransport(app=app), base_url="http://test"
|
||||
) as c:
|
||||
r = await c.post(
|
||||
"/api/auth/login",
|
||||
json={"email": "x@x.ro", "password": "wrong"},
|
||||
)
|
||||
assert r.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_me_returns_user_info():
|
||||
async with AsyncClient(
|
||||
transport=ASGITransport(app=app), base_url="http://test"
|
||||
) as c:
|
||||
reg = await c.post(
|
||||
"/api/auth/register",
|
||||
json={
|
||||
"email": "me@test.ro",
|
||||
"password": "pass123",
|
||||
"tenant_name": "My Service",
|
||||
"telefon": "0733",
|
||||
},
|
||||
)
|
||||
token = reg.json()["access_token"]
|
||||
r = await c.get(
|
||||
"/api/auth/me",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert data["email"] == "me@test.ro"
|
||||
assert data["rol"] == "owner"
|
||||
assert data["plan"] == "trial"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_me_without_token_returns_403():
|
||||
async with AsyncClient(
|
||||
transport=ASGITransport(app=app), base_url="http://test"
|
||||
) as c:
|
||||
r = await c.get("/api/auth/me")
|
||||
assert r.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_endpoint():
|
||||
async with AsyncClient(
|
||||
transport=ASGITransport(app=app), base_url="http://test"
|
||||
) as c:
|
||||
r = await c.get("/api/health")
|
||||
assert r.status_code == 200
|
||||
assert r.json() == {"status": "ok"}
|
||||
Reference in New Issue
Block a user