Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ jobs:
bazel test //apollo/tests:test_api_updateinfo --test_output=all
bazel test //apollo/tests:test_validation --test_output=all
bazel test //apollo/tests:test_admin_routes_supported_products --test_output=all
bazel test //apollo/tests:test_admin_users --test_output=all
bazel test //apollo/tests:test_api_osv --test_output=all
bazel test //apollo/tests:test_database_service --test_output=all
bazel test //apollo/tests:test_rh_matcher_activities --test_output=all
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ node_modules
container_data
personal_work_dir
*.log
CLAUDE.md
temp/
10 changes: 0 additions & 10 deletions apollo/server/routes/admin_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,6 @@ async def admin_user_delete(request: Request, user_id: int):
}
)

# Cannot delete yourself
if user.id == request.state.user.id:
return templates.TemplateResponse(
"error.jinja", {
Expand All @@ -220,15 +219,6 @@ async def admin_user_delete(request: Request, user_id: int):
}
)

# Cannot delete admins
if user.role == "admin":
return templates.TemplateResponse(
"error.jinja", {
"request": request,
"message": "Cannot delete admin users",
}
)

await user.delete()

return RedirectResponse("/admin/users", status_code=302)
21 changes: 3 additions & 18 deletions apollo/server/templates/admin_user.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -42,25 +42,10 @@

<h2 class="bx--type-productive-heading-03" style="margin-top:2rem;">Danger Zone</h2>

<bx-modal id="delete-user-modal">
<bx-modal-header>
<bx-modal-close-button></bx-modal-close-button>
<bx-modal-heading>Delete user</bx-modal-heading>
</bx-modal-header>
<bx-modal-body>
<p>Are you sure you want to delete {{ user.name }}?</p>
</bx-modal-body>
<bx-modal-footer>
<bx-modal-footer-button kind="secondary" data-modal-close>Cancel</bx-modal-footer-button>
<bx-modal-footer-button kind="danger">Delete</bx-modal-footer-button>
</bx-modal-footer>
</bx-modal>

<form id="delete_user_form" action="/admin/users/{{ user.id }}/delete" method="POST">
<form action="/admin/users/{{ user.id }}/delete" method="POST"
onsubmit="return confirm('Delete this user? This cannot be undone.');">
<button type="submit" class="bx--btn bx--btn--danger">Delete user</button>
</form>
<bx-btn kind="danger" open_modal="delete-user-modal">
Delete user
</bx-btn>
</div>
</div>
</div>
Expand Down
8 changes: 8 additions & 0 deletions apollo/tests/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ py_test(
],
)

py_test(
name = "test_admin_users",
srcs = ["test_admin_users.py"],
deps = [
"//apollo/server:server_lib",
],
)

py_test(
name = "test_api_osv",
srcs = ["test_api_osv.py"],
Expand Down
104 changes: 104 additions & 0 deletions apollo/tests/test_admin_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""
Tests for the admin_users delete route.
Mocks the database and template layers to test authorization behavior.
"""

import asyncio
import sys
import os
import unittest
from unittest.mock import AsyncMock, MagicMock, patch

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../.."))

from apollo.server.routes.admin_users import admin_user_delete


def _make_request(current_user_id: int) -> MagicMock:
request = MagicMock()
request.state.user = MagicMock()
request.state.user.id = current_user_id
return request


def _make_user(user_id: int, role: str = "elevated") -> MagicMock:
user = MagicMock()
user.id = user_id
user.role = role
user.delete = AsyncMock()
return user


class TestAdminUserDelete(unittest.TestCase):
"""Authorization and behavior tests for admin_user_delete."""

def test_delete_user_success_returns_redirect(self):
"""Deleting another user redirects to the users list and calls delete()."""
target = _make_user(user_id=7, role="elevated")
request = _make_request(current_user_id=1)

with patch(
"apollo.server.routes.admin_users.User.get_or_none",
new=AsyncMock(return_value=target),
):
response = asyncio.run(admin_user_delete(request, user_id=7))

target.delete.assert_awaited_once()
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["location"], "/admin/users")

def test_delete_admin_user_is_allowed(self):
"""Admin users can be deleted (regression: prior guard was removed)."""
target = _make_user(user_id=7, role="admin")
request = _make_request(current_user_id=1)

with patch(
"apollo.server.routes.admin_users.User.get_or_none",
new=AsyncMock(return_value=target),
):
response = asyncio.run(admin_user_delete(request, user_id=7))

target.delete.assert_awaited_once()
self.assertEqual(response.status_code, 302)

def test_cannot_delete_yourself(self):
"""Self-deletion is blocked and returns an error response without deleting."""
target = _make_user(user_id=1, role="admin")
request = _make_request(current_user_id=1)

with patch(
"apollo.server.routes.admin_users.User.get_or_none",
new=AsyncMock(return_value=target),
), patch(
"apollo.server.routes.admin_users.templates.TemplateResponse"
) as mock_template:
mock_template.return_value = MagicMock()
asyncio.run(admin_user_delete(request, user_id=1))

target.delete.assert_not_awaited()
mock_template.assert_called_once()
template_name, context = mock_template.call_args.args
self.assertEqual(template_name, "error.jinja")
self.assertEqual(context["message"], "Cannot delete yourself")

def test_delete_nonexistent_user_returns_error(self):
"""Deleting a missing user returns an error response, no delete() call."""
request = _make_request(current_user_id=1)

with patch(
"apollo.server.routes.admin_users.User.get_or_none",
new=AsyncMock(return_value=None),
), patch(
"apollo.server.routes.admin_users.templates.TemplateResponse"
) as mock_template:
mock_template.return_value = MagicMock()
asyncio.run(admin_user_delete(request, user_id=999))

mock_template.assert_called_once()
template_name, context = mock_template.call_args.args
self.assertEqual(template_name, "error.jinja")
self.assertIn("999", context["message"])


if __name__ == "__main__":
unittest.main(verbosity=2)
Loading