diff --git a/backend/README.md b/backend/README.md index 7193195..eb08029 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,5 +1,16 @@ # 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 - Use FastAPI's `router` to organize different API routes diff --git a/backend/pdm.lock b/backend/pdm.lock index f1db5c4..809be35 100644 --- a/backend/pdm.lock +++ b/backend/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["cross_platform"] lock_version = "4.4.1" -content_hash = "sha256:334c1bb9213ebdcc88123f9dbc50d1c3e2548a23775c8b3c5c27bd01e5fcc9ff" +content_hash = "sha256:a705bf9c8354b802c6aa86d8d35e46e83b9137cf3663c22e7f1442c07eecc38a" [[package]] name = "annotated-types" @@ -134,6 +134,70 @@ files = [ {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]] name = "dnspython" version = "2.5.0" @@ -492,6 +556,20 @@ files = [ {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]] name = "python-dateutil" version = "2.8.2" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 549c424..128d098 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -16,6 +16,7 @@ dependencies = [ "redis>=5.0.1", "psycopg[pool]>=3.1.18", "fastapi-limiter>=0.1.6", + "pytest-cov>=4.1.0", ] requires-python = ">=3.11" 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" recreate-db = {composite = ["recreate-db-base", "migration-base"], env_file = "development.env"} 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"} diff --git a/backend/src/neo_neo_todo/models/member.py b/backend/src/neo_neo_todo/models/member.py index 78754f9..8f787f7 100644 --- a/backend/src/neo_neo_todo/models/member.py +++ b/backend/src/neo_neo_todo/models/member.py @@ -91,6 +91,11 @@ async def insert_member( async def update_member_password( db: AsyncConnectionPool, id: int, password_hash: str ) -> 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 conn.cursor(row_factory=class_row(Member)) as curr: query = sql.SQL( @@ -113,6 +118,11 @@ async def update_member_password( 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 conn.cursor(row_factory=class_row(Member)) as curr: query = sql.SQL( diff --git a/backend/src/neo_neo_todo/models/todo.py b/backend/src/neo_neo_todo/models/todo.py index 0c9cc14..bab997e 100644 --- a/backend/src/neo_neo_todo/models/todo.py +++ b/backend/src/neo_neo_todo/models/todo.py @@ -18,6 +18,11 @@ class Todo: async def select_todos( db: AsyncConnectionPool, member_id: int, complete: bool | None = None ) -> 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 conn.cursor(row_factory=class_row(Todo)) as curr: @@ -43,3 +48,27 @@ async def select_todos( todos = await curr.fetchall() 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 diff --git a/backend/tests/test_model_todo.py b/backend/tests/test_model_todo.py index 9460c26..7e40de8 100644 --- a/backend/tests/test_model_todo.py +++ b/backend/tests/test_model_todo.py @@ -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): @@ -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) 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