87 lines
4 KiB
Python
87 lines
4 KiB
Python
|
|
"""Bounded source counts do not equate occurrence, acknowledgment and acceptance."""
|
||
|
|
from dataclasses import replace
|
||
|
|
from datetime import timedelta, timezone
|
||
|
|
from urllib.parse import urlencode
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from approval_engine.api import App, call
|
||
|
|
from approval_engine.auth import StaticTokenAuthenticator
|
||
|
|
from approval_engine.errors import StoreUnavailable
|
||
|
|
from approval_engine.store import EVENT_CLASSES
|
||
|
|
from tests.conftest import FROZEN, approve
|
||
|
|
|
||
|
|
|
||
|
|
def query(since=FROZEN, until=FROZEN + timedelta(seconds=1)):
|
||
|
|
return urlencode({"since": since.isoformat(), "until": until.isoformat()})
|
||
|
|
|
||
|
|
|
||
|
|
def test_occurrence_window_separates_pending_and_delayed_acknowledgments(engine, app):
|
||
|
|
engine.clock = lambda: FROZEN - timedelta(seconds=1)
|
||
|
|
approve(engine)
|
||
|
|
engine.clock = lambda: FROZEN
|
||
|
|
current = approve(engine)
|
||
|
|
engine.clock = lambda: FROZEN + timedelta(seconds=1)
|
||
|
|
engine.consume(current.id, current.binding_digest)
|
||
|
|
status, before = call(app, "GET", "/v1/cadence", query_string=query())
|
||
|
|
assert status == 200
|
||
|
|
assert before["counts"]["issuance"] == {
|
||
|
|
"committed": 1, "drained": 0, "pending": 1, "acknowledged_in_window": 0,
|
||
|
|
}
|
||
|
|
assert before["counts"]["use"]["committed"] == 0 # until is exclusive
|
||
|
|
engine.clock = lambda: FROZEN + timedelta(seconds=2)
|
||
|
|
engine.drain(lambda _: None)
|
||
|
|
_, after = call(app, "GET", "/v1/cadence", query_string=query())
|
||
|
|
assert after["counts"]["issuance"] == {
|
||
|
|
"committed": 1, "drained": 1, "pending": 0, "acknowledged_in_window": 0,
|
||
|
|
}
|
||
|
|
_, ack = call(app, "GET", "/v1/cadence", query_string=query(
|
||
|
|
FROZEN + timedelta(seconds=2), FROZEN + timedelta(seconds=3)))
|
||
|
|
assert ack["counts"]["issuance"]["acknowledged_in_window"] == 2
|
||
|
|
assert ack["counts"]["use"]["acknowledged_in_window"] == 1
|
||
|
|
assert all(c["committed"] == 0 for c in ack["counts"].values())
|
||
|
|
assert set(after["counts"]) == set(EVENT_CLASSES)
|
||
|
|
assert after["source"] == "approval-engine" and after["tenant"] == engine.tenant
|
||
|
|
assert after["window_basis"] == "created_at"
|
||
|
|
assert "acceptance-time" in after["means"]
|
||
|
|
assert "approvers" not in str(after) and current.id not in str(after)
|
||
|
|
|
||
|
|
|
||
|
|
def test_timezone_and_subsecond_bounds_are_preserved(app, engine):
|
||
|
|
approve(engine)
|
||
|
|
offset = FROZEN.astimezone(timezone(timedelta(hours=2)))
|
||
|
|
_, result = call(app, "GET", "/v1/cadence", query_string=query(offset, offset + timedelta(seconds=1)))
|
||
|
|
assert result["counts"]["issuance"]["committed"] == 1
|
||
|
|
_, result = call(app, "GET", "/v1/cadence", query_string=query(
|
||
|
|
FROZEN + timedelta(microseconds=1), FROZEN + timedelta(seconds=1)))
|
||
|
|
assert result["counts"]["issuance"]["committed"] == 0
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize("qs", [
|
||
|
|
"since=bad&until=bad", "since=2026-10-04&until=2026-10-05",
|
||
|
|
query(FROZEN, FROZEN), query(FROZEN + timedelta(seconds=1), FROZEN),
|
||
|
|
"since=x", "until=x", "since=&until=x", "since=x&since=y&until=z",
|
||
|
|
query() + "&tenant=other", "window=all",
|
||
|
|
])
|
||
|
|
def test_bad_or_unbounded_queries_are_refused(app, qs):
|
||
|
|
assert call(app, "GET", "/v1/cadence", query_string=qs)[0] == 422
|
||
|
|
|
||
|
|
|
||
|
|
def test_window_counts_preserve_lifetime_endpoint_and_authentication(app, engine):
|
||
|
|
approve(engine)
|
||
|
|
_, counts = call(app, "GET", "/v1/cadence")
|
||
|
|
assert counts["issuance"] == 1 and counts["form"] == "heartbeat-or-reconciliation"
|
||
|
|
assert call(app, "GET", "/v1/cadence", authorization=None, query_string=query())[0] == 401
|
||
|
|
identity = app.authenticator.identities["test-token"]
|
||
|
|
for altered in (replace(identity, scopes=frozenset({"approval:read"})),
|
||
|
|
replace(identity, tenant="tenant:other")):
|
||
|
|
other = App(engine, StaticTokenAuthenticator({"test-token": altered}))
|
||
|
|
assert call(other, "GET", "/v1/cadence", query_string=query())[0] == 403
|
||
|
|
|
||
|
|
|
||
|
|
def test_window_counts_fail_closed_on_store_failure(app, engine, monkeypatch):
|
||
|
|
def unavailable():
|
||
|
|
raise StoreUnavailable("unavailable")
|
||
|
|
monkeypatch.setattr(engine, "_conn", unavailable)
|
||
|
|
assert call(app, "GET", "/v1/cadence", query_string=query())[0] == 503
|