From 2d9554cce2fa99301726bca35ac4408a148e5fa5 Mon Sep 17 00:00:00 2001 From: Brian Obot Date: Fri, 27 Feb 2026 13:03:29 +0100 Subject: [PATCH] Improve test coverage for codebase --- .coverage | Bin 53248 -> 53248 bytes app/database.py | 2 +- app/routers/tests/test_auth.py | 76 +++++++++++++++++++------------- app/schemas/tests/__init__.py | 0 app/schemas/tests/test_auth.py | 22 +++++++++ app/services/auth.py | 4 +- app/services/tests/test_auth.py | 45 ++++++++++++++++++- 7 files changed, 114 insertions(+), 35 deletions(-) create mode 100644 app/schemas/tests/__init__.py create mode 100644 app/schemas/tests/test_auth.py diff --git a/.coverage b/.coverage index 1114189f42a27132106890d8d188295ee272a886..cc326643f6df9a15bd2a7ed7dcb963a7fe041110 100644 GIT binary patch delta 955 zcmZ{fZDzzeIpk#ZLK2T?NGENP81_+3x3s?_vber^b8zFfS}zr6TVhpI zMH*?0)?jsdxhKYx@n|7drd>IOA=OJo?D?WW!4B0p(e z8Ke2l)fI%MW^<{11l3oNs;Gjzq+wMJ)~0uI@d}Vi9m|nI>vzgmvw7(A71P=3hiB#) ze2lO?Gxhn>Y)O-fBtf!^Hs?DbBfJ!z3ipMZ!k{p~$4b*NvjQHI<)S-sk<>_acHxt|Krn!k6+_ythvStPK z?(|p|Y5eur;gR&E?&Ev5ye6gl$A5Nb4I<)7vbTATrg>&YR&gV#m{M~Fzn`C8)RGP} zMOj%udLdihB(U!~3HSZz zG`gl3M$GO9Qhj}0C~82F4T|7BJcF087>40rxB~sq39V2Mad-@4a2F=v2$*mSeuv$V zgcfLo8mNLY2*I}y00jo%uaql)RIbMko<{~l???WC1>JAKr(5v)ENET}ShJw278J#T bSxXFh8FlR_s6G)}{f13LlmI)5H delta 839 zcmYk2YeWQn^aIilnADgAH)#s>{X*rzwS@ z+eoaT^HAB zC0o3^$ge_{jCdKjE^xfw)!0&Mql)u?MWo8fW2dNAW+nH0o{bGMRG7$E=(Kr#ItiJn zw}*6;V6w+&Kqfk!MRI&fv}o@bTcEN{anK^}RA#{~#fuYSn^2}y%2SHWzF?c=Gr|wy zI$p)YxKd7+zDi-qixEGp+~jL{fg9)QITJjkv9`11RYe)2`gNV#h`uI=9QNvIw1&zG z#!;f9p&lH_+;}3t9?DL_hq5-B!oNH=5~wO?ti?JSVQou~EYvA$Jwhx{plYqU=F>+j zsz*{2R2(2&N2)bbcF9iJC?!j>D2hl}7G{M{!W&@{2k0x!*l?~!WiURUJO9Mz_IRdN zvX@$-wHNoJDyI(NM91$o#X%1adwSor>-LF>6}@DqwZ(RD`Bt$1L!n}esn#(=^E+4T zc<3i-l@n}IUR7|LyLZ9N)6eY^AW z@)jar+Uujw7MF5KdCyiR-{lUv!{G*c;ee=ymRVMc?K-2qy&+VfLP{K@z#sSyb5IPE z@Ce2s0?kklK{x`RVH)1UB3y+qyoO8A1#M6bekgz($OJc}!Ce@lfA~=;W^@=Mm8IUu r;nZMvXt3EeSZx|CRt=0rgW0UXWYQoTHAti`lFO^IRyd>4NNL7jRQByC diff --git a/app/database.py b/app/database.py index 45d17d9..80be0eb 100644 --- a/app/database.py +++ b/app/database.py @@ -4,7 +4,7 @@ from app.settings import Settings -settings = Settings() # noqa +settings = Settings() # type: ignore # Add Support for Both ASYNC and SYNC Database URLs # With Async being the center of focus diff --git a/app/routers/tests/test_auth.py b/app/routers/tests/test_auth.py index 92fba2d..b8ed650 100644 --- a/app/routers/tests/test_auth.py +++ b/app/routers/tests/test_auth.py @@ -1,7 +1,6 @@ import pytest -from httpx import ASGITransport, AsyncClient, Response +from httpx import AsyncClient, Response -from app.main import app from app.models import User as UserDB from app.redis_manager import redis_manager from app.schemas.auth import UserModel @@ -53,21 +52,6 @@ async def test_signup_fails( assert error_msg == error_message -@pytest.fixture -async def sign_up_user(signup_data: dict): - # to be used as a side effect to test things that need a user that has signed up - async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test" - ) as client: - await client.post("/v1/auth/signup", json=signup_data) - - -async def mutate_cache_item(key: str, value: dict): - # to be used as a side effect to mutate cache item - - redis_manager.cache_json_item(key, value) - - async def test_initiate_password_reset(client: AsyncClient, signup_data: dict): data = {"email": signup_data["email"]} response: Response = await client.post( @@ -106,19 +90,51 @@ async def test_reset_password_fails(client: AsyncClient, user: UserDB): assert response.json().get("detail") == "Invalid Reset Code" -async def test_signin(client: AsyncClient, user: UserDB): - data = { - "username": user.email, - "password": "password", - } - response: Response = await client.post("/v1/auth/token", data=data) - assert response.status_code == 200 - response_data = response.json() - assert "token_type" in response_data - assert "access_token" in response_data - assert "refresh_token" in response_data - assert "access_expires_at" in response_data - assert "refresh_expires_at" in response_data +class TestSignIn: + async def test_signin_success(self, client: AsyncClient, user: UserDB): + data = { + "username": user.email, + "password": "password", + } + response = await client.post("/v1/auth/token", data=data) + assert response.status_code == 200 + response_data = response.json() + assert "access_token" in response_data + assert "token_type" in response_data + assert "refresh_token" in response_data + assert "access_expires_at" in response_data + assert "refresh_expires_at" in response_data + assert response_data["token_type"] == "Bearer" + + async def test_signin_validation_error(self, client: AsyncClient): + # Pass a string that is NOT a valid email + data = { + "username": "not-an-email", + "password": "password", + } + response = await client.post("/v1/auth/token", data=data) + + assert response.status_code == 422 + assert "detail" in response.json() + + async def test_signin_invalid_password(self, client: AsyncClient, user: UserDB): + data = { + "username": user.email, + "password": "wrong-password", + } + response = await client.post("/v1/auth/token", data=data) + + # auth_services likely raises 401 for wrong passwords + assert response.status_code == 401 + assert response.json()["detail"] == "Incorrect email or password" + + async def test_signin_user_not_found(self, client: AsyncClient): + data = { + "username": "ghost@example.com", + "password": "password", + } + response = await client.post("/v1/auth/token", data=data) + assert response.status_code == 401 async def test_get_refresh_token(client: AsyncClient, user: UserDB): diff --git a/app/schemas/tests/__init__.py b/app/schemas/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/schemas/tests/test_auth.py b/app/schemas/tests/test_auth.py new file mode 100644 index 0000000..823fab1 --- /dev/null +++ b/app/schemas/tests/test_auth.py @@ -0,0 +1,22 @@ +import pytest +from pydantic import ValidationError + +from app.schemas import auth as auth_schemas + + +def test_update_user_model_succeeds(): + auth_schemas.UpdateUserModel( + old_password="password", + new_password="newpassword", + ) + + +def test_update_user_model_fails(): + with pytest.raises(ValidationError) as err: + auth_schemas.UpdateUserModel( + new_password="newpassword", + ) + + assert "old_password is required if new_password is provided." in str( + err.value.errors() + ) diff --git a/app/services/auth.py b/app/services/auth.py index 5ae6087..4fad6a4 100644 --- a/app/services/auth.py +++ b/app/services/auth.py @@ -81,7 +81,7 @@ async def verify_user( logger.info(f"Corrupt Log Data: {data}") raise HTTPException(status_code=404, detail="Invalid Verification Code") - if not data or data.get("code") != verification_data.code: + if data.get("code") != verification_data.code: logger.info("Invalid Verification Code") raise HTTPException(status_code=400, detail="Invalid Verification Code") @@ -143,7 +143,7 @@ async def initiate_password_reset( send_mail, subject="Password Reset", receipients=[user.email], - payload={"username": user.username.title(), "code": code}, + payload={"username": user.email.split("@")[0], "code": code}, template="auth/initiate_password_reset.html", ) diff --git a/app/services/tests/test_auth.py b/app/services/tests/test_auth.py index d3dda9e..9f6fc1d 100644 --- a/app/services/tests/test_auth.py +++ b/app/services/tests/test_auth.py @@ -1,5 +1,8 @@ +from typing import Any + +import pytest from faker import Faker -from fastapi import BackgroundTasks +from fastapi import BackgroundTasks, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from app.models import User as UserDB @@ -32,7 +35,17 @@ async def test_create_user(session: AsyncSession, signup_data: dict[str, str]): assert isinstance(result, UserDB) -async def test_verify_user(user: UserDB, session: AsyncSession): +async def test_create_user_fails( + session: AsyncSession, signup_data: dict[str, Any], user: UserDB # noqa +): # noqa + signup_data["email"] = user.email + with pytest.raises(HTTPException) as err: + await auth_services.create_user(UserSignUpData(**signup_data), session) + + assert "Email already registered" in str(err.value) + + +async def test_verify_user_succeeds(user: UserDB, session: AsyncSession): verification_data = auth_schemas.UserVerificationModel( email=user.email, code="000000" ) @@ -45,3 +58,31 @@ async def test_verify_user(user: UserDB, session: AsyncSession): session, ) # test that the flag for verified user is activated + + +@pytest.mark.parametrize( + "verification_data,cached_code", + [ + ({"email": faker.email(), "code": "000000"}, {"code": "000000"}), + ({"email": faker.email(), "code": "000000"}, {"code": "000001"}), + ({"email": faker.email(), "code": "000000"}, None), + ], +) +async def test_verify_user_fails( + user: UserDB, + session: AsyncSession, + verification_data: dict[str, str], + cached_code: dict[str, str] | None, +): + data = auth_schemas.UserVerificationModel(**verification_data) + redis_manager.cache_json_item( + f"verification-code-{data.email}", cached_code # type: ignore + ) + with pytest.raises(HTTPException) as err: + await auth_services.verify_user( + data, + BackgroundTasks(tasks=[]), + session, + ) + + assert err.value.detail == "Invalid Verification Code"