- 76 new tests covering CLI commands, API routes, auth, runner, load modules — coverage 99% (169 remaining lines are infrastructure) - Enhanced /health endpoint with DuckDB, bib, pipeline service checks - Added `stack health` CLI command with graceful degradation - SnapshotManager for DuckDB data versioning and rollback - .woodpecker.yml CI pipeline: lint, test (99% gate), marimo check - Error handling audit: no bare except clauses, all exceptions logged Closes #31, #32, #33, #34, #35.
104 lines
3.1 KiB
Python
104 lines
3.1 KiB
Python
"""Coverage tests for API route edge cases and deps."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
|
|
import jwt
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
@pytest.fixture()
|
|
def api_secret(monkeypatch):
|
|
secret = "test-secret-key-for-jwt-testing"
|
|
monkeypatch.setenv("STACK_API_SECRET", secret)
|
|
return secret
|
|
|
|
|
|
@pytest.fixture()
|
|
def client():
|
|
from api.server import app
|
|
|
|
return TestClient(app)
|
|
|
|
|
|
@pytest.fixture()
|
|
def authed_client(client, api_secret):
|
|
token = jwt.encode({"sub": "test"}, api_secret, algorithm="HS256")
|
|
client.headers["Authorization"] = f"Bearer {token}"
|
|
return client
|
|
|
|
|
|
class TestDepsAuth:
|
|
def test_expired_token(self, client, api_secret) -> None:
|
|
token = jwt.encode(
|
|
{"sub": "test", "exp": int(time.time()) - 10},
|
|
api_secret,
|
|
algorithm="HS256",
|
|
)
|
|
client.headers["Authorization"] = f"Bearer {token}"
|
|
r = client.post("/pipelines/run/readmissions")
|
|
assert r.status_code == 401
|
|
assert "expired" in r.json()["detail"].lower()
|
|
|
|
def test_invalid_token(self, client, api_secret) -> None:
|
|
client.headers["Authorization"] = "Bearer invalid.token.here"
|
|
r = client.post("/pipelines/run/readmissions")
|
|
assert r.status_code == 401
|
|
|
|
def test_missing_bearer(self, client, api_secret) -> None:
|
|
r = client.post("/pipelines/run/readmissions")
|
|
assert r.status_code in (401, 403)
|
|
|
|
|
|
class TestBibRoutes:
|
|
def test_list_items_with_filter(self, client) -> None:
|
|
r = client.get("/bib/items?tag=module:aco&limit=5")
|
|
assert r.status_code == 200
|
|
|
|
def test_list_items_with_type(self, client) -> None:
|
|
r = client.get("/bib/items?item_type=Source")
|
|
assert r.status_code == 200
|
|
|
|
def test_list_tags(self, client) -> None:
|
|
r = client.get("/bib/tags")
|
|
assert r.status_code == 200
|
|
|
|
|
|
class TestSchemaRoutes:
|
|
def test_get_schema_found(self, client) -> None:
|
|
# Try to find a real table
|
|
|
|
# Pick any table that exists
|
|
import aco.table.core as core_mod
|
|
|
|
for attr in dir(core_mod):
|
|
cls = getattr(core_mod, attr)
|
|
if isinstance(cls, type) and hasattr(cls, "__tablename__"):
|
|
table_name = cls.__tablename__
|
|
r = client.get(f"/schema/{table_name}")
|
|
if r.status_code == 200:
|
|
assert "properties" in r.json()
|
|
return
|
|
pytest.skip("No table models found")
|
|
|
|
|
|
class TestPipelineRoutes:
|
|
def test_run_with_body(self, authed_client) -> None:
|
|
r = authed_client.post(
|
|
"/pipelines/run/readmissions",
|
|
json={"target": "local", "save": False},
|
|
)
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
assert data["status"] == "running"
|
|
|
|
def test_pipeline_detail_inputs_outputs(self, client) -> None:
|
|
r = client.get("/pipelines/readmissions")
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
assert "inputs" in data
|
|
assert "outputs" in data
|
|
assert len(data["outputs"]) > 0
|