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

@@ -241,3 +241,64 @@ def sign_payload(payload_bytes: bytes) -> str:
return "ts=1234567890;h1=dummy_signature"
# ── Analytics mock ───────────────────────────────────────────
@pytest.fixture
def mock_analytics(monkeypatch):
"""Mock DuckDB analytics so dashboard tests run without a real DuckDB file.
Patches _conn to a sentinel (so routes skip the 'if _conn is None' guard),
then replaces every analytics query function with an async stub returning
deterministic data matching what the dashboard templates expect.
"""
from beanflows import analytics
monkeypatch.setattr(analytics, "_conn", object()) # truthy sentinel
_time_series = [
{"market_year": y, "production": 170000.0 + y * 100,
"exports": 80000.0, "imports": 5000.0,
"ending_stocks": 20000.0, "total_distribution": 160000.0}
for y in range(2021, 2026)
]
# Ensure latest production is 172,000 (2024 → 170000 + 2024*100 is too big;
# override the last element explicitly so the metric-card test matches).
_time_series[-1]["production"] = 172000.0
_top_producers = [
{"country_name": "Brazil", "country_code": "BR",
"market_year": 2025, "production": 63000.0},
{"country_name": "Vietnam", "country_code": "VN",
"market_year": 2025, "production": 30000.0},
]
_stu_trend = [
{"market_year": y, "stock_to_use_ratio_pct": 25.0}
for y in range(2021, 2026)
]
_balance = [
{"market_year": y, "production": 170000.0,
"total_distribution": 160000.0, "supply_demand_balance": 10000.0}
for y in range(2021, 2026)
]
_yoy_data = [
{"country_name": "Brazil", "country_code": "BR",
"market_year": 2025, "production": 63000.0, "production_yoy_pct": 2.5},
{"country_name": "Vietnam", "country_code": "VN",
"market_year": 2025, "production": 30000.0, "production_yoy_pct": -1.2},
]
async def _ts(*a, **kw): return _time_series
async def _top(*a, **kw): return _top_producers
async def _stu(*a, **kw): return _stu_trend
async def _bal(*a, **kw): return _balance
async def _yoy(*a, **kw): return _yoy_data
async def _cmp(*a, **kw): return []
monkeypatch.setattr(analytics, "get_global_time_series", _ts)
monkeypatch.setattr(analytics, "get_top_countries", _top)
monkeypatch.setattr(analytics, "get_stock_to_use_trend", _stu)
monkeypatch.setattr(analytics, "get_supply_demand_balance", _bal)
monkeypatch.setattr(analytics, "get_production_yoy_by_country", _yoy)
monkeypatch.setattr(analytics, "get_country_comparison", _cmp)