feat(api+testing): select todo by member_id and id

- Added test code coverage with `pytest-cov`
- Added comments for member model functions
This commit is contained in:
minhtrannhat 2024-03-03 00:57:14 -05:00
parent 9abd4c4867
commit 60b6babc15
Signed by: minhtrannhat
GPG Key ID: E13CFA85C53F8062
6 changed files with 154 additions and 3 deletions

View File

@ -1,5 +1,16 @@
# Backend Technical Write Up # Backend Technical Write Up
## Setup
- Install `pdm`
- Install dependencies with `pdm sync`
- Run development backend with `pdm run dev`
- Run tests with `pdm run test`
### Setup for Developmentk
- run `eval $(pdm venv activate in-project)` to activate the virtual env.
## Structure ## Structure
- Use FastAPI's `router` to organize different API routes - Use FastAPI's `router` to organize different API routes

80
backend/pdm.lock generated
View File

@ -5,7 +5,7 @@
groups = ["default", "dev"] groups = ["default", "dev"]
strategy = ["cross_platform"] strategy = ["cross_platform"]
lock_version = "4.4.1" lock_version = "4.4.1"
content_hash = "sha256:334c1bb9213ebdcc88123f9dbc50d1c3e2548a23775c8b3c5c27bd01e5fcc9ff" content_hash = "sha256:a705bf9c8354b802c6aa86d8d35e46e83b9137cf3663c22e7f1442c07eecc38a"
[[package]] [[package]]
name = "annotated-types" name = "annotated-types"
@ -134,6 +134,70 @@ files = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
] ]
[[package]]
name = "coverage"
version = "7.4.3"
requires_python = ">=3.8"
summary = "Code coverage measurement for Python"
files = [
{file = "coverage-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47"},
{file = "coverage-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113"},
{file = "coverage-7.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe"},
{file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc"},
{file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3"},
{file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba"},
{file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079"},
{file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840"},
{file = "coverage-7.4.3-cp311-cp311-win32.whl", hash = "sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3"},
{file = "coverage-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e"},
{file = "coverage-7.4.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10"},
{file = "coverage-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328"},
{file = "coverage-7.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30"},
{file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7"},
{file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e"},
{file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003"},
{file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d"},
{file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a"},
{file = "coverage-7.4.3-cp312-cp312-win32.whl", hash = "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352"},
{file = "coverage-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914"},
{file = "coverage-7.4.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51"},
{file = "coverage-7.4.3.tar.gz", hash = "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52"},
]
[[package]]
name = "coverage"
version = "7.4.3"
extras = ["toml"]
requires_python = ">=3.8"
summary = "Code coverage measurement for Python"
dependencies = [
"coverage==7.4.3",
]
files = [
{file = "coverage-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47"},
{file = "coverage-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113"},
{file = "coverage-7.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe"},
{file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc"},
{file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3"},
{file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba"},
{file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079"},
{file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840"},
{file = "coverage-7.4.3-cp311-cp311-win32.whl", hash = "sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3"},
{file = "coverage-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e"},
{file = "coverage-7.4.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10"},
{file = "coverage-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328"},
{file = "coverage-7.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30"},
{file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7"},
{file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e"},
{file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003"},
{file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d"},
{file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a"},
{file = "coverage-7.4.3-cp312-cp312-win32.whl", hash = "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352"},
{file = "coverage-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914"},
{file = "coverage-7.4.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51"},
{file = "coverage-7.4.3.tar.gz", hash = "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52"},
]
[[package]] [[package]]
name = "dnspython" name = "dnspython"
version = "2.5.0" version = "2.5.0"
@ -492,6 +556,20 @@ files = [
{file = "pytest_asyncio-0.23.5-py3-none-any.whl", hash = "sha256:4e7093259ba018d58ede7d5315131d21923a60f8a6e9ee266ce1589685c89eac"}, {file = "pytest_asyncio-0.23.5-py3-none-any.whl", hash = "sha256:4e7093259ba018d58ede7d5315131d21923a60f8a6e9ee266ce1589685c89eac"},
] ]
[[package]]
name = "pytest-cov"
version = "4.1.0"
requires_python = ">=3.7"
summary = "Pytest plugin for measuring coverage."
dependencies = [
"coverage[toml]>=5.2.1",
"pytest>=4.6",
]
files = [
{file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"},
{file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"},
]
[[package]] [[package]]
name = "python-dateutil" name = "python-dateutil"
version = "2.8.2" version = "2.8.2"

View File

@ -16,6 +16,7 @@ dependencies = [
"redis>=5.0.1", "redis>=5.0.1",
"psycopg[pool]>=3.1.18", "psycopg[pool]>=3.1.18",
"fastapi-limiter>=0.1.6", "fastapi-limiter>=0.1.6",
"pytest-cov>=4.1.0",
] ]
requires-python = ">=3.11" requires-python = ">=3.11"
readme = "README.md" readme = "README.md"
@ -71,4 +72,4 @@ recreate-db-base = "python3 src/neo_neo_todo/utils/database.py"
migration-base = "python3 src/neo_neo_todo/migrations/0.py" migration-base = "python3 src/neo_neo_todo/migrations/0.py"
recreate-db = {composite = ["recreate-db-base", "migration-base"], env_file = "development.env"} recreate-db = {composite = ["recreate-db-base", "migration-base"], env_file = "development.env"}
generate-test-data = "python3 src/neo_neo_todo/migrations/test_data.py" generate-test-data = "python3 src/neo_neo_todo/migrations/test_data.py"
test = {composite = ["recreate-db-base", "migration-base", "generate-test-data", "pytest tests/"], env_file = "testing.env"} test = {composite = ["recreate-db-base", "migration-base", "generate-test-data", "pytest tests/ --cov=src.neo_neo_todo"], env_file = "testing.env"}

View File

@ -91,6 +91,11 @@ async def insert_member(
async def update_member_password( async def update_member_password(
db: AsyncConnectionPool, id: int, password_hash: str db: AsyncConnectionPool, id: int, password_hash: str
) -> bool: ) -> bool:
"""
Updates a member's password (their password_hash to be exact)
Returns a bool, True if successful and False if not
"""
async with db.connection() as conn: async with db.connection() as conn:
async with conn.cursor(row_factory=class_row(Member)) as curr: async with conn.cursor(row_factory=class_row(Member)) as curr:
query = sql.SQL( query = sql.SQL(
@ -113,6 +118,11 @@ async def update_member_password(
async def update_member_email_verified(db: AsyncConnectionPool, id: int) -> bool: async def update_member_email_verified(db: AsyncConnectionPool, id: int) -> bool:
"""
Updates a member's email verified status
Returns a bool, True if successful and False if not
"""
async with db.connection() as conn: async with db.connection() as conn:
async with conn.cursor(row_factory=class_row(Member)) as curr: async with conn.cursor(row_factory=class_row(Member)) as curr:
query = sql.SQL( query = sql.SQL(

View File

@ -18,6 +18,11 @@ class Todo:
async def select_todos( async def select_todos(
db: AsyncConnectionPool, member_id: int, complete: bool | None = None db: AsyncConnectionPool, member_id: int, complete: bool | None = None
) -> list[Todo]: ) -> list[Todo]:
"""
Select multiple Todo(s) from a member_id
Return a list of Todos (can be empty list)
"""
async with db.connection() as conn: async with db.connection() as conn:
async with conn.cursor(row_factory=class_row(Todo)) as curr: async with conn.cursor(row_factory=class_row(Todo)) as curr:
@ -43,3 +48,27 @@ async def select_todos(
todos = await curr.fetchall() todos = await curr.fetchall()
return todos return todos
async def select_todo(db: AsyncConnectionPool, id: int, member_id: int) -> Todo | None:
"""
Find exactly one todo task that matches id and member_id
in todos list in PostgreSQL database
Return a Todo object or None
"""
async with db.connection() as conn:
async with conn.cursor(row_factory=class_row(Todo)) as curr:
query = sql.SQL(
"""
SELECT id, complete, due, task
FROM todos
WHERE id = (%s) AND member_id = (%s)
"""
)
await curr.execute(query, (id, member_id))
member = await curr.fetchone()
return None if member is None else member

View File

@ -1,4 +1,6 @@
from src.neo_neo_todo.models.todo import select_todos import asyncio
from src.neo_neo_todo.models.todo import select_todo, select_todos
async def test_model_todo_select_todos(db_pool): async def test_model_todo_select_todos(db_pool):
@ -17,3 +19,23 @@ async def test_model_todo_select_todos(db_pool):
todos_list_non_existent_member_id = await select_todos(db_pool, 12341234) todos_list_non_existent_member_id = await select_todos(db_pool, 12341234)
assert len(todos_list_non_existent_member_id) == 0 assert len(todos_list_non_existent_member_id) == 0
async def test_model_todo_select_todo(db_pool):
todo_queries = [
select_todo(db_pool, member_id=1, id=1),
select_todo(db_pool, member_id=1, id=2),
select_todo(db_pool, member_id=1, id=3),
]
# Now you can await all the select_todo calls
todos = await asyncio.gather(*todo_queries)
for i in range(len(todos)):
assert todos[i] is not None
assert todos[i].id == (i + 1) # type: ignore
assert not todos[i].complete # type: ignore
non_existent_member_id_todo = await select_todo(db_pool, member_id=1234123, id=1)
assert non_existent_member_id_todo is None