refactor: restructure repo into core/app modules with per-study folders
Reorganize Palladium codebase into a modular architecture with `core/` shared logic and `app/` Streamlit UI, separating per-study assets into `studies/YYYYMM_<Vendor>/` folders containing notebooks, seed data, and configuration. Update README to reflect new structure, add `.gitignore` entries for `.env` and study exports, and refresh component documentation.
This commit is contained in:
174
tests/test_client.py
Normal file
174
tests/test_client.py
Normal file
@@ -0,0 +1,174 @@
|
||||
"""
|
||||
TEI client tests with mocked HTTP.
|
||||
|
||||
We mock ``requests.Session.request`` so tests do not require network access
|
||||
or a live Athena instance.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from core.tei_client import AthenaAPIError, TEIClient
|
||||
|
||||
|
||||
def _mock_response(status: int, body=None) -> MagicMock:
|
||||
resp = MagicMock()
|
||||
resp.status_code = status
|
||||
resp.content = b"{}" if body is None else json.dumps(body).encode()
|
||||
resp.json.return_value = body if body is not None else {}
|
||||
resp.text = json.dumps(body or {})
|
||||
return resp
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(monkeypatch) -> TEIClient:
|
||||
c = TEIClient()
|
||||
c.session = MagicMock()
|
||||
return c
|
||||
|
||||
|
||||
class TestConfig:
|
||||
def test_requires_base_url(self, monkeypatch):
|
||||
monkeypatch.delenv("ATHENA_BASE_URL", raising=False)
|
||||
with pytest.raises(ValueError, match="ATHENA_BASE_URL"):
|
||||
TEIClient(api_key="x")
|
||||
|
||||
def test_requires_api_key(self, monkeypatch):
|
||||
monkeypatch.delenv("ATHENA_API_KEY", raising=False)
|
||||
with pytest.raises(ValueError, match="ATHENA_API_KEY"):
|
||||
TEIClient(base_url="https://example.com")
|
||||
|
||||
def test_authorization_header(self):
|
||||
c = TEIClient(base_url="https://example.com", api_key="abc123")
|
||||
assert c.session.headers["Authorization"] == "Api-Key abc123"
|
||||
|
||||
|
||||
class TestPaths:
|
||||
"""Verify each endpoint targets the documented URL."""
|
||||
|
||||
def _last_call_url(self, client: TEIClient) -> str:
|
||||
return client.session.request.call_args.kwargs["url"]
|
||||
|
||||
def test_list_reports_path(self, client):
|
||||
client.session.request.return_value = _mock_response(
|
||||
200, {"results": [], "next": None}
|
||||
)
|
||||
client.list_reports()
|
||||
assert self._last_call_url(client) == "https://athena.test/api/v1/tei/reports/"
|
||||
|
||||
def test_get_tool_path(self, client):
|
||||
client.session.request.return_value = _mock_response(200, {"id": "abc"})
|
||||
client.get_tool("abc123")
|
||||
assert self._last_call_url(client).endswith("/api/v1/tei/tools/abc123/")
|
||||
|
||||
def test_calculate_path(self, client):
|
||||
client.session.request.return_value = _mock_response(200, {})
|
||||
client.calculate("abc")
|
||||
assert self._last_call_url(client).endswith("/api/v1/tei/tools/abc/calculate/")
|
||||
assert client.session.request.call_args.kwargs["method"] == "POST"
|
||||
|
||||
def test_export_path(self, client):
|
||||
client.session.request.return_value = _mock_response(200, {})
|
||||
client.export("abc")
|
||||
assert self._last_call_url(client).endswith("/api/v1/tei/tools/abc/export/")
|
||||
|
||||
def test_aggregate_summary_path(self, client):
|
||||
client.session.request.return_value = _mock_response(200, {})
|
||||
client.aggregate_summary()
|
||||
assert self._last_call_url(client).endswith("/api/v1/tei/summary/")
|
||||
|
||||
def test_save_version_path(self, client):
|
||||
client.session.request.return_value = _mock_response(201, {"version_number": 1})
|
||||
client.save_version("abc", note="initial")
|
||||
url = self._last_call_url(client)
|
||||
assert url.endswith("/api/v1/tei/tools/abc/versions/")
|
||||
body = client.session.request.call_args.kwargs["json"]
|
||||
assert body == {"note": "initial"}
|
||||
|
||||
|
||||
class TestErrorHandling:
|
||||
def test_404_raises_athena_error(self, client):
|
||||
client.session.request.return_value = _mock_response(
|
||||
404, {"detail": "Not found"}
|
||||
)
|
||||
with pytest.raises(AthenaAPIError) as ei:
|
||||
client.get_tool("missing")
|
||||
assert ei.value.status_code == 404
|
||||
assert "Not found" in ei.value.detail
|
||||
|
||||
def test_test_connection_returns_error_dict(self, client):
|
||||
client.session.request.return_value = _mock_response(
|
||||
401, {"detail": "Invalid token"}
|
||||
)
|
||||
result = client.test_connection()
|
||||
assert result["status"] == "error"
|
||||
assert result["authenticated"] is False
|
||||
assert result["error_code"] == 401
|
||||
|
||||
|
||||
class TestPagination:
|
||||
def test_walks_next_links(self, client):
|
||||
# First page returns one item with a `next` URL; second page returns
|
||||
# one more item and no next.
|
||||
page1 = _mock_response(
|
||||
200,
|
||||
{
|
||||
"results": [{"id": 1}],
|
||||
"next": "https://athena.test/api/v1/tei/reports/?page=2",
|
||||
},
|
||||
)
|
||||
page2 = _mock_response(200, {"results": [{"id": 2}], "next": None})
|
||||
client.session.request.return_value = page1
|
||||
client.session.get.return_value = page2 # follow next via session.get
|
||||
|
||||
out = client.list_reports()
|
||||
assert [r["id"] for r in out] == [1, 2]
|
||||
|
||||
|
||||
class TestNormalizeValue:
|
||||
def test_year_underscore_keys(self):
|
||||
out = TEIClient._normalize_value(
|
||||
{"field_key": "x", "year_1": 100, "year_2": 200, "risk_adjustment": 0.1}
|
||||
)
|
||||
assert out["year_values"] == {"1": 100.0, "2": 200.0}
|
||||
assert out["risk_adjustment"] == 0.1
|
||||
|
||||
def test_year_values_dict_passthrough(self):
|
||||
out = TEIClient._normalize_value(
|
||||
{
|
||||
"field_key": "x",
|
||||
"year_values": {"1": 50, "3": 75},
|
||||
"notes": " hi ",
|
||||
}
|
||||
)
|
||||
assert out["year_values"] == {"1": 50.0, "3": 75.0}
|
||||
assert out["notes"] == " hi "
|
||||
|
||||
def test_initial_carried(self):
|
||||
out = TEIClient._normalize_value(
|
||||
{"field_key": "x", "initial": 1000, "year_1": 5}
|
||||
)
|
||||
assert out["initial"] == 1000.0
|
||||
|
||||
def test_scalar_value(self):
|
||||
out = TEIClient._normalize_value({"field_key": "rate", "value": 0.10})
|
||||
assert out["value"] == 0.10
|
||||
assert "year_values" not in out
|
||||
|
||||
|
||||
class TestUpdateValuesPayload:
|
||||
def test_wraps_in_envelope(self, client):
|
||||
client.session.request.return_value = _mock_response(200, {})
|
||||
client.update_values(
|
||||
"abc",
|
||||
[{"field_key": "x", "year_1": 100}, {"field_key": "y", "year_1": 200}],
|
||||
)
|
||||
body = client.session.request.call_args.kwargs["json"]
|
||||
assert "values" in body
|
||||
assert len(body["values"]) == 2
|
||||
assert body["values"][0]["field_key"] == "x"
|
||||
assert body["values"][0]["year_values"] == {"1": 100.0}
|
||||
Reference in New Issue
Block a user