From a78f5ae5c3a4747417f3631ce7d9f62a9fe740de Mon Sep 17 00:00:00 2001 From: Amanda Zhu Date: Fri, 29 Aug 2025 15:48:18 +1000 Subject: [PATCH 1/6] add route for is_email_verified --- routers/user.py | 8 ++++++++ tests/test_user.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/routers/user.py b/routers/user.py index 3a41e041..aeb55ca0 100644 --- a/routers/user.py +++ b/routers/user.py @@ -90,6 +90,14 @@ async def check_is_admin( return {"is_admin": user.is_admin(settings)} +@router.get("/is-email-verified") +async def check_is_email_verified( + user: Annotated[SessionUser, Depends(get_current_user)], + settings: Annotated[Settings, Depends(get_settings)], +): + user_data = await get_user_data(user, settings) + return {"is_email_verified": user_data.email_verified} + @router.get("/services/approved", response_model=Dict[str, List[Service]]) async def get_approved_services( user: Annotated[SessionUser, Depends(get_current_user)], diff --git a/tests/test_user.py b/tests/test_user.py index eba70332..f0a28305 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -71,6 +71,7 @@ def mock_user_data(): "/me/resources/approved", "/me/resources/pending", "/me/all/pending", + "/me/is-email-verified", ], ) def test_endpoints_require_auth(endpoint, test_client): @@ -529,3 +530,40 @@ def test_check_is_admin_without_authentication(test_client): """Test that admin check requires authentication""" response = test_client.get("/me/is-admin") assert response.status_code == 401 + +def test_check_is_email_verified_true(mock_auth_token, auth_headers, mocker, test_client): + """Returns True when Auth0 user has verified email""" + mock_user = Auth0UserDataFactory.build(email_verified=True) + mocker.patch("routers.user.get_user_data", return_value=mock_user) + mocker.patch("routers.user.get_management_token", return_value="mock_management_token") + + response = test_client.get("/me/is-email-verified", headers=auth_headers) + + assert response.status_code == 200 + assert response.json() == {"is_email_verified": True} + + +def test_check_is_email_verified_false(mock_auth_token, auth_headers, mocker, test_client): + """Returns False when Auth0 user email is not verified""" + mock_user = Auth0UserDataFactory.build(email_verified=False) + mocker.patch("routers.user.get_user_data", return_value=mock_user) + mocker.patch("routers.user.get_management_token", return_value="mock_management_token") + + response = test_client.get("/me/is-email-verified", headers=auth_headers) + + assert response.status_code == 200 + assert response.json() == {"is_email_verified": False} + + +def test_check_is_email_verified_failed_fetch(mock_auth_token, auth_headers, mocker, test_client): + """Propagates HTTPException when user data fetch fails""" + mocker.patch( + "routers.user.get_user_data", + side_effect=HTTPException(status_code=403, detail="Failed to fetch user data"), + ) + mocker.patch("routers.user.get_management_token", return_value="mock_management_token") + + response = test_client.get("/me/is-email-verified", headers=auth_headers) + + assert response.status_code == 403 + assert response.json() == {"detail": "Failed to fetch user data"} From 1732122df321a6e9190419e35a2febd8238b945b Mon Sep 17 00:00:00 2001 From: Amanda Zhu Date: Mon, 1 Sep 2025 15:54:57 +1000 Subject: [PATCH 2/6] add unverified users router to admin --- routers/admin.py | 10 ++++++++++ routers/user.py | 9 --------- tests/test_admin.py | 20 ++++++++++++++------ tests/test_user.py | 37 ------------------------------------- 4 files changed, 24 insertions(+), 52 deletions(-) diff --git a/routers/admin.py b/routers/admin.py index f5357887..dfd09b26 100644 --- a/routers/admin.py +++ b/routers/admin.py @@ -75,6 +75,16 @@ def get_revoked_users(client: Annotated[Auth0Client, Depends(get_auth0_client)], resp = client.get_revoked_users(page=pagination.page, per_page=pagination.per_page) return resp +@router.get("/users/unverified", response_model=list[Auth0UserData]) +def get_unverified_users( + client: Annotated[Auth0Client, Depends(get_auth0_client)], + pagination: Annotated[PaginationParams, Depends(get_pagination_params)], +): + """ + Return users whose email is not verified. + """ + resp = client.get_users(page=pagination.page, per_page=pagination.per_page) + return [u for u in resp if not getattr(u, "email_verified", False)] @router.get("/users/{user_id}", response_model=Auth0UserData) diff --git a/routers/user.py b/routers/user.py index aeb55ca0..fd7ca82d 100644 --- a/routers/user.py +++ b/routers/user.py @@ -89,15 +89,6 @@ async def check_is_admin( """Check if the current user has admin privileges.""" return {"is_admin": user.is_admin(settings)} - -@router.get("/is-email-verified") -async def check_is_email_verified( - user: Annotated[SessionUser, Depends(get_current_user)], - settings: Annotated[Settings, Depends(get_settings)], -): - user_data = await get_user_data(user, settings) - return {"is_email_verified": user_data.email_verified} - @router.get("/services/approved", response_model=Dict[str, List[Service]]) async def get_approved_services( user: Annotated[SessionUser, Depends(get_current_user)], diff --git a/tests/test_admin.py b/tests/test_admin.py index 488a3387..9a269c51 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -290,11 +290,19 @@ def test_approve_resource(test_client, as_admin_user, mock_auth0_client, mocker) assert resource_data["status"] == "approved" assert resource_data["id"] == resource.id +def test_get_unverified_users(test_client, as_admin_user, mock_auth0_client): + # Mix of verified and unverified users + u1 = Auth0UserDataFactory.build(email_verified=False) + u2 = Auth0UserDataFactory.build(email_verified=True) + u3 = Auth0UserDataFactory.build(email_verified=False) -def test_resend_verification_email(test_client, as_admin_user, mock_auth0_client): - user = Auth0UserDataFactory.build() - response_data = EmailVerificationResponseFactory.build() - mock_auth0_client.resend_verification_email.return_value = response_data - resp = test_client.post(f"/admin/users/{user.user_id}/verification-email/resend") + mock_auth0_client.get_users.return_value = [u1, u2, u3] + + resp = test_client.get("/admin/users/unverified") assert resp.status_code == 200 - assert resp.json() == {"message": "Verification email resent."} + + data = resp.json() + assert len(data) == 2 + returned_ids = {u["user_id"] for u in data} + assert returned_ids == {u1.user_id, u3.user_id} + assert all(u["email_verified"] is False for u in data) diff --git a/tests/test_user.py b/tests/test_user.py index f0a28305..1a57cccb 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -530,40 +530,3 @@ def test_check_is_admin_without_authentication(test_client): """Test that admin check requires authentication""" response = test_client.get("/me/is-admin") assert response.status_code == 401 - -def test_check_is_email_verified_true(mock_auth_token, auth_headers, mocker, test_client): - """Returns True when Auth0 user has verified email""" - mock_user = Auth0UserDataFactory.build(email_verified=True) - mocker.patch("routers.user.get_user_data", return_value=mock_user) - mocker.patch("routers.user.get_management_token", return_value="mock_management_token") - - response = test_client.get("/me/is-email-verified", headers=auth_headers) - - assert response.status_code == 200 - assert response.json() == {"is_email_verified": True} - - -def test_check_is_email_verified_false(mock_auth_token, auth_headers, mocker, test_client): - """Returns False when Auth0 user email is not verified""" - mock_user = Auth0UserDataFactory.build(email_verified=False) - mocker.patch("routers.user.get_user_data", return_value=mock_user) - mocker.patch("routers.user.get_management_token", return_value="mock_management_token") - - response = test_client.get("/me/is-email-verified", headers=auth_headers) - - assert response.status_code == 200 - assert response.json() == {"is_email_verified": False} - - -def test_check_is_email_verified_failed_fetch(mock_auth_token, auth_headers, mocker, test_client): - """Propagates HTTPException when user data fetch fails""" - mocker.patch( - "routers.user.get_user_data", - side_effect=HTTPException(status_code=403, detail="Failed to fetch user data"), - ) - mocker.patch("routers.user.get_management_token", return_value="mock_management_token") - - response = test_client.get("/me/is-email-verified", headers=auth_headers) - - assert response.status_code == 403 - assert response.json() == {"detail": "Failed to fetch user data"} From a9623e8a94d2fb7b8940553b90183cf298c4ae57 Mon Sep 17 00:00:00 2001 From: Amanda Zhu Date: Mon, 1 Sep 2025 16:06:12 +1000 Subject: [PATCH 3/6] fix unnecessary changes to users --- routers/user.py | 1 + tests/test_user.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/user.py b/routers/user.py index fd7ca82d..3a41e041 100644 --- a/routers/user.py +++ b/routers/user.py @@ -89,6 +89,7 @@ async def check_is_admin( """Check if the current user has admin privileges.""" return {"is_admin": user.is_admin(settings)} + @router.get("/services/approved", response_model=Dict[str, List[Service]]) async def get_approved_services( user: Annotated[SessionUser, Depends(get_current_user)], diff --git a/tests/test_user.py b/tests/test_user.py index 1a57cccb..eba70332 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -71,7 +71,6 @@ def mock_user_data(): "/me/resources/approved", "/me/resources/pending", "/me/all/pending", - "/me/is-email-verified", ], ) def test_endpoints_require_auth(endpoint, test_client): From e0e549394074438de920d03eeac1b93035f008cd Mon Sep 17 00:00:00 2001 From: Amanda Zhu Date: Mon, 1 Sep 2025 20:49:17 +1000 Subject: [PATCH 4/6] use auth0 search filter --- routers/admin.py | 11 ++++++++--- tests/test_admin.py | 15 +++++++-------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/routers/admin.py b/routers/admin.py index dfd09b26..2a31425b 100644 --- a/routers/admin.py +++ b/routers/admin.py @@ -75,16 +75,21 @@ def get_revoked_users(client: Annotated[Auth0Client, Depends(get_auth0_client)], resp = client.get_revoked_users(page=pagination.page, per_page=pagination.per_page) return resp + @router.get("/users/unverified", response_model=list[Auth0UserData]) def get_unverified_users( client: Annotated[Auth0Client, Depends(get_auth0_client)], pagination: Annotated[PaginationParams, Depends(get_pagination_params)], ): """ - Return users whose email is not verified. + Return users whose email is not verified, using Auth0 search for efficiency. """ - resp = client.get_users(page=pagination.page, per_page=pagination.per_page) - return [u for u in resp if not getattr(u, "email_verified", False)] + return client.get_users( + page=pagination.page, + per_page=pagination.per_page, + q="email_verified:false", + ) + @router.get("/users/{user_id}", response_model=Auth0UserData) diff --git a/tests/test_admin.py b/tests/test_admin.py index 9a269c51..86b6bc32 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -291,18 +291,17 @@ def test_approve_resource(test_client, as_admin_user, mock_auth0_client, mocker) assert resource_data["id"] == resource.id def test_get_unverified_users(test_client, as_admin_user, mock_auth0_client): - # Mix of verified and unverified users u1 = Auth0UserDataFactory.build(email_verified=False) - u2 = Auth0UserDataFactory.build(email_verified=True) - u3 = Auth0UserDataFactory.build(email_verified=False) + u2 = Auth0UserDataFactory.build(email_verified=False) + mock_auth0_client.get_users.return_value = [u1, u2] - mock_auth0_client.get_users.return_value = [u1, u2, u3] - - resp = test_client.get("/admin/users/unverified") + resp = test_client.get("/admin/users/unverified?page=2&per_page=10") assert resp.status_code == 200 + mock_auth0_client.get_users.assert_called_once_with( + page=2, per_page=10, q="email_verified:false" + ) + data = resp.json() assert len(data) == 2 - returned_ids = {u["user_id"] for u in data} - assert returned_ids == {u1.user_id, u3.user_id} assert all(u["email_verified"] is False for u in data) From 430f1e5e24116c21b20c2e3d1a5e613b471f3c9b Mon Sep 17 00:00:00 2001 From: Amanda Zhu Date: Tue, 2 Sep 2025 09:10:12 +1000 Subject: [PATCH 5/6] make ruff happy --- tests/test_admin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_admin.py b/tests/test_admin.py index 86b6bc32..4e582a8c 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -14,7 +14,6 @@ AccessTokenPayloadFactory, AppMetadataFactory, Auth0UserDataFactory, - EmailVerificationResponseFactory, SessionUserFactory, ) From b41612111b4f4b59ca1d87af0b74116dbaf0a298 Mon Sep 17 00:00:00 2001 From: Amanda Zhu Date: Tue, 2 Sep 2025 09:42:01 +1000 Subject: [PATCH 6/6] put back test_resend_verification_email --- tests/test_admin.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_admin.py b/tests/test_admin.py index 4e582a8c..71eefdbf 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -14,6 +14,7 @@ AccessTokenPayloadFactory, AppMetadataFactory, Auth0UserDataFactory, + EmailVerificationResponseFactory, SessionUserFactory, ) @@ -289,6 +290,16 @@ def test_approve_resource(test_client, as_admin_user, mock_auth0_client, mocker) assert resource_data["status"] == "approved" assert resource_data["id"] == resource.id + +def test_resend_verification_email(test_client, as_admin_user, mock_auth0_client): + user = Auth0UserDataFactory.build() + response_data = EmailVerificationResponseFactory.build() + mock_auth0_client.resend_verification_email.return_value = response_data + resp = test_client.post(f"/admin/users/{user.user_id}/verification-email/resend") + assert resp.status_code == 200 + assert resp.json() == {"message": "Verification email resent."} + + def test_get_unverified_users(test_client, as_admin_user, mock_auth0_client): u1 = Auth0UserDataFactory.build(email_verified=False) u2 = Auth0UserDataFactory.build(email_verified=False)