Files
stack/tests/conf/test_conf.py
kert b45c1a9b74 add FastAPI app with JWT auth, [api] config section, stack api serve refs #3 refs #4
New [api] section in stack.toml (host, port, secret, workers) with
env var override via STACK_API_SECRET.  conf.secret() resolves
env var > config file for sensitive values.

FastAPI endpoints:
  POST /auth/token — issue JWT from configured secret
  GET  /health — service status
  GET  /pipelines — list pipelines with step counts
  GET  /pipelines/{name} — pipeline detail
  POST /pipelines/run/{pipeline} — trigger run (auth required)
  GET  /pipelines/run/{job_id}/status — poll job status
  GET  /bib/items — query bibliography
  GET  /bib/tags — tag namespace counts
  GET  /schema/{table} — JSON Schema from SQLTable models

CLI: stack api serve [--host] [--port] [--workers] [--reload]
12 new API tests, 6 new conf tests.
2026-03-12 14:59:30 -04:00

168 lines
5.0 KiB
Python

"""Tests for conf — centralised configuration loader."""
from __future__ import annotations
from pathlib import Path
from conf import ROOT, cfg, context, path, reload, secret
class TestCfg:
def test_root_is_repo_root(self) -> None:
assert (ROOT / "stack.toml").exists()
assert (ROOT / "pyproject.toml").exists()
def test_db_section_exists(self) -> None:
assert "db" in cfg
def test_db_aco_value(self) -> None:
assert cfg.db.aco == "notebooks/aco.duckdb"
def test_db_bib_value(self) -> None:
assert cfg.db.bib == "data/bib.sqlite"
def test_db_zotero_value(self) -> None:
assert cfg.db.zotero == "zotero/data/zotero.sqlite"
def test_storage_bcda(self) -> None:
assert cfg.storage.bcda == "data/bcda"
def test_bcda_timeout_is_float(self) -> None:
assert isinstance(cfg.bcda.timeout, float)
def test_bcda_max_retries_is_int(self) -> None:
assert isinstance(cfg.bcda.max_retries, int)
def test_lake_trino_port_is_int(self) -> None:
assert isinstance(cfg.lake.trino.port, int)
def test_dict_access(self) -> None:
val = cfg["db"]["aco"]
assert val == "notebooks/aco.duckdb"
def test_to_dict(self) -> None:
d = cfg.db.to_dict()
assert isinstance(d, dict)
assert "aco" in d
def test_contains(self) -> None:
assert "db" in cfg
assert "nonexistent_key" not in cfg
def test_repr(self) -> None:
r = repr(cfg.db)
assert "aco" in r
def test_missing_attr_raises(self) -> None:
import pytest
with pytest.raises(AttributeError):
cfg.no_such_section
class TestPath:
def test_path_returns_absolute(self) -> None:
p = path("db.aco")
assert isinstance(p, Path)
assert p.is_absolute()
def test_path_resolves_relative_to_root(self) -> None:
p = path("db.aco")
assert p == ROOT / "notebooks/aco.duckdb"
def test_path_nested_key(self) -> None:
p = path("db.bib")
assert p == ROOT / "data/bib.sqlite"
class TestReload:
def test_reload_does_not_raise(self) -> None:
reload()
assert cfg.db.aco == "notebooks/aco.duckdb"
class TestContext:
def test_default_context_is_local(self) -> None:
ctx = context()
assert ctx.db_backend == "duckdb"
assert ctx.storage_backend == "local"
def test_env_var_overrides_active(self, monkeypatch) -> None:
monkeypatch.setenv("STACK_CONTEXT", "lake")
reload()
ctx = context()
assert ctx.db_backend == "iceberg"
assert ctx.storage_backend == "s3"
def test_databricks_context(self, monkeypatch) -> None:
monkeypatch.setenv("STACK_CONTEXT", "databricks")
ctx = context()
assert ctx.db_backend == "databricks"
assert ctx.storage_backend == "dbfs"
def test_trino_context(self, monkeypatch) -> None:
monkeypatch.setenv("STACK_CONTEXT", "trino")
ctx = context()
assert ctx.db_backend == "trino"
assert ctx.storage_backend == "s3"
def test_unknown_context_raises(self, monkeypatch) -> None:
import pytest
monkeypatch.setenv("STACK_CONTEXT", "nonexistent")
with pytest.raises(KeyError):
context()
class TestSecret:
def test_env_var_takes_precedence(self, monkeypatch) -> None:
monkeypatch.setenv("STACK_API_SECRET", "from-env")
assert secret("api.secret", "STACK_API_SECRET") == "from-env"
def test_falls_back_to_config(self, monkeypatch) -> None:
monkeypatch.delenv("STACK_API_SECRET", raising=False)
# config value is empty string by default
assert secret("api.secret", "STACK_API_SECRET") == ""
def test_reads_non_empty_config(self, monkeypatch) -> None:
import conf
monkeypatch.delenv("_CONF_TEST_SECRET_", raising=False)
monkeypatch.setitem(conf.cfg._data["api"], "secret", "from-toml")
result = secret("api.secret", "_CONF_TEST_SECRET_")
assert result == "from-toml"
class TestApiConfig:
def test_api_host(self) -> None:
assert cfg.api.host == "0.0.0.0"
def test_api_port(self) -> None:
assert cfg.api.port == 8000
def test_api_workers(self) -> None:
assert cfg.api.workers == 1
class TestFindRootError:
def test_raises_when_no_stack_toml(self, tmp_path: Path) -> None:
import pytest
# Monkeypatch __file__ won't work easily, but we can test
# the function directly with a path that has no stack.toml
import conf
from conf import _find_root
original = conf.__file__
try:
# Point to a deep tmp path with no stack.toml
deep = tmp_path / "a" / "b" / "c" / "d" / "e"
deep.mkdir(parents=True)
fake_init = deep / "__init__.py"
fake_init.write_text("")
conf.__file__ = str(fake_init)
with pytest.raises(FileNotFoundError, match="stack.toml"):
_find_root()
finally:
conf.__file__ = original