from datetime import datetime, timezone, timedelta from types import SimpleNamespace import pytest from railiance_clock.core import Window, unix_ns from secrets_engine.application_time import read_window, validity_bounds, recheck_binding from secrets_engine.approval_claim import validate_approval_claim from secrets_engine.authorization import validate_decision_envelope from secrets_engine.approval_consume import ConsumeBinding from secrets_engine.errors import DecisionError from tests.test_approval_claim import _claim, _binding, APPROVAL_ID, claim_binding_digest from tests.test_decision_replay import pair def window(lower, upper): return Window(unix_ns(lower),unix_ns(upper),'test-authority','epoch','test-policy','sample') def test_claim_uses_interval_not_workstation_time(): claim=_claim();start=datetime(2030,1,1,tzinfo=timezone.utc) claim['validity']={'not_before':start.isoformat(),'expires_at':(start+timedelta(seconds=30)).isoformat()} claim['freshness']['not_after']=(start+timedelta(seconds=10)).isoformat() kw=dict(approval_id=APPROVAL_ID,expected_binding_digest=claim_binding_digest(**_binding())) validate_approval_claim(claim,time_window=window(start+timedelta(seconds=1),start+timedelta(seconds=2)),**kw) with pytest.raises(DecisionError):validate_approval_claim(claim,time_window=window(start-timedelta(seconds=1),start+timedelta(seconds=1)),**kw) with pytest.raises(DecisionError):validate_approval_claim(claim,time_window=window(start+timedelta(seconds=9),start+timedelta(seconds=10)),**kw) def test_producer_decision_interval_boundary(): request,decision=pair('rotate') start=datetime.fromisoformat(decision['lifetime']['not_before'].replace('Z','+00:00')) end=datetime.fromisoformat(decision['lifetime']['expires_at'].replace('Z','+00:00')) kw=dict(accepted_policy_packages={'secrets-engine.catalog-lane.lifecycle'},accepted_policy_versions={'v2'}) validate_decision_envelope(decision,request,time_window=window(start,start+timedelta(seconds=1)),**kw) with pytest.raises(DecisionError):validate_decision_envelope(decision,request,time_window=window(start-timedelta(microseconds=1),start+timedelta(seconds=1)),**kw) with pytest.raises(DecisionError):validate_decision_envelope(decision,request,time_window=window(end-timedelta(seconds=1),end),**kw) def test_configured_missing_trust_never_falls_back(tmp_path): with pytest.raises(DecisionError):read_window(SimpleNamespace(clock_trust_file=tmp_path/'missing.json')) assert read_window(SimpleNamespace(clock_trust_file=None)) is None def test_no_mixing_shifted_now_and_interval(): now=datetime.now(timezone.utc) with pytest.raises(DecisionError):validity_bounds(now,window(now,now)) def test_consume_rechecks_expiry(monkeypatch): now=datetime.now(timezone.utc) cfg=SimpleNamespace(clock_trust_file='configured') binding=ConsumeBinding('approval','sha256:'+'a'*64,decision_expires_at=now.isoformat()) monkeypatch.setattr('secrets_engine.application_time.read_window',lambda cfg:window(now,now+timedelta(seconds=1))) with pytest.raises(DecisionError):recheck_binding(cfg,binding)