Remove password admin login, seed dev accounts, add regression tests

Admin flow:
- Remove /admin/login (password-based) and /admin/dev-login routes entirely
- admin_required now checks only the 'admin' role; redirects to auth.login
- auth/dev-login with an ADMIN_EMAILS address redirects directly to /admin/
- .env.example: replace ADMIN_PASSWORD with ADMIN_EMAILS=admin@beanflows.coffee

Dev seeding:
- Add dev_seed.py: idempotent upsert of 4 fixed accounts (admin, free,
  starter, pro) so every access tier is testable after dev_run.sh
- dev_run.sh: seed after migrations, show all 4 login shortcuts

Regression tests (37 passing):
- test_analytics.py: concurrent fetch_analytics calls return correct row
  counts (cursor thread-safety regression), column names are lowercase
- test_roles.py TestAdminAuthFlow: password login routes return 404,
  admin_required redirects to auth.login, dev-login grants admin role
  and redirects to admin panel when email is in ADMIN_EMAILS
- conftest.py: add mock_analytics fixture (fixes 7 pre-existing dashboard
  test errors); fix assertion text and lowercase metric param in tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-20 20:10:45 +01:00
parent fef9f3d705
commit d09ba91023
9 changed files with 425 additions and 69 deletions

View File

@@ -240,3 +240,97 @@ class TestImpersonation:
async with c.session_transaction() as sess:
assert sess["user_id"] == test_user["id"]
assert "admin_impersonating" not in sess
# ════════════════════════════════════════════════════════════
# Admin auth flow regressions
# ════════════════════════════════════════════════════════════
class TestAdminAuthFlow:
"""Regression tests for the admin authentication flow.
Previously the admin panel used a password-based login (/admin/login).
That has been removed; the only way in is via the 'admin' role, granted
through ADMIN_EMAILS + auth/dev-login (dev) or the user_roles table (prod).
"""
async def test_admin_login_route_removed(self, client):
"""
Regression: /admin/login existed as a password-based route.
It has been removed; any request should 404.
"""
response = await client.get("/admin/login")
assert response.status_code == 404
async def test_admin_dev_login_route_removed(self, client):
"""
Regression: /admin/dev-login set session['is_admin']=True without
a user_id, making the user invisible to load_user and breaking
everything that checked g.user. It has been removed.
"""
response = await client.get("/admin/dev-login")
assert response.status_code == 404
async def test_admin_required_redirects_to_auth_login(self, auth_client, db):
"""
Regression: admin_required used to redirect to admin.login (the
password page). Now it redirects to auth.login.
"""
response = await auth_client.get("/admin/", follow_redirects=False)
assert response.status_code in (302, 303, 307)
location = response.headers.get("Location", "")
assert "/auth/login" in location, (
f"Expected redirect to /auth/login, got: {location}"
)
async def test_dev_login_grants_admin_role_for_admin_email(
self, app, db, monkeypatch
):
"""
Regression: auth/dev-login was not granting the admin role because
ADMIN_EMAILS was empty (missing from .env). The role must be granted
when the email matches ADMIN_EMAILS.
"""
from beanflows import core as _core
monkeypatch.setattr(_core.config, "ADMIN_EMAILS", ["admin@example.com"])
async with app.test_client() as c:
response = await c.get(
"/auth/dev-login?email=admin@example.com",
follow_redirects=False,
)
assert response.status_code in (302, 303, 307)
# user_id must be set in session
async with c.session_transaction() as sess:
user_id = sess.get("user_id")
assert user_id is not None
# admin role must exist in the DB
row = await core.fetch_one(
"SELECT role FROM user_roles WHERE user_id = ? AND role = 'admin'",
(user_id,),
)
assert row is not None, "admin role was not granted via dev-login"
async def test_dev_login_admin_redirects_to_admin_panel(
self, app, db, monkeypatch
):
"""
Regression: auth/dev-login redirected everyone to dashboard.index.
Admin-email logins should land directly on /admin/.
"""
from beanflows import core as _core
monkeypatch.setattr(_core.config, "ADMIN_EMAILS", ["admin@example.com"])
async with app.test_client() as c:
response = await c.get(
"/auth/dev-login?email=admin@example.com",
follow_redirects=False,
)
location = response.headers.get("Location", "")
assert "/admin" in location, (
f"Admin dev-login should redirect to /admin, got: {location}"
)