- Fix pipeline granularity: add market_year to cleaned/serving SQL models - Add DuckDB data access layer with async query functions (analytics.py) - Build Chart.js dashboard: supply/demand, STU ratio, top producers, YoY table - Add country comparison page with multi-select picker - Replace items CRUD with read-only commodity API (list, metrics, countries, CSV) - Configure BeanFlows plan tiers (Free/Starter/Pro) with feature gating - Rewrite public pages for coffee market intelligence positioning - Remove boilerplate items schema, update health check for DuckDB - Add test suite: 139 tests passing (dashboard, API, billing) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
130 lines
4.5 KiB
Python
130 lines
4.5 KiB
Python
"""
|
|
Tests for the commodity analytics API endpoints.
|
|
"""
|
|
import hashlib
|
|
import secrets
|
|
from datetime import datetime
|
|
|
|
import pytest
|
|
|
|
|
|
async def _create_api_key_for_user(db, user_id, plan="starter"):
|
|
"""Helper: create an API key and subscription, return the raw key."""
|
|
raw_key = f"sk_{secrets.token_urlsafe(32)}"
|
|
key_hash = hashlib.sha256(raw_key.encode()).hexdigest()
|
|
now = datetime.utcnow().isoformat()
|
|
|
|
await db.execute(
|
|
"""INSERT INTO api_keys (user_id, name, key_hash, key_prefix, scopes, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)""",
|
|
(user_id, "test-key", key_hash, raw_key[:12], "read,write", now),
|
|
)
|
|
|
|
# Create subscription for plan
|
|
if plan != "free":
|
|
await db.execute(
|
|
"""INSERT OR REPLACE INTO subscriptions
|
|
(user_id, plan, status, created_at, updated_at)
|
|
VALUES (?, ?, 'active', ?, ?)""",
|
|
(user_id, plan, now, now),
|
|
)
|
|
await db.commit()
|
|
|
|
return raw_key
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_api_requires_auth(client):
|
|
"""API returns 401 without auth header."""
|
|
response = await client.get("/api/v1/commodities")
|
|
assert response.status_code == 401
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_api_rejects_free_plan(client, db, test_user, mock_analytics):
|
|
"""API returns 403 for free plan users."""
|
|
raw_key = await _create_api_key_for_user(db, test_user["id"], plan="free")
|
|
response = await client.get(
|
|
"/api/v1/commodities",
|
|
headers={"Authorization": f"Bearer {raw_key}"},
|
|
)
|
|
assert response.status_code == 403
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_commodities(client, db, test_user, mock_analytics):
|
|
"""GET /commodities returns commodity list."""
|
|
raw_key = await _create_api_key_for_user(db, test_user["id"])
|
|
response = await client.get(
|
|
"/api/v1/commodities",
|
|
headers={"Authorization": f"Bearer {raw_key}"},
|
|
)
|
|
assert response.status_code == 200
|
|
data = await response.get_json()
|
|
assert "commodities" in data
|
|
assert len(data["commodities"]) == 2
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_commodity_metrics(client, db, test_user, mock_analytics):
|
|
"""GET /commodities/<code>/metrics returns time series."""
|
|
raw_key = await _create_api_key_for_user(db, test_user["id"])
|
|
response = await client.get(
|
|
"/api/v1/commodities/711100/metrics?metrics=Production&metrics=Exports",
|
|
headers={"Authorization": f"Bearer {raw_key}"},
|
|
)
|
|
assert response.status_code == 200
|
|
data = await response.get_json()
|
|
assert data["commodity_code"] == 711100
|
|
assert "Production" in data["metrics"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_commodity_metrics_invalid_metric(client, db, test_user, mock_analytics):
|
|
"""GET /commodities/<code>/metrics rejects invalid metrics."""
|
|
raw_key = await _create_api_key_for_user(db, test_user["id"])
|
|
response = await client.get(
|
|
"/api/v1/commodities/711100/metrics?metrics=DROP_TABLE",
|
|
headers={"Authorization": f"Bearer {raw_key}"},
|
|
)
|
|
assert response.status_code == 400
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_commodity_countries(client, db, test_user, mock_analytics):
|
|
"""GET /commodities/<code>/countries returns ranking."""
|
|
raw_key = await _create_api_key_for_user(db, test_user["id"])
|
|
response = await client.get(
|
|
"/api/v1/commodities/711100/countries?metric=Production&limit=5",
|
|
headers={"Authorization": f"Bearer {raw_key}"},
|
|
)
|
|
assert response.status_code == 200
|
|
data = await response.get_json()
|
|
assert data["metric"] == "Production"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_commodity_csv_export(client, db, test_user, mock_analytics):
|
|
"""GET /commodities/<code>/metrics.csv returns CSV."""
|
|
raw_key = await _create_api_key_for_user(db, test_user["id"])
|
|
response = await client.get(
|
|
"/api/v1/commodities/711100/metrics.csv",
|
|
headers={"Authorization": f"Bearer {raw_key}"},
|
|
)
|
|
assert response.status_code == 200
|
|
assert "text/csv" in response.content_type
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_me_endpoint(client, db, test_user, mock_analytics):
|
|
"""GET /me returns user info."""
|
|
raw_key = await _create_api_key_for_user(db, test_user["id"])
|
|
response = await client.get(
|
|
"/api/v1/me",
|
|
headers={"Authorization": f"Bearer {raw_key}"},
|
|
)
|
|
assert response.status_code == 200
|
|
data = await response.get_json()
|
|
assert data["email"] == "test@example.com"
|
|
assert data["plan"] == "starter"
|