diff --git a/backend/dwf/api/deps.py b/backend/dwf/api/deps.py index e665fd6..120b6ab 100644 --- a/backend/dwf/api/deps.py +++ b/backend/dwf/api/deps.py @@ -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() @@ -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: diff --git a/backend/dwf/api/routers/auth.py b/backend/dwf/api/routers/auth.py index da41545..f441ddf 100644 --- a/backend/dwf/api/routers/auth.py +++ b/backend/dwf/api/routers/auth.py @@ -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 @@ -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: @@ -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) @@ -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: diff --git a/backend/dwf/main.py b/backend/dwf/main.py index c31463f..c40aaa9 100644 --- a/backend/dwf/main.py +++ b/backend/dwf/main.py @@ -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) @@ -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 diff --git a/backend/pyproject.toml b/backend/pyproject.toml index e75a707..a11bdb1 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -25,6 +25,7 @@ dev = [ ] [tool.pytest.ini_options] +asyncio_mode = "auto" testpaths = ["tests"] pythonpath = ["."] diff --git a/backend/test.db b/backend/test.db new file mode 100644 index 0000000..6e73964 Binary files /dev/null and b/backend/test.db differ diff --git a/tests/__init__.py b/backend/tests/__init__.py similarity index 100% rename from tests/__init__.py rename to backend/tests/__init__.py diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py new file mode 100644 index 0000000..526c564 --- /dev/null +++ b/backend/tests/conftest.py @@ -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() diff --git a/tests/simulation/__init__.py b/backend/tests/simulation/__init__.py similarity index 100% rename from tests/simulation/__init__.py rename to backend/tests/simulation/__init__.py diff --git a/tests/conftest.py b/backend/tests/unit/__init__.py similarity index 100% rename from tests/conftest.py rename to backend/tests/unit/__init__.py diff --git a/backend/tests/unit/test_auth.py b/backend/tests/unit/test_auth.py new file mode 100644 index 0000000..374e10e --- /dev/null +++ b/backend/tests/unit/test_auth.py @@ -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