v0.1.27
Fix: Throughput API v1 available - Details posted to Irving. POWERBI_KEY was missing from the .ENV file, so was not live. Add: Editor now supports editing a mix's resolved formula directly, with % and kg dual entry on ingredient rows Fix: Mix Editor should bring through correct ingredients. New resolved formula (same logic we use in Mix Calculator). Fix: Security headers on all API responses (hardening) Add: New mix button available on the Mix Editor. Add: New ingredient button available on the Ingredient Editor
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
"""Tests for the read-only external data API (`/api/v1`) used by Power BI.
|
||||
|
||||
Drives the real FastAPI app via TestClient against an in-memory database, and
|
||||
swaps the configured API key in by replacing the module-level `settings` object
|
||||
(the real one is a frozen dataclass).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import replace
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from app.api import public_v1
|
||||
from app.core.config import settings
|
||||
from app.db.session import Base, get_db
|
||||
from app.main import app
|
||||
from app.models.throughput import ProductionThroughput
|
||||
|
||||
API_KEY = "test-powerbi-key"
|
||||
TENANT = "hunter-premium-produce"
|
||||
|
||||
|
||||
def _seed_entry(db, *, tenant_id: str, product: str, quantity: float, when: date) -> None:
|
||||
db.add(
|
||||
ProductionThroughput(
|
||||
tenant_id=tenant_id,
|
||||
production_date=when,
|
||||
product_name_snapshot=product,
|
||||
bag_size=20,
|
||||
quantity=quantity,
|
||||
quantity_type="bags",
|
||||
calculated_kg=quantity * 20,
|
||||
created_by="test",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def db_factory():
|
||||
engine = create_engine(
|
||||
"sqlite:///:memory:",
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
TestingSession = sessionmaker(bind=engine, autoflush=False, expire_on_commit=False)
|
||||
|
||||
def override_get_db():
|
||||
db = TestingSession()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
yield TestingSession
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
def _make_client(monkeypatch, *, api_key: str) -> TestClient:
|
||||
# Replace the whole settings object the router reads (frozen dataclass).
|
||||
monkeypatch.setattr(public_v1, "settings", replace(settings, powerbi_api_key=api_key, powerbi_tenant_id=TENANT))
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def test_disabled_when_key_unset(monkeypatch, db_factory):
|
||||
client = _make_client(monkeypatch, api_key="")
|
||||
response = client.get("/api/v1/throughput", headers={"X-API-Key": "anything"})
|
||||
assert response.status_code == 503
|
||||
|
||||
|
||||
def test_rejects_missing_and_wrong_key(monkeypatch, db_factory):
|
||||
client = _make_client(monkeypatch, api_key=API_KEY)
|
||||
assert client.get("/api/v1/throughput").status_code == 401
|
||||
assert client.get("/api/v1/throughput", headers={"X-API-Key": "nope"}).status_code == 401
|
||||
|
||||
|
||||
def test_returns_entries_for_tenant(monkeypatch, db_factory):
|
||||
db = db_factory()
|
||||
_seed_entry(db, tenant_id=TENANT, product="Maize", quantity=10, when=date(2026, 1, 5))
|
||||
_seed_entry(db, tenant_id=TENANT, product="Barley", quantity=5, when=date(2026, 1, 6))
|
||||
# An entry in another tenant must never leak through.
|
||||
_seed_entry(db, tenant_id="someone-else", product="Secret", quantity=99, when=date(2026, 1, 7))
|
||||
db.commit()
|
||||
db.close()
|
||||
|
||||
client = _make_client(monkeypatch, api_key=API_KEY)
|
||||
|
||||
# Header auth.
|
||||
response = client.get("/api/v1/throughput", headers={"X-API-Key": API_KEY})
|
||||
assert response.status_code == 200
|
||||
rows = response.json()
|
||||
names = [row["product_name_snapshot"] for row in rows]
|
||||
assert names == ["Maize", "Barley"] # oldest first, other tenant excluded
|
||||
assert rows[0]["calculated_kg"] == 200.0
|
||||
|
||||
# Query-string auth works too (Power BI Web connector convenience).
|
||||
assert client.get(f"/api/v1/throughput?api_key={API_KEY}").status_code == 200
|
||||
|
||||
|
||||
def test_date_filter(monkeypatch, db_factory):
|
||||
db = db_factory()
|
||||
_seed_entry(db, tenant_id=TENANT, product="Old", quantity=1, when=date(2026, 1, 1))
|
||||
_seed_entry(db, tenant_id=TENANT, product="New", quantity=1, when=date(2026, 2, 1))
|
||||
db.commit()
|
||||
db.close()
|
||||
|
||||
client = _make_client(monkeypatch, api_key=API_KEY)
|
||||
response = client.get("/api/v1/throughput?date_from=2026-01-15", headers={"X-API-Key": API_KEY})
|
||||
assert [row["product_name_snapshot"] for row in response.json()] == ["New"]
|
||||
Reference in New Issue
Block a user