"""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"]