Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion backend/dwf/api/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")


def get_settings(request: Request) -> Settings:
return request.app.state.settings


async def get_db(request: Request) -> AsyncGenerator[AsyncSession, None]:
session_factory = request.app.state.session_factory
session = session_factory()
Expand All @@ -24,7 +28,7 @@ async def get_db(request: Request) -> AsyncGenerator[AsyncSession, None]:
async def get_current_user(
token: str = Depends(oauth2_scheme),
db: AsyncSession = Depends(get_db),
settings: Settings = Depends(),
settings: Settings = Depends(get_settings),
) -> User:
auth_service = AuthService(db, settings)
try:
Expand Down
8 changes: 4 additions & 4 deletions backend/dwf/api/routers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from pydantic import BaseModel, EmailStr
from sqlalchemy.ext.asyncio import AsyncSession

from dwf.api.deps import get_db
from dwf.api.deps import get_db, get_settings
from dwf.services.auth_service import AuthService
from dwf.settings import Settings

Expand Down Expand Up @@ -33,7 +33,7 @@ class RefreshRequest(BaseModel):
async def register(
request: RegisterRequest,
db: AsyncSession = Depends(get_db),
settings: Settings = Depends(),
settings: Settings = Depends(get_settings),
):
auth_service = AuthService(db, settings)
try:
Expand All @@ -51,7 +51,7 @@ async def register(
async def login(
request: LoginRequest,
db: AsyncSession = Depends(get_db),
settings: Settings = Depends(),
settings: Settings = Depends(get_settings),
):
auth_service = AuthService(db, settings)
user = await auth_service.login(request.email, request.password)
Expand All @@ -69,7 +69,7 @@ async def login(
@router.post("/refresh")
async def refresh(
request: RefreshRequest,
settings: Settings = Depends(),
settings: Settings = Depends(get_settings),
):
auth_service = AuthService(None, settings)
try:
Expand Down
3 changes: 2 additions & 1 deletion backend/dwf/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ async def lifespan(app: FastAPI):
settings: Settings = app.state.settings
setup_logging(settings.log_level)


engine = create_engine(settings.database_url)
app.state.engine = engine
app.state.session_factory = create_session_factory(engine)
Expand All @@ -40,8 +39,10 @@ def create_app(settings: Settings | None = None) -> FastAPI:
app.state.settings = settings

from dwf.api.routers.health import router as health_router
from dwf.api.routers.auth import router as auth_router

app.include_router(health_router)
app.include_router(auth_router)

return app

Expand Down
1 change: 1 addition & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ dev = [
]

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
pythonpath = ["."]

Expand Down
Binary file added backend/test.db
Binary file not shown.
File renamed without changes.
52 changes: 52 additions & 0 deletions backend/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import pytest
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine

from dwf.infrastructure.database.models import Base
from dwf.main import create_app
from dwf.api.deps import get_db
from dwf.settings import Settings

TEST_DATABASE_URL = "sqlite+aiosqlite:///test.db"


@pytest.fixture()
async def engine():
engine = create_async_engine(TEST_DATABASE_URL)
async with engine.begin() as conn:
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.fixture()
async def db_session(engine):
session_factory = async_sessionmaker(
bind=engine, class_=AsyncSession, expire_on_commit=False
)
async with session_factory() as session:
yield session


@pytest.fixture()
async def client(db_session):
settings = Settings(
app_name="test",
database_url=TEST_DATABASE_URL,
jwt_secret_key="test-secret-key",
log_level="info",
)
app = create_app(settings)

async def override_get_db():
yield db_session

app.dependency_overrides[get_db] = override_get_db

transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac

app.dependency_overrides.clear()
File renamed without changes.
File renamed without changes.
97 changes: 97 additions & 0 deletions backend/tests/unit/test_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
async def test_register_success(client):
response = await client.post(
"/auth/register",
json={"email": "test@example.com", "password": "testpassword"},
)
assert response.status_code == 201
data = response.json()
assert "access_token" in data
assert "refresh_token" in data
assert data["token_type"] == "bearer"


async def test_register_duplicate_email(client):
await client.post(
"/auth/register",
json={"email": "test@example.com", "password": "testpassword"},
)
response = await client.post(
"/auth/register",
json={"email": "test@example.com", "password": "otherpassword"},
)
assert response.status_code == 409


async def test_register_invalid_email(client):
response = await client.post(
"/auth/register",
json={"email": "not-an-email", "password": "testpassword"},
)
assert response.status_code == 422


async def test_login_success(client):
await client.post(
"/auth/register",
json={"email": "test@example.com", "password": "testpassword"},
)
response = await client.post(
"/auth/login",
json={"email": "test@example.com", "password": "testpassword"},
)
assert response.status_code == 200
data = response.json()
assert "access_token" in data
assert "refresh_token" in data
assert data["token_type"] == "bearer"


async def test_login_wrong_password(client):
await client.post(
"/auth/register",
json={"email": "test@example.com", "password": "testpassword"},
)
response = await client.post(
"/auth/login",
json={"email": "test@example.com", "password": "wrongpassword"},
)
assert response.status_code == 401


async def test_login_nonexistent_user(client):
response = await client.post(
"/auth/login",
json={"email": "nobody@example.com", "password": "testpassword"},
)
assert response.status_code == 401


async def test_refresh_token(client):
reg = await client.post(
"/auth/register",
json={"email": "test@example.com", "password": "testpassword"},
)
refresh_token = reg.json()["refresh_token"]

response = await client.post(
"/auth/refresh",
json={"refresh_token": refresh_token},
)
assert response.status_code == 200
data = response.json()
assert "access_token" in data
assert "refresh_token" in data


async def test_refresh_with_access_token_rejected(client):
reg = await client.post(
"/auth/register",
json={"email": "test@example.com", "password": "testpassword"},
)
access_token = reg.json()["access_token"]

response = await client.post(
"/auth/refresh",
json={"refresh_token": access_token},
)
assert response.status_code == 401
Loading