From 49c83fa2870a411ea2cc05a7eaa082c1721cf8cc Mon Sep 17 00:00:00 2001 From: minhtrannhat Date: Fri, 1 Mar 2024 20:14:20 -0500 Subject: [PATCH] feat(api+testing): select member by email - Reorganized folder structure so routes live in the same folder - Set up pytest fixtures for future use --- backend/pyproject.toml | 4 +++ backend/src/neo_neo_todo/main.py | 3 +- backend/src/neo_neo_todo/models/member.py | 27 ++++++++++++++++++ .../{control => routes}/__init__.py | 0 .../{control => routes}/control.py | 3 +- .../{sessions => routes}/sessions.py | 3 +- backend/src/neo_neo_todo/sessions/__init__.py | 0 backend/tests/conftest.py | 28 +++++++++++++++++++ backend/tests/test_control.py | 11 ++------ backend/tests/test_model_member.py | 7 +++++ 10 files changed, 72 insertions(+), 14 deletions(-) rename backend/src/neo_neo_todo/{control => routes}/__init__.py (100%) rename backend/src/neo_neo_todo/{control => routes}/control.py (67%) rename backend/src/neo_neo_todo/{sessions => routes}/sessions.py (80%) delete mode 100644 backend/src/neo_neo_todo/sessions/__init__.py create mode 100644 backend/tests/conftest.py create mode 100644 backend/tests/test_model_member.py diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 0dc7ccb..549c424 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -46,6 +46,10 @@ target-version = ["py311"] addopts = "--showlocals" asyncio_mode = "auto" pythonpath = ["src"] +filterwarnings = [ + "error", + 'ignore::DeprecationWarning', +] [tool.pdm.scripts] diff --git a/backend/src/neo_neo_todo/main.py b/backend/src/neo_neo_todo/main.py index b94b33d..388ebc8 100644 --- a/backend/src/neo_neo_todo/main.py +++ b/backend/src/neo_neo_todo/main.py @@ -4,8 +4,7 @@ import redis.asyncio as redis from fastapi import FastAPI from fastapi_limiter import FastAPILimiter -from src.neo_neo_todo.control import control -from src.neo_neo_todo.sessions import sessions +from src.neo_neo_todo.routes import control, sessions from src.neo_neo_todo.utils.database import pool diff --git a/backend/src/neo_neo_todo/models/member.py b/backend/src/neo_neo_todo/models/member.py index a869519..651d233 100644 --- a/backend/src/neo_neo_todo/models/member.py +++ b/backend/src/neo_neo_todo/models/member.py @@ -1,6 +1,10 @@ from dataclasses import dataclass from datetime import datetime +from psycopg import sql +from psycopg.rows import class_row +from psycopg_pool import AsyncConnectionPool + @dataclass class Member: @@ -9,3 +13,26 @@ class Member: password_hash: str created: datetime email_verified: datetime | None + + +async def select_member_by_email(db: AsyncConnectionPool, email: str) -> Member | None: + """ + Find exactly one email match in users list in PostgreSQL database + """ + async with db.connection() as conn: + async with conn.cursor(row_factory=class_row(Member)) as curr: + query = sql.SQL( + """ + SELECT id, email, password_hash, created, email_verified + FROM members + WHERE LOWER(email) = LOWER(%s); + """ + ) + + await curr.execute(query, (email,)) + + print(query.as_string(conn)) + + member = await curr.fetchone() + + return None if member is None else member diff --git a/backend/src/neo_neo_todo/control/__init__.py b/backend/src/neo_neo_todo/routes/__init__.py similarity index 100% rename from backend/src/neo_neo_todo/control/__init__.py rename to backend/src/neo_neo_todo/routes/__init__.py diff --git a/backend/src/neo_neo_todo/control/control.py b/backend/src/neo_neo_todo/routes/control.py similarity index 67% rename from backend/src/neo_neo_todo/control/control.py rename to backend/src/neo_neo_todo/routes/control.py index e707b36..5b87e0c 100644 --- a/backend/src/neo_neo_todo/control/control.py +++ b/backend/src/neo_neo_todo/routes/control.py @@ -1,5 +1,4 @@ from fastapi import APIRouter -from starlette.requests import Request router = APIRouter( prefix="/control", @@ -8,5 +7,5 @@ router = APIRouter( @router.get("/ping") -async def ping(request: Request): +async def ping(): return {"ping": "pong"} diff --git a/backend/src/neo_neo_todo/sessions/sessions.py b/backend/src/neo_neo_todo/routes/sessions.py similarity index 80% rename from backend/src/neo_neo_todo/sessions/sessions.py rename to backend/src/neo_neo_todo/routes/sessions.py index aa98312..8f4e133 100644 --- a/backend/src/neo_neo_todo/sessions/sessions.py +++ b/backend/src/neo_neo_todo/routes/sessions.py @@ -1,6 +1,5 @@ from fastapi import APIRouter, Depends from fastapi_limiter.depends import RateLimiter -from starlette.requests import Request from src.neo_neo_todo.models.session import LoginData @@ -11,7 +10,7 @@ router = APIRouter( @router.post("", dependencies=[Depends(RateLimiter(times=100, seconds=600))]) -async def login(request: Request, login_data: LoginData): +async def login(login_data: LoginData): """ Login to the todo app diff --git a/backend/src/neo_neo_todo/sessions/__init__.py b/backend/src/neo_neo_todo/sessions/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py new file mode 100644 index 0000000..c3b4478 --- /dev/null +++ b/backend/tests/conftest.py @@ -0,0 +1,28 @@ +import os + +import pytest +from fastapi.testclient import TestClient +from psycopg_pool import AsyncConnectionPool + +from src.neo_neo_todo.main import app + + +@pytest.fixture +async def client(): + yield TestClient(app) + + +@pytest.fixture +async def db_pool(): + try: + postgres_db_url = os.environ["TODO_DB_DATABASE_URL"] + except KeyError: + raise KeyError("Can't find postgres DB URL") + + pool = AsyncConnectionPool(postgres_db_url, open=False) + + await pool.open() + + yield pool + + await pool.close() diff --git a/backend/tests/test_control.py b/backend/tests/test_control.py index 5b13f80..17daa69 100644 --- a/backend/tests/test_control.py +++ b/backend/tests/test_control.py @@ -1,12 +1,7 @@ -import pytest -from httpx import AsyncClient - -from src.neo_neo_todo.main import app +from fastapi.testclient import TestClient -@pytest.mark.anyio -async def test_ping(): - async with AsyncClient(app=app, base_url="http://test") as ac: - response = await ac.get("/control/ping") +async def test_ping(client: TestClient): + response = client.get("/control/ping") assert response.status_code == 200 assert response.json() == {"ping": "pong"} diff --git a/backend/tests/test_model_member.py b/backend/tests/test_model_member.py new file mode 100644 index 0000000..e44d476 --- /dev/null +++ b/backend/tests/test_model_member.py @@ -0,0 +1,7 @@ +from src.neo_neo_todo.models.member import select_member_by_email + + +async def test_model_member(db_pool): + member = await select_member_by_email(db_pool, "member@todo.test") + assert member is not None + assert member.email == "member@todo.test"