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:
@@ -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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user