From bb1f33f4e75a36180ab7f98955a791baa4557187 Mon Sep 17 00:00:00 2001 From: Brian Obot Date: Fri, 27 Feb 2026 22:08:42 +0100 Subject: [PATCH 1/2] Add Github Coverage Badge to the Readme file --- .coverage | Bin 53248 -> 53248 bytes .github/workflows/create_pull_request.yml | 20 ++++- README.md | 3 + app/services/auth.py | 12 ++- app/services/tests/test_auth.py | 87 ++++++++++++++++++++-- 5 files changed, 109 insertions(+), 13 deletions(-) diff --git a/.coverage b/.coverage index 89efd3d0706319239ce9ce2da2bb5898731be75e..fd7fb8ef36ece43d132aba19df62cece112fe986 100644 GIT binary patch delta 85 zcmV-b0IL6hpaX!Q1F-*!9rk`X=bT+`4?o|1a=Bc#d_G^k^7%`C&0qOEJ`a(RUz6;O rev|o&AR6q>Pu^vBKg;^Qe&;>!`OXV~0{{TnngD_u=jE@Fk!q9djZ%@ID3kSzBpPha jPu^rVKg;?6e&;>UdB6*R0{{TFCIFxTk&(=^|BL}cGx{c_ diff --git a/.github/workflows/create_pull_request.yml b/.github/workflows/create_pull_request.yml index 105cf0c..d583500 100644 --- a/.github/workflows/create_pull_request.yml +++ b/.github/workflows/create_pull_request.yml @@ -42,6 +42,7 @@ jobs: python -m pip install --upgrade pip if [ -f requirements.txt ]; then pip install -r requirements.txt; fi pip install pytest # Ensures pytest is available even if not in requirements.txt + pip install pytest pytest-cov - name: Create .env file run: | @@ -55,11 +56,26 @@ jobs: echo "MAIL_PORT=465" >> .env echo "MAIL_SERVER=smtp.gmail.com" >> .env - - name: Run Tests + id: coverage_step run: | mkdir logs - pytest -s + coverage run -m pytest + PERCENT=$(coverage report | grep TOTAL | awk '{print $NF}' | sed 's/%//') + echo "PERCENTAGE=$PERCENT" >> $GITHUB_OUTPUT + + - name: Create Coverage Badge + uses: schneegans/dynamic-badges-action@v1.7.0 + with: + auth: ${{ secrets.MY_PERSONAL_TOKEN }} + gistID: b56b3d61a5e739fd26252cda094bace2 + filename: fastapi_project_structure_coverage.json + label: Coverage + message: ${{ steps.coverage_step.outputs.PERCENTAGE }}% + valColorRange: ${{ steps.coverage_step.outputs.PERCENTAGE }} + maxColorRange: 100 + minColorRange: 0 + create_pull_request: runs-on: ubuntu-latest diff --git a/README.md b/README.md index 247cd3f..dfbd175 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # FastAPI Project Structure Template ⚙️ You can Generate Project Interactively Based on this template with the [FastAPI Gen8 CLI Tool](https://pypi.org/project/fastapi-gen8/) + +![Coverage Badge](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/brianobot/b56b3d61a5e739fd26252cda094bace2/raw/fastapi_project_structure_coverage.json) + [📖 Read Article here](https://medium.com/@brianobot9/the-ultimate-fastapi-project-blueprint-build-scalable-secure-and-maintainable-systems-with-ease-acbc4e058012) This repository provides a clean and scalable template for building FastAPI applications. It is designed to help you start new projects quickly with best practices in mind. diff --git a/app/services/auth.py b/app/services/auth.py index 4fad6a4..3185e1f 100644 --- a/app/services/auth.py +++ b/app/services/auth.py @@ -171,7 +171,7 @@ async def reset_password( user = await get_user(reset_data.email, session) if not user: - raise HTTPException(status_code=404, detail="User not found") + raise HTTPException(status_code=404, detail="Invalid Reset Code") hashed_password = get_password_hash(reset_data.new_password) stmt = ( @@ -201,7 +201,7 @@ async def update_user( if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect email or password", + detail="Incorrect Old Password", headers={"WWW-Authenticate": "Bearer"}, ) data.update({"password": get_password_hash(new_password)}) @@ -218,7 +218,9 @@ async def update_user( return result.scalar_one() -def create_access_token(data: dict, expires_delta: timedelta | None = None): +def create_access_token( + data: dict[str, str | datetime], expires_delta: timedelta | None = None +): to_encode = data.copy() if expires_delta: expire = datetime.now(UTC) + expires_delta @@ -229,7 +231,9 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None): return encoded_jwt -def create_refresh_token(data: dict, expires_delta: timedelta | None = None): +def create_refresh_token( + data: dict[str, str | datetime], expires_delta: timedelta | None = None +): to_encode = data.copy() if expires_delta: expire = datetime.now(UTC) + expires_delta diff --git a/app/services/tests/test_auth.py b/app/services/tests/test_auth.py index e0b4118..e1e643b 100644 --- a/app/services/tests/test_auth.py +++ b/app/services/tests/test_auth.py @@ -1,3 +1,4 @@ +from datetime import timedelta from typing import Any import pytest @@ -109,11 +110,9 @@ async def test_initiate_password_reset_for_user(user: UserDB, session: AsyncSess assert result == {"detail": "Password reset code sent"} -async def test_initiate_password_reset_for_none_user( - user: UserDB, session: AsyncSession -): +async def test_initiate_password_reset_for_none_user(session: AsyncSession): result = await auth_services.initiate_password_reset( - user.email, session, BackgroundTasks(tasks=[]) + faker.email(), session, BackgroundTasks(tasks=[]) ) assert result == {"detail": "Password reset code sent"} @@ -133,14 +132,19 @@ async def test_verify_reset_password_otp_for_user(user: UserDB, session: AsyncSe ], ) async def test_verify_reset_password_otp_fails(session: AsyncSession, code: str): - redis_manager.cache_json_item(f"reset-code-{faker.email()}", {"code": "000000"}) + none_existence_email = faker.email() + redis_manager.cache_json_item( + f"reset-code-{none_existence_email}", {"code": "000000"} + ) with pytest.raises(HTTPException) as err: - await auth_services.verify_reset_password_otp(code, faker.email(), session) + await auth_services.verify_reset_password_otp( + code, none_existence_email, session + ) assert err.value.detail == "Invalid Reset Code" -async def test_reset_password(user: UserDB, session: AsyncSession): +async def test_reset_password_for_user(user: UserDB, session: AsyncSession): redis_manager.cache_json_item(f"reset-code-{user.email}", {"code": "000000"}) result = await auth_services.reset_password( auth_schemas.PasswordResetData( @@ -149,3 +153,72 @@ async def test_reset_password(user: UserDB, session: AsyncSession): session, ) assert result == {"detail": "Password reset successfully"} + + +@pytest.mark.parametrize( + "code", + [ + "000000", + "000001", + ], +) +async def test_reset_password_fails(user: UserDB, session: AsyncSession, code: str): + none_existence_email = faker.email() + redis_manager.cache_json_item( + f"reset-code-{none_existence_email}", {"code": "000000"} + ) + with pytest.raises(HTTPException) as err: + await auth_services.reset_password( + auth_schemas.PasswordResetData( + code=code, email=none_existence_email, new_password="newpassword" + ), + session, + ) + + assert err.value.detail == "Invalid Reset Code" + + +async def test_update_user(user: UserDB, session: AsyncSession): + updated_user = await auth_services.update_user( + user.email, + auth_schemas.UpdateUserModel( + old_password="password", new_password="new_password" + ), + session, + ) + assert isinstance(updated_user, UserDB) + assert auth_services.verify_password("new_password", updated_user.password) + + +async def test_update_user_fails(user: UserDB, session: AsyncSession): + with pytest.raises(HTTPException) as err: + await auth_services.update_user( + user.email, + auth_schemas.UpdateUserModel( + old_password="incorrectpassword", new_password="new_password" + ), + session, + ) + assert err.value.detail == "Incorrect Old Password" + + +@pytest.mark.parametrize( + "expires_delta", + [ + None, + timedelta(days=10), + ], +) +async def test_create_access_token(expires_delta: timedelta | None): + auth_services.create_access_token({"sub": faker.email()}, expires_delta) + + +@pytest.mark.parametrize( + "expires_delta", + [ + None, + timedelta(days=10), + ], +) +async def test_refresh_access_token(expires_delta: timedelta | None): + auth_services.create_refresh_token({"sub": faker.email()}, expires_delta) From 9711eed1d8b40c27e840c58e0c87ccc76d598d9f Mon Sep 17 00:00:00 2001 From: Brian Obot Date: Fri, 27 Feb 2026 22:15:59 +0100 Subject: [PATCH 2/2] Update Coverage Workflow --- .github/workflows/create_pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/create_pull_request.yml b/.github/workflows/create_pull_request.yml index d583500..7d4242b 100644 --- a/.github/workflows/create_pull_request.yml +++ b/.github/workflows/create_pull_request.yml @@ -67,7 +67,7 @@ jobs: - name: Create Coverage Badge uses: schneegans/dynamic-badges-action@v1.7.0 with: - auth: ${{ secrets.MY_PERSONAL_TOKEN }} + auth: ${{ secrets.GIST_SECRET }} gistID: b56b3d61a5e739fd26252cda094bace2 filename: fastapi_project_structure_coverage.json label: Coverage