approval-engine/tests/test_reissue.py

107 lines
4.1 KiB
Python
Raw Normal View History

"""Reissuing an expired approval: supersede() extended to link a fresh
successor to an already-`expired` approval, given a new validity window.
`expired` stays a terminal state (SCOPE.md's closed state machine is
unchanged) -- this never reopens the original in place. It only lets a new,
linked approval be created for the same binding, and marks the original
`superseded` (rather than leaving it a dead-end `expired`) so the link is
visible without deleting or overwriting the original record.
"""
import pytest
from approval_engine.errors import Conflict, Unprocessable
from tests.conftest import FROZEN, approve, binding, validity
def expired_validity():
return {
"not_before": "2026-08-29T09:00:00+00:00",
"expires_at": "2026-08-29T10:00:00+00:00", # before FROZEN (12:00)
}
def fresh_validity():
return {
"not_before": "2026-08-29T13:00:00+00:00",
"expires_at": "2026-08-29T18:00:00+00:00",
}
def test_reissue_links_successor_and_retires_original(engine):
obj = engine.create(binding(), expired_validity())
assert engine.get(obj.id).status == "expired"
result = engine.supersede(obj.id, validity=fresh_validity())
successor_id = result["successor_id"]
original = engine.get(obj.id)
assert original.status == "superseded" # not left as a bare dead-end "expired"
assert original.superseded_by == successor_id # link is documented, not hidden
successor = engine.get(successor_id)
assert successor.status == "requested"
assert successor.not_before == fresh_validity()["not_before"]
assert successor.expires_at == fresh_validity()["expires_at"]
# Same intent carried over: actor/principal/action/purpose/target unchanged.
assert successor.actor == original.actor
assert successor.principal == original.principal
assert successor.action == original.action
assert successor.purpose == original.purpose
assert successor.target == original.target
assert successor.binding_digest == original.binding_digest
def test_reissue_without_validity_conflicts(engine):
obj = engine.create(binding(), expired_validity())
assert engine.get(obj.id).status == "expired"
with pytest.raises(Conflict):
engine.supersede(obj.id)
def test_reissue_requires_expires_after_not_before(engine):
obj = engine.create(binding(), expired_validity())
engine.get(obj.id)
with pytest.raises(Unprocessable):
engine.supersede(obj.id, validity={
"not_before": "2026-08-29T18:00:00+00:00",
"expires_at": "2026-08-29T13:00:00+00:00",
})
# A failed reissue must not have mutated the original.
assert engine.get(obj.id).status == "expired"
def test_reissue_requires_expires_at(engine):
obj = engine.create(binding(), expired_validity())
engine.get(obj.id)
with pytest.raises(Unprocessable):
engine.supersede(obj.id, validity={"not_before": "2026-08-29T13:00:00+00:00"})
@pytest.mark.parametrize("terminal_verb", ["revoke"])
def test_cannot_reissue_other_terminal_states_even_with_validity(engine, terminal_verb):
obj = approve(engine)
getattr(engine, terminal_verb)(obj.id)
assert engine.get(obj.id).status == "revoked"
with pytest.raises(Conflict):
engine.supersede(obj.id, validity=fresh_validity())
def test_cannot_reissue_consumed_even_with_validity(engine):
obj = approve(engine)
engine.consume(obj.id, "sha256:" + "ab" * 32)
assert engine.get(obj.id).status == "consumed"
with pytest.raises(Conflict):
engine.supersede(obj.id, validity=fresh_validity())
def test_ordinary_supersession_of_an_active_approval_still_copies_the_window(engine):
"""Regression: reissue's validity override must not change the
unrelated, already-working case of superseding a still-active approval
(e.g. correcting a binding) -- that keeps inheriting the same window."""
obj = approve(engine)
result = engine.supersede(obj.id)
successor = engine.get(result["successor_id"])
original_window = validity()
assert successor.not_before == original_window["not_before"]
assert successor.expires_at == original_window["expires_at"]