diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index b754c1e..de225c6 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -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
diff --git a/.gitignore b/.gitignore
index 2a814ab..d6adbc4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,5 @@ node_modules
container_data
personal_work_dir
*.log
+CLAUDE.md
+temp/
diff --git a/apollo/server/routes/admin_users.py b/apollo/server/routes/admin_users.py
index c45c9d6..b30a336 100644
--- a/apollo/server/routes/admin_users.py
+++ b/apollo/server/routes/admin_users.py
@@ -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", {
@@ -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)
diff --git a/apollo/server/templates/admin_user.jinja b/apollo/server/templates/admin_user.jinja
index efc427e..b9f5ca2 100644
--- a/apollo/server/templates/admin_user.jinja
+++ b/apollo/server/templates/admin_user.jinja
@@ -42,25 +42,10 @@
Danger Zone
-
-
-
- Delete user
-
-
- Are you sure you want to delete {{ user.name }}?
-
-
- Cancel
- Delete
-
-
-
-
-
- Delete user
-
diff --git a/apollo/tests/BUILD.bazel b/apollo/tests/BUILD.bazel
index 806782f..f8a9725 100644
--- a/apollo/tests/BUILD.bazel
+++ b/apollo/tests/BUILD.bazel
@@ -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"],
diff --git a/apollo/tests/test_admin_users.py b/apollo/tests/test_admin_users.py
new file mode 100644
index 0000000..75070c7
--- /dev/null
+++ b/apollo/tests/test_admin_users.py
@@ -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)