"""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"]