From 6b6477db01a4855e3453b9fa2e8a8e1a69ab0378 Mon Sep 17 00:00:00 2001 From: Brian Obot Date: Wed, 29 Apr 2026 15:55:47 +0100 Subject: [PATCH] Add unittest --- .coverage | Bin 53248 -> 53248 bytes app/middlewares.py | 1 + app/routers/auth.py | 20 +++++---- app/routers/tests/test_auth.py | 24 +++++----- app/services/auth.py | 42 +++++++++++------- app/services/tests/test_auth.py | 33 ++------------ .../auth/password_reset_confirmation.html | 0 app/tests/test_middlewares.py | 20 +++++++++ 8 files changed, 71 insertions(+), 69 deletions(-) create mode 100644 app/templates/auth/password_reset_confirmation.html create mode 100644 app/tests/test_middlewares.py diff --git a/.coverage b/.coverage index 7d81490873b42d8091e7307de256b5aad3a70b3b..7f0c5cc5a117e45bb65f6b312bca54e4a4a091c5 100644 GIT binary patch delta 462 zcmZozz}&Eac>{|BvodS(WOfG!_5wC3*5j1#TfsK?)$55AJM}*SoQD=Vqp)SO;8P6!B#KOoap!lhVLlMXjQV_c@GkJ4Ygy_CcmHIzt#s55$ zKJWZ9^XdL^dHU~JCr*r*T+zKy;@!LS$otpdznORU^Zf8>3=H?T|J=B{wk~6`Vb6bg zSz(|-HsbGo{(1Yhx7IHI*O#B`vh}M!SLYSafBwF9azL*l$ET-Ht9S01vv=ae{K+qR zr*kWp{bXa~0qWyZn%vc=$yCoad2630Yux8}JKoKY`xF`3l=*oX_<1LDO|WMb=3mRd k4Cssfd_1fyoSclxY(R~`;E|j^i9?we%u(9BdA_d!0O9qb@c;k- delta 431 zcmZozz}&Eac>{|Bvl8p^$?Ogeta)tGtj9M_tY&2jQkvvtIeCH~`{b>z!mNrc42|lO z8@>xs!q0qV^8{QEaftYMv85*o~^6vP5FS1`_YGN+p$t70e%(9Dey zW(YIG95k7gI6Wt`H>k-Q@M`c%@(S{@^Ze)j%l(z>GuKP5hg=uA4s&hhTE*$PSy13F z2Xj-L(q`w*ct#FI7Di41g-cXPlmXI?&8~pqX`()A}U2mCAmyvGD*k z^C?d5>eFOmXP&&ZPm^`tv;8$vn;-WnGO{W0i!ktuOy-(k&%Bm@8PE?W_*hw4I5`<7 VOU|Ffp~TAu)Xb>3dGmZ<0|3SooQ(hg diff --git a/app/middlewares.py b/app/middlewares.py index 0c43973..a3b76d6 100644 --- a/app/middlewares.py +++ b/app/middlewares.py @@ -31,6 +31,7 @@ async def dispatch( self, request: Request, call_next: RequestResponseEndpoint ) -> Response: client_ip = request.client.host # type: ignore + if "/docs" in request.url.path: if client_ip not in self.allowed_ips: return JSONResponse( diff --git a/app/routers/auth.py b/app/routers/auth.py index e5ef39e..98cdf07 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -27,9 +27,18 @@ async def signup( db: DBDep, bg_task: BackgroundTasks, # needed to send verification/welcome email - request_data: auth_schemas.UserSignUpData, + payload: auth_schemas.UserSignUpData, ): - return await auth_services.signup_user(request_data, db, bg_task) + return await auth_services.signup_user(payload, db, bg_task) + + +@router.post("/activation") +async def activate_user( + db: DBDep, + bg_task: BackgroundTasks, # needed to send verification/welcome email + payload: auth_schemas.UserVerificationModel, +): + return await auth_services.activate_user(payload, db, bg_task) @router.post("/initiate_password_reset") @@ -41,13 +50,6 @@ async def initiate_password_reset( return await auth_services.initiate_password_reset(email, db, background_task) -@router.post("/verify_password_reset") -async def verify_reset_password_otp( - db: DBDep, code: Annotated[str, Body(embed=True)], email: EmailBody -): - return await auth_services.verify_reset_password_otp(code, email, db) - - @router.post("/reset_password") async def reset_password( db: DBDep, diff --git a/app/routers/tests/test_auth.py b/app/routers/tests/test_auth.py index 00cc46e..e1b0dca 100644 --- a/app/routers/tests/test_auth.py +++ b/app/routers/tests/test_auth.py @@ -53,33 +53,31 @@ async def test_signup_fails( assert error_msg == error_message -async def test_initiate_password_reset(client: AsyncClient, signup_data: dict): - data = {"email": signup_data["email"]} +async def test_activate_user(client: AsyncClient, user: UserDB): + redis_manager.cache_json_item(f"verification-code-{user.email}", {"code": "000000"}) response: Response = await client.post( - "/v1/auth/initiate_password_reset", json=data + "/v1/auth/activation", json={"code": "000000", "email": user.email} ) assert response.status_code == 200 - assert response.json().get("detail") == "Password reset code sent" + assert response.json().get("detail") == "Email Activation Successful" -async def test_verify_reset_password_otp(client: AsyncClient, user: UserDB): - # Seed the value to be validated against - redis_manager.cache_json_item(f"reset-code-{user.email}", {"code": "0000"}) - verification_data = {"email": user.email, "code": "0000"} +async def test_initiate_password_reset(client: AsyncClient, signup_data: dict): + data = {"email": signup_data["email"]} response: Response = await client.post( - "/v1/auth/verify_password_reset", json=verification_data + "/v1/auth/initiate_password_reset", json=data ) assert response.status_code == 200 - assert response.json().get("detail") == "Verification is Successful" + assert response.json().get("detail") == "Password Reset Code Sent" async def test_reset_password(client: AsyncClient, user: UserDB): # Seed the value to be validated against - redis_manager.cache_json_item(f"reset-code-{user.email}", {"code": "0000"}) - data = {"new_password": "password", "email": user.email, "code": "0000"} + redis_manager.cache_json_item(f"reset-code-{user.email}", {"code": "000000"}) + data = {"new_password": "password", "email": user.email, "code": "000000"} response: Response = await client.post("/v1/auth/reset_password", json=data) assert response.status_code == 200 - assert response.json().get("detail") == "Password reset successfully" + assert response.json().get("detail") == "Password Reset Successfully" async def test_reset_password_fails(client: AsyncClient, user: UserDB): diff --git a/app/services/auth.py b/app/services/auth.py index e05344a..531662a 100644 --- a/app/services/auth.py +++ b/app/services/auth.py @@ -132,7 +132,7 @@ async def initiate_password_reset( ): user = await get_user(email, session) if not user: - return {"detail": "Password reset code sent"} + return {"detail": "Password Reset Code Sent"} code = str(randint(1000, 9999)) redis_manager.cache_json_item( @@ -147,19 +147,7 @@ async def initiate_password_reset( template="auth/initiate_password_reset.html", ) - return {"detail": "Password reset code sent"} - - -async def verify_reset_password_otp(code: str, email: str, session: AsyncSession): - data = redis_manager.get_json_item(f"reset-code-{email}") - if not data or data.get("code") != code: - raise HTTPException(status_code=400, detail="Invalid Reset Code") - - user = await get_user(email, session) - if not user: - raise HTTPException(status_code=400, detail="Invalid Reset Code") - - return {"detail": "Verification is Successful"} + return {"detail": "Password Reset Code Sent"} async def reset_password( @@ -184,7 +172,7 @@ async def reset_password( await session.execute(stmt) await session.commit() - return {"detail": "Password reset successfully"} + return {"detail": "Password Reset Successfully"} async def update_user( @@ -262,14 +250,34 @@ async def signup_user( ): user = await create_user(data, session) - background_task.add_task( + return user + + +async def activate_user( + verification_data: auth_schema.UserVerificationModel, + session: AsyncSession, + bg_task: BackgroundTasks, +): + data = redis_manager.get_json_item(f"verification-code-{verification_data.email}") + + if not data or data.get("code") != verification_data.code: + raise HTTPException(status_code=400, detail="Invalid Reset Code") + + user = await get_user(verification_data.email, session) + if not user: + raise HTTPException(status_code=400, detail="Invalid Reset Code") + + # TODO: Perform the actual user verification here + + bg_task.add_task( send_mail, subject="Welcome to {{ project_name }}", receipients=[user.email], payload={"username": user.email.split("@")[0].title()}, template="auth/welcome.html", ) - return user + + return {"detail": "Email Activation Successful"} async def signin_user(data: auth_schema.UserSignInData, session: AsyncSession): diff --git a/app/services/tests/test_auth.py b/app/services/tests/test_auth.py index 78931fd..206a4f2 100644 --- a/app/services/tests/test_auth.py +++ b/app/services/tests/test_auth.py @@ -108,41 +108,14 @@ async def test_initiate_password_reset_for_user(user: UserDB, session: AsyncSess result = await auth_services.initiate_password_reset( user.email, session, BackgroundTasks(tasks=[]) ) - assert result == {"detail": "Password reset code sent"} + assert result == {"detail": "Password Reset Code Sent"} async def test_initiate_password_reset_for_none_user(session: AsyncSession): result = await auth_services.initiate_password_reset( faker.email(), session, BackgroundTasks(tasks=[]) ) - assert result == {"detail": "Password reset code sent"} - - -async def test_verify_reset_password_otp_for_user(user: UserDB, session: AsyncSession): - code = "000000" - redis_manager.cache_json_item(f"reset-code-{user.email}", {"code": code}) - result = await auth_services.verify_reset_password_otp(code, user.email, session) - assert result == {"detail": "Verification is Successful"} - - -@pytest.mark.parametrize( - "code", - [ - "000001", - "000000", - ], -) -async def test_verify_reset_password_otp_fails(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.verify_reset_password_otp( - code, none_existence_email, session - ) - - assert err.value.detail == "Invalid Reset Code" + assert result == {"detail": "Password Reset Code Sent"} async def test_reset_password_for_user(user: UserDB, session: AsyncSession): @@ -153,7 +126,7 @@ async def test_reset_password_for_user(user: UserDB, session: AsyncSession): ), session, ) - assert result == {"detail": "Password reset successfully"} + assert result == {"detail": "Password Reset Successfully"} @pytest.mark.parametrize( diff --git a/app/templates/auth/password_reset_confirmation.html b/app/templates/auth/password_reset_confirmation.html new file mode 100644 index 0000000..e69de29 diff --git a/app/tests/test_middlewares.py b/app/tests/test_middlewares.py new file mode 100644 index 0000000..d18b066 --- /dev/null +++ b/app/tests/test_middlewares.py @@ -0,0 +1,20 @@ +import pytest +from httpx import ASGITransport, AsyncClient, Response + +from app.main import app + + +# Lazy override of the global client fixture to patch the ip address +@pytest.fixture +async def client(): + transport = ASGITransport( + app=app, + client=("10.0.0.5", 12345), + ) + async with AsyncClient(transport=transport, base_url="http://testserver") as ac: + yield ac + + +async def test_docs_route_is_whitelisted(client: AsyncClient): + response: Response = await client.get("/docs") + assert response.status_code == 500