feat: add draft data, gap analysis report, and workspace config
This commit is contained in:
145
workspace/act/tests/test_crypto.py
Normal file
145
workspace/act/tests/test_crypto.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""Tests for act.crypto module."""
|
||||
|
||||
import pytest
|
||||
|
||||
from act.crypto import (
|
||||
ACTKeyResolver,
|
||||
KeyRegistry,
|
||||
X509TrustStore,
|
||||
b64url_sha256,
|
||||
compute_sha256,
|
||||
did_key_from_ed25519,
|
||||
generate_ed25519_keypair,
|
||||
generate_p256_keypair,
|
||||
resolve_did_key,
|
||||
sign,
|
||||
verify,
|
||||
)
|
||||
from act.errors import ACTKeyResolutionError, ACTSignatureError
|
||||
|
||||
|
||||
class TestEd25519:
|
||||
def test_generate_keypair(self):
|
||||
priv, pub = generate_ed25519_keypair()
|
||||
assert priv is not None
|
||||
assert pub is not None
|
||||
|
||||
def test_sign_verify(self):
|
||||
priv, pub = generate_ed25519_keypair()
|
||||
data = b"test data"
|
||||
sig = sign(priv, data)
|
||||
verify(pub, sig, data)
|
||||
|
||||
def test_verify_wrong_data(self):
|
||||
priv, pub = generate_ed25519_keypair()
|
||||
sig = sign(priv, b"correct data")
|
||||
with pytest.raises(ACTSignatureError):
|
||||
verify(pub, sig, b"wrong data")
|
||||
|
||||
def test_verify_wrong_key(self):
|
||||
priv1, pub1 = generate_ed25519_keypair()
|
||||
_, pub2 = generate_ed25519_keypair()
|
||||
sig = sign(priv1, b"data")
|
||||
with pytest.raises(ACTSignatureError):
|
||||
verify(pub2, sig, b"data")
|
||||
|
||||
|
||||
class TestP256:
|
||||
def test_generate_keypair(self):
|
||||
priv, pub = generate_p256_keypair()
|
||||
assert priv is not None
|
||||
assert pub is not None
|
||||
|
||||
def test_sign_verify(self):
|
||||
priv, pub = generate_p256_keypair()
|
||||
data = b"test data for p256"
|
||||
sig = sign(priv, data)
|
||||
assert len(sig) == 64 # r||s, 32 bytes each
|
||||
verify(pub, sig, data)
|
||||
|
||||
def test_verify_wrong_data(self):
|
||||
priv, pub = generate_p256_keypair()
|
||||
sig = sign(priv, b"correct")
|
||||
with pytest.raises(ACTSignatureError):
|
||||
verify(pub, sig, b"wrong")
|
||||
|
||||
|
||||
class TestSHA256:
|
||||
def test_compute(self):
|
||||
h = compute_sha256(b"hello")
|
||||
assert len(h) == 32
|
||||
|
||||
def test_b64url(self):
|
||||
result = b64url_sha256(b"hello world")
|
||||
assert "=" not in result
|
||||
assert isinstance(result, str)
|
||||
|
||||
|
||||
class TestKeyRegistry:
|
||||
def test_register_and_get(self):
|
||||
reg = KeyRegistry()
|
||||
_, pub = generate_ed25519_keypair()
|
||||
reg.register("key-1", pub)
|
||||
assert reg.get("key-1") is pub
|
||||
assert "key-1" in reg
|
||||
assert len(reg) == 1
|
||||
|
||||
def test_missing_key(self):
|
||||
reg = KeyRegistry()
|
||||
assert reg.get("missing") is None
|
||||
assert "missing" not in reg
|
||||
|
||||
|
||||
class TestDIDKey:
|
||||
def test_ed25519_roundtrip(self):
|
||||
_, pub = generate_ed25519_keypair()
|
||||
did = did_key_from_ed25519(pub)
|
||||
assert did.startswith("did:key:z6Mk")
|
||||
resolved = resolve_did_key(did)
|
||||
# Verify same key by signing/verifying
|
||||
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
||||
original_bytes = pub.public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
resolved_bytes = resolved.public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
assert original_bytes == resolved_bytes
|
||||
|
||||
def test_invalid_prefix(self):
|
||||
with pytest.raises(ACTKeyResolutionError):
|
||||
resolve_did_key("did:web:example.com")
|
||||
|
||||
def test_with_fragment(self):
|
||||
_, pub = generate_ed25519_keypair()
|
||||
did = did_key_from_ed25519(pub)
|
||||
did_with_fragment = f"{did}#{did.split(':')[2]}"
|
||||
resolved = resolve_did_key(did_with_fragment)
|
||||
assert resolved is not None
|
||||
|
||||
|
||||
class TestACTKeyResolver:
|
||||
def test_tier1_resolution(self):
|
||||
reg = KeyRegistry()
|
||||
_, pub = generate_ed25519_keypair()
|
||||
reg.register("my-key", pub)
|
||||
resolver = ACTKeyResolver(registry=reg)
|
||||
assert resolver.resolve("my-key") is pub
|
||||
|
||||
def test_tier3_did_key(self):
|
||||
_, pub = generate_ed25519_keypair()
|
||||
did = did_key_from_ed25519(pub)
|
||||
resolver = ACTKeyResolver()
|
||||
resolved = resolver.resolve(did)
|
||||
assert resolved is not None
|
||||
|
||||
def test_unresolvable(self):
|
||||
resolver = ACTKeyResolver()
|
||||
with pytest.raises(ACTKeyResolutionError):
|
||||
resolver.resolve("unknown-kid")
|
||||
|
||||
def test_did_web_resolver_callback(self):
|
||||
_, pub = generate_ed25519_keypair()
|
||||
def resolver_cb(did: str):
|
||||
if did == "did:web:example.com":
|
||||
return pub
|
||||
return None
|
||||
resolver = ACTKeyResolver(did_web_resolver=resolver_cb)
|
||||
result = resolver.resolve("did:web:example.com")
|
||||
assert result is pub
|
||||
103
workspace/act/tests/test_dag.py
Normal file
103
workspace/act/tests/test_dag.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""Tests for act.dag module."""
|
||||
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from act.dag import validate_dag
|
||||
from act.errors import ACTCapabilityError, ACTDAGError
|
||||
from act.ledger import ACTLedger
|
||||
from act.token import ACTRecord, Capability, TaskClaim
|
||||
|
||||
|
||||
def make_record(jti, par=None, exec_act="do.thing", exec_ts=None, cap=None):
|
||||
"""Helper to create a minimal ACTRecord."""
|
||||
return ACTRecord(
|
||||
alg="EdDSA", kid="k", iss="a", sub="b", aud="b",
|
||||
iat=1772064000, exp=1772064900,
|
||||
jti=jti,
|
||||
task=TaskClaim(purpose="t"),
|
||||
cap=cap or [Capability(action="do.thing")],
|
||||
exec_act=exec_act,
|
||||
par=par or [],
|
||||
exec_ts=exec_ts or 1772064100,
|
||||
status="completed",
|
||||
)
|
||||
|
||||
|
||||
class TestDAGValidation:
|
||||
def test_root_task(self):
|
||||
ledger = ACTLedger()
|
||||
r = make_record("root-1")
|
||||
validate_dag(r, ledger)
|
||||
|
||||
def test_child_with_parent(self):
|
||||
ledger = ACTLedger()
|
||||
parent = make_record("parent-1", exec_ts=1772064050)
|
||||
ledger.append(parent)
|
||||
child = make_record("child-1", par=["parent-1"], exec_ts=1772064100)
|
||||
validate_dag(child, ledger)
|
||||
|
||||
def test_fan_in(self):
|
||||
ledger = ACTLedger()
|
||||
p1 = make_record("p1", exec_ts=1772064050)
|
||||
p2 = make_record("p2", exec_ts=1772064060)
|
||||
ledger.append(p1)
|
||||
ledger.append(p2)
|
||||
child = make_record("child", par=["p1", "p2"], exec_ts=1772064100)
|
||||
validate_dag(child, ledger)
|
||||
|
||||
def test_duplicate_jti(self):
|
||||
ledger = ACTLedger()
|
||||
r = make_record("dup-1")
|
||||
ledger.append(r)
|
||||
r2 = make_record("dup-1")
|
||||
with pytest.raises(ACTDAGError, match="Duplicate"):
|
||||
validate_dag(r2, ledger)
|
||||
|
||||
def test_missing_parent(self):
|
||||
ledger = ACTLedger()
|
||||
r = make_record("orphan", par=["nonexistent"])
|
||||
with pytest.raises(ACTDAGError, match="not found"):
|
||||
validate_dag(r, ledger)
|
||||
|
||||
def test_self_cycle(self):
|
||||
ledger = ACTLedger()
|
||||
r = make_record("cycle", par=["cycle"])
|
||||
with pytest.raises(ACTDAGError, match="cycle"):
|
||||
validate_dag(r, ledger)
|
||||
|
||||
def test_indirect_cycle(self):
|
||||
ledger = ACTLedger()
|
||||
# a -> b -> a would be a cycle
|
||||
a = make_record("a", par=["b"], exec_ts=1772064100)
|
||||
b = make_record("b", par=["a"], exec_ts=1772064100)
|
||||
ledger.append(b)
|
||||
# When validating a, following par leads to b,
|
||||
# which has par=["a"] — cycle!
|
||||
with pytest.raises(ACTDAGError, match="cycle"):
|
||||
validate_dag(a, ledger)
|
||||
|
||||
def test_temporal_ordering_violation(self):
|
||||
ledger = ACTLedger()
|
||||
parent = make_record("parent", exec_ts=1772064200)
|
||||
ledger.append(parent)
|
||||
# Child's exec_ts is way before parent
|
||||
child = make_record("child", par=["parent"], exec_ts=1772064100)
|
||||
with pytest.raises(ACTDAGError, match="Temporal"):
|
||||
validate_dag(child, ledger)
|
||||
|
||||
def test_temporal_within_tolerance(self):
|
||||
ledger = ACTLedger()
|
||||
parent = make_record("parent", exec_ts=1772064120)
|
||||
ledger.append(parent)
|
||||
# Child exec_ts is slightly before parent but within 30s tolerance
|
||||
child = make_record("child", par=["parent"], exec_ts=1772064100)
|
||||
validate_dag(child, ledger)
|
||||
|
||||
def test_bad_exec_act(self):
|
||||
ledger = ACTLedger()
|
||||
r = make_record("bad", exec_act="not.authorized",
|
||||
cap=[Capability(action="do.thing")])
|
||||
with pytest.raises(ACTCapabilityError):
|
||||
validate_dag(r, ledger)
|
||||
229
workspace/act/tests/test_delegation.py
Normal file
229
workspace/act/tests/test_delegation.py
Normal file
@@ -0,0 +1,229 @@
|
||||
"""Tests for act.delegation module."""
|
||||
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from act.crypto import generate_ed25519_keypair, sign, verify, compute_sha256
|
||||
from act.delegation import (
|
||||
create_delegated_mandate,
|
||||
verify_capability_subset,
|
||||
verify_delegation_chain,
|
||||
)
|
||||
from act.errors import (
|
||||
ACTDelegationError,
|
||||
ACTPrivilegeEscalationError,
|
||||
)
|
||||
from act.token import (
|
||||
ACTMandate,
|
||||
Capability,
|
||||
Delegation,
|
||||
DelegationEntry,
|
||||
TaskClaim,
|
||||
_b64url_decode,
|
||||
encode_jws,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def parent_setup():
|
||||
iss_priv, iss_pub = generate_ed25519_keypair()
|
||||
mandate = ACTMandate(
|
||||
alg="EdDSA", kid="iss-key",
|
||||
iss="agent-a", sub="agent-b", aud="agent-b",
|
||||
iat=1772064000, exp=1772064900,
|
||||
jti="parent-jti-1",
|
||||
task=TaskClaim(purpose="parent_task"),
|
||||
cap=[
|
||||
Capability(action="read.data", constraints={"max_records": 10}),
|
||||
Capability(action="write.result"),
|
||||
],
|
||||
delegation=Delegation(depth=0, max_depth=3, chain=[]),
|
||||
)
|
||||
sig = sign(iss_priv, mandate.signing_input())
|
||||
compact = encode_jws(mandate, sig)
|
||||
return mandate, compact, iss_priv, iss_pub
|
||||
|
||||
|
||||
class TestCreateDelegatedMandate:
|
||||
def test_basic_delegation(self, parent_setup):
|
||||
mandate, compact, priv, _ = parent_setup
|
||||
delegated, _ = create_delegated_mandate(
|
||||
parent_mandate=mandate, parent_compact=compact,
|
||||
delegator_private_key=priv,
|
||||
sub="agent-c", kid="key-b", iss="agent-a", aud="agent-c",
|
||||
iat=1772064010, exp=1772064600,
|
||||
jti="child-jti-1",
|
||||
cap=[Capability(action="read.data", constraints={"max_records": 5})],
|
||||
task=TaskClaim(purpose="child_task"),
|
||||
)
|
||||
assert delegated.delegation.depth == 1
|
||||
assert len(delegated.delegation.chain) == 1
|
||||
assert delegated.delegation.chain[0].delegator == "agent-a"
|
||||
|
||||
def test_depth_exceeded(self, parent_setup):
|
||||
mandate, compact, priv, _ = parent_setup
|
||||
# Set parent to max depth
|
||||
mandate.delegation = Delegation(depth=3, max_depth=3, chain=[
|
||||
DelegationEntry(delegator="x", jti="j", sig="s")
|
||||
for _ in range(3)
|
||||
])
|
||||
with pytest.raises(ACTDelegationError, match="exceeds max_depth"):
|
||||
create_delegated_mandate(
|
||||
parent_mandate=mandate, parent_compact=compact,
|
||||
delegator_private_key=priv,
|
||||
sub="c", kid="k", iss="a", aud="c",
|
||||
iat=1, exp=2, jti="j",
|
||||
cap=[Capability(action="read.data", constraints={"max_records": 5})],
|
||||
task=TaskClaim(purpose="t"),
|
||||
)
|
||||
|
||||
def test_no_del_claim(self):
|
||||
priv, _ = generate_ed25519_keypair()
|
||||
mandate = ACTMandate(
|
||||
alg="EdDSA", kid="k", iss="a", sub="b", aud="b",
|
||||
iat=1, exp=2,
|
||||
task=TaskClaim(purpose="t"),
|
||||
cap=[Capability(action="x.y")],
|
||||
delegation=None, # no del claim
|
||||
)
|
||||
with pytest.raises(ACTDelegationError, match="not permitted"):
|
||||
create_delegated_mandate(
|
||||
parent_mandate=mandate, parent_compact="compact",
|
||||
delegator_private_key=priv,
|
||||
sub="c", kid="k", iss="a", aud="c",
|
||||
iat=1, exp=2, jti="j",
|
||||
cap=[Capability(action="x.y")],
|
||||
task=TaskClaim(purpose="t"),
|
||||
)
|
||||
|
||||
def test_max_depth_reduction(self, parent_setup):
|
||||
mandate, compact, priv, _ = parent_setup
|
||||
delegated, _ = create_delegated_mandate(
|
||||
parent_mandate=mandate, parent_compact=compact,
|
||||
delegator_private_key=priv,
|
||||
sub="c", kid="k", iss="a", aud="c",
|
||||
iat=1, exp=2, jti="j",
|
||||
cap=[Capability(action="read.data", constraints={"max_records": 5})],
|
||||
task=TaskClaim(purpose="t"),
|
||||
max_depth=2,
|
||||
)
|
||||
assert delegated.delegation.max_depth == 2
|
||||
|
||||
def test_max_depth_escalation(self, parent_setup):
|
||||
mandate, compact, priv, _ = parent_setup
|
||||
with pytest.raises(ACTDelegationError, match="exceeds parent max_depth"):
|
||||
create_delegated_mandate(
|
||||
parent_mandate=mandate, parent_compact=compact,
|
||||
delegator_private_key=priv,
|
||||
sub="c", kid="k", iss="a", aud="c",
|
||||
iat=1, exp=2, jti="j",
|
||||
cap=[Capability(action="read.data", constraints={"max_records": 5})],
|
||||
task=TaskClaim(purpose="t"),
|
||||
max_depth=10,
|
||||
)
|
||||
|
||||
|
||||
class TestCapabilitySubset:
|
||||
def test_valid_subset(self):
|
||||
parent = [Capability(action="read.data", constraints={"max_records": 10})]
|
||||
child = [Capability(action="read.data", constraints={"max_records": 5})]
|
||||
verify_capability_subset(parent, child)
|
||||
|
||||
def test_extra_action(self):
|
||||
parent = [Capability(action="read.data")]
|
||||
child = [Capability(action="delete.data")]
|
||||
with pytest.raises(ACTPrivilegeEscalationError):
|
||||
verify_capability_subset(parent, child)
|
||||
|
||||
def test_numeric_escalation(self):
|
||||
parent = [Capability(action="read.data", constraints={"max_records": 10})]
|
||||
child = [Capability(action="read.data", constraints={"max_records": 100})]
|
||||
with pytest.raises(ACTPrivilegeEscalationError):
|
||||
verify_capability_subset(parent, child)
|
||||
|
||||
def test_sensitivity_escalation(self):
|
||||
parent = [Capability(action="read.data",
|
||||
constraints={"data_sensitivity": "confidential"})]
|
||||
child = [Capability(action="read.data",
|
||||
constraints={"data_sensitivity": "internal"})]
|
||||
with pytest.raises(ACTPrivilegeEscalationError):
|
||||
verify_capability_subset(parent, child)
|
||||
|
||||
def test_sensitivity_more_restrictive(self):
|
||||
parent = [Capability(action="read.data",
|
||||
constraints={"data_sensitivity": "internal"})]
|
||||
child = [Capability(action="read.data",
|
||||
constraints={"data_sensitivity": "restricted"})]
|
||||
verify_capability_subset(parent, child) # should pass
|
||||
|
||||
def test_missing_constraint(self):
|
||||
parent = [Capability(action="read.data",
|
||||
constraints={"max_records": 10, "scope": "local"})]
|
||||
child = [Capability(action="read.data",
|
||||
constraints={"max_records": 5})]
|
||||
with pytest.raises(ACTPrivilegeEscalationError, match="missing"):
|
||||
verify_capability_subset(parent, child)
|
||||
|
||||
def test_domain_specific_identical(self):
|
||||
parent = [Capability(action="read.data",
|
||||
constraints={"custom": "value_a"})]
|
||||
child = [Capability(action="read.data",
|
||||
constraints={"custom": "value_a"})]
|
||||
verify_capability_subset(parent, child)
|
||||
|
||||
def test_domain_specific_different(self):
|
||||
parent = [Capability(action="read.data",
|
||||
constraints={"custom": "value_a"})]
|
||||
child = [Capability(action="read.data",
|
||||
constraints={"custom": "value_b"})]
|
||||
with pytest.raises(ACTPrivilegeEscalationError, match="identical"):
|
||||
verify_capability_subset(parent, child)
|
||||
|
||||
|
||||
class TestVerifyDelegationChain:
|
||||
def test_chain_sig_verification(self, parent_setup):
|
||||
mandate, compact, priv, pub = parent_setup
|
||||
delegated, _ = create_delegated_mandate(
|
||||
parent_mandate=mandate, parent_compact=compact,
|
||||
delegator_private_key=priv,
|
||||
sub="c", kid="k", iss="agent-a", aud="c",
|
||||
iat=1, exp=2, jti="j",
|
||||
cap=[Capability(action="read.data", constraints={"max_records": 5})],
|
||||
task=TaskClaim(purpose="t"),
|
||||
)
|
||||
|
||||
# Verify the chain
|
||||
def resolve_key(delegator_id):
|
||||
return pub
|
||||
|
||||
def resolve_compact(jti):
|
||||
if jti == "parent-jti-1":
|
||||
return compact
|
||||
return None
|
||||
|
||||
verify_delegation_chain(delegated, resolve_key, resolve_compact)
|
||||
|
||||
def test_no_delegation(self):
|
||||
mandate = ACTMandate(
|
||||
alg="EdDSA", kid="k", iss="a", sub="b", aud="b",
|
||||
iat=1, exp=2,
|
||||
task=TaskClaim(purpose="t"),
|
||||
cap=[Capability(action="x.y")],
|
||||
)
|
||||
verify_delegation_chain(mandate, lambda x: None) # no-op
|
||||
|
||||
def test_depth_exceeds_max(self):
|
||||
mandate = ACTMandate(
|
||||
alg="EdDSA", kid="k", iss="a", sub="b", aud="b",
|
||||
iat=1, exp=2,
|
||||
task=TaskClaim(purpose="t"),
|
||||
cap=[Capability(action="x.y")],
|
||||
delegation=Delegation(depth=5, max_depth=3, chain=[
|
||||
DelegationEntry(delegator="x", jti="j", sig="s")
|
||||
for _ in range(5)
|
||||
]),
|
||||
)
|
||||
with pytest.raises(ACTDelegationError, match="exceeds"):
|
||||
verify_delegation_chain(mandate, lambda x: None)
|
||||
84
workspace/act/tests/test_ledger.py
Normal file
84
workspace/act/tests/test_ledger.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""Tests for act.ledger module."""
|
||||
|
||||
import pytest
|
||||
|
||||
from act.errors import ACTLedgerImmutabilityError
|
||||
from act.ledger import ACTLedger
|
||||
from act.token import ACTRecord, Capability, TaskClaim
|
||||
|
||||
|
||||
def make_record(jti, wid=None):
|
||||
return ACTRecord(
|
||||
alg="EdDSA", kid="k", iss="a", sub="b", aud="b",
|
||||
iat=1772064000, exp=1772064900,
|
||||
jti=jti, wid=wid,
|
||||
task=TaskClaim(purpose="t"),
|
||||
cap=[Capability(action="do.thing")],
|
||||
exec_act="do.thing", par=[], exec_ts=1772064100,
|
||||
status="completed",
|
||||
)
|
||||
|
||||
|
||||
class TestACTLedger:
|
||||
def test_append_and_get(self):
|
||||
ledger = ACTLedger()
|
||||
r = make_record("jti-1")
|
||||
seq = ledger.append(r)
|
||||
assert seq == 0
|
||||
assert ledger.get("jti-1") is r
|
||||
|
||||
def test_sequential_ordering(self):
|
||||
ledger = ACTLedger()
|
||||
for i in range(5):
|
||||
seq = ledger.append(make_record(f"jti-{i}"))
|
||||
assert seq == i
|
||||
|
||||
def test_duplicate_rejected(self):
|
||||
ledger = ACTLedger()
|
||||
ledger.append(make_record("jti-1"))
|
||||
with pytest.raises(ACTLedgerImmutabilityError):
|
||||
ledger.append(make_record("jti-1"))
|
||||
|
||||
def test_get_missing(self):
|
||||
ledger = ACTLedger()
|
||||
assert ledger.get("missing") is None
|
||||
|
||||
def test_list_all(self):
|
||||
ledger = ACTLedger()
|
||||
ledger.append(make_record("a"))
|
||||
ledger.append(make_record("b"))
|
||||
records = ledger.list()
|
||||
assert len(records) == 2
|
||||
|
||||
def test_list_by_wid(self):
|
||||
ledger = ACTLedger()
|
||||
ledger.append(make_record("a", wid="w1"))
|
||||
ledger.append(make_record("b", wid="w2"))
|
||||
ledger.append(make_record("c", wid="w1"))
|
||||
assert len(ledger.list("w1")) == 2
|
||||
assert len(ledger.list("w2")) == 1
|
||||
assert len(ledger.list("w3")) == 0
|
||||
|
||||
def test_verify_integrity_empty(self):
|
||||
ledger = ACTLedger()
|
||||
assert ledger.verify_integrity() is True
|
||||
|
||||
def test_verify_integrity_with_records(self):
|
||||
ledger = ACTLedger()
|
||||
for i in range(10):
|
||||
ledger.append(make_record(f"jti-{i}"))
|
||||
assert ledger.verify_integrity() is True
|
||||
|
||||
def test_verify_integrity_tampered(self):
|
||||
ledger = ACTLedger()
|
||||
ledger.append(make_record("jti-1"))
|
||||
ledger.append(make_record("jti-2"))
|
||||
# Tamper with chain hash
|
||||
ledger._chain_hashes[0] = b"\x00" * 32
|
||||
assert ledger.verify_integrity() is False
|
||||
|
||||
def test_len(self):
|
||||
ledger = ACTLedger()
|
||||
assert len(ledger) == 0
|
||||
ledger.append(make_record("a"))
|
||||
assert len(ledger) == 1
|
||||
103
workspace/act/tests/test_lifecycle.py
Normal file
103
workspace/act/tests/test_lifecycle.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""Tests for act.lifecycle module."""
|
||||
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from act.crypto import generate_ed25519_keypair, sign
|
||||
from act.errors import ACTCapabilityError, ACTPhaseError
|
||||
from act.lifecycle import transition_to_record
|
||||
from act.token import (
|
||||
ACTMandate,
|
||||
ACTRecord,
|
||||
Capability,
|
||||
Delegation,
|
||||
ErrorClaim,
|
||||
TaskClaim,
|
||||
decode_jws,
|
||||
encode_jws,
|
||||
)
|
||||
from act.crypto import verify
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def keys():
|
||||
iss_priv, iss_pub = generate_ed25519_keypair()
|
||||
sub_priv, sub_pub = generate_ed25519_keypair()
|
||||
return iss_priv, iss_pub, sub_priv, sub_pub
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mandate(keys):
|
||||
iss_priv, _, _, _ = keys
|
||||
m = ACTMandate(
|
||||
alg="EdDSA", kid="iss-key",
|
||||
iss="agent-a", sub="agent-b", aud="agent-b",
|
||||
iat=1772064000, exp=1772064900,
|
||||
jti=str(uuid.uuid4()),
|
||||
task=TaskClaim(purpose="test"),
|
||||
cap=[Capability(action="read.data"), Capability(action="write.result")],
|
||||
delegation=Delegation(depth=0, max_depth=2, chain=[]),
|
||||
)
|
||||
return m
|
||||
|
||||
|
||||
class TestTransitionToRecord:
|
||||
def test_basic_transition(self, mandate, keys):
|
||||
_, _, sub_priv, sub_pub = keys
|
||||
record, compact = transition_to_record(
|
||||
mandate, sub_kid="sub-key", sub_private_key=sub_priv,
|
||||
exec_act="read.data", par=[], status="completed",
|
||||
)
|
||||
assert isinstance(record, ACTRecord)
|
||||
assert record.exec_act == "read.data"
|
||||
assert record.kid == "sub-key"
|
||||
assert record.iss == mandate.iss # preserved
|
||||
# Verify signature
|
||||
_, _, sig, si = decode_jws(compact)
|
||||
verify(sub_pub, sig, si)
|
||||
|
||||
def test_with_hashes(self, mandate, keys):
|
||||
_, _, sub_priv, _ = keys
|
||||
record, _ = transition_to_record(
|
||||
mandate, sub_kid="k", sub_private_key=sub_priv,
|
||||
exec_act="write.result", par=[], status="completed",
|
||||
inp_hash="abc", out_hash="def",
|
||||
)
|
||||
assert record.inp_hash == "abc"
|
||||
assert record.out_hash == "def"
|
||||
|
||||
def test_with_error(self, mandate, keys):
|
||||
_, _, sub_priv, _ = keys
|
||||
record, _ = transition_to_record(
|
||||
mandate, sub_kid="k", sub_private_key=sub_priv,
|
||||
exec_act="read.data", par=[], status="failed",
|
||||
err=ErrorClaim(code="timeout", detail="request timed out"),
|
||||
)
|
||||
assert record.status == "failed"
|
||||
assert record.err is not None
|
||||
assert record.err.code == "timeout"
|
||||
|
||||
def test_rejects_bad_exec_act(self, mandate, keys):
|
||||
_, _, sub_priv, _ = keys
|
||||
with pytest.raises(ACTCapabilityError):
|
||||
transition_to_record(
|
||||
mandate, sub_kid="k", sub_private_key=sub_priv,
|
||||
exec_act="delete.everything", par=[],
|
||||
)
|
||||
|
||||
def test_preserves_phase1_claims(self, mandate, keys):
|
||||
_, _, sub_priv, _ = keys
|
||||
record, _ = transition_to_record(
|
||||
mandate, sub_kid="k", sub_private_key=sub_priv,
|
||||
exec_act="read.data", par=[], status="completed",
|
||||
)
|
||||
assert record.iss == mandate.iss
|
||||
assert record.sub == mandate.sub
|
||||
assert record.aud == mandate.aud
|
||||
assert record.iat == mandate.iat
|
||||
assert record.exp == mandate.exp
|
||||
assert record.jti == mandate.jti
|
||||
assert record.task == mandate.task
|
||||
assert record.cap == mandate.cap
|
||||
244
workspace/act/tests/test_token.py
Normal file
244
workspace/act/tests/test_token.py
Normal file
@@ -0,0 +1,244 @@
|
||||
"""Tests for act.token module."""
|
||||
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from act.token import (
|
||||
ACTMandate,
|
||||
ACTRecord,
|
||||
Capability,
|
||||
Delegation,
|
||||
DelegationEntry,
|
||||
ErrorClaim,
|
||||
Oversight,
|
||||
TaskClaim,
|
||||
_b64url_decode,
|
||||
_b64url_encode,
|
||||
decode_jws,
|
||||
encode_jws,
|
||||
parse_token,
|
||||
validate_action_name,
|
||||
)
|
||||
from act.errors import ACTPhaseError, ACTValidationError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def base_time():
|
||||
return 1772064000
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mandate(base_time):
|
||||
return ACTMandate(
|
||||
alg="EdDSA",
|
||||
kid="test-key",
|
||||
iss="agent-a",
|
||||
sub="agent-b",
|
||||
aud="agent-b",
|
||||
iat=base_time,
|
||||
exp=base_time + 900,
|
||||
jti=str(uuid.uuid4()),
|
||||
task=TaskClaim(purpose="test_task"),
|
||||
cap=[Capability(action="read.data")],
|
||||
)
|
||||
|
||||
|
||||
class TestBase64url:
|
||||
def test_roundtrip(self):
|
||||
data = b"hello world"
|
||||
assert _b64url_decode(_b64url_encode(data)) == data
|
||||
|
||||
def test_no_padding(self):
|
||||
encoded = _b64url_encode(b"test")
|
||||
assert "=" not in encoded
|
||||
|
||||
|
||||
class TestActionNameValidation:
|
||||
def test_valid_simple(self):
|
||||
validate_action_name("read")
|
||||
|
||||
def test_valid_dotted(self):
|
||||
validate_action_name("read.data")
|
||||
|
||||
def test_valid_with_hyphens(self):
|
||||
validate_action_name("read-write.data_item")
|
||||
|
||||
def test_invalid_starts_with_digit(self):
|
||||
with pytest.raises(ACTValidationError):
|
||||
validate_action_name("1read")
|
||||
|
||||
def test_invalid_empty(self):
|
||||
with pytest.raises(ACTValidationError):
|
||||
validate_action_name("")
|
||||
|
||||
def test_invalid_double_dot(self):
|
||||
with pytest.raises(ACTValidationError):
|
||||
validate_action_name("read..data")
|
||||
|
||||
|
||||
class TestTaskClaim:
|
||||
def test_roundtrip(self):
|
||||
t = TaskClaim(purpose="test", data_sensitivity="restricted")
|
||||
d = t.to_dict()
|
||||
t2 = TaskClaim.from_dict(d)
|
||||
assert t == t2
|
||||
|
||||
def test_missing_purpose(self):
|
||||
with pytest.raises(ACTValidationError):
|
||||
TaskClaim.from_dict({})
|
||||
|
||||
|
||||
class TestCapability:
|
||||
def test_roundtrip(self):
|
||||
c = Capability(action="read.data", constraints={"max": 10})
|
||||
d = c.to_dict()
|
||||
c2 = Capability.from_dict(d)
|
||||
assert c == c2
|
||||
|
||||
def test_validates_action(self):
|
||||
with pytest.raises(ACTValidationError):
|
||||
Capability(action="")
|
||||
|
||||
|
||||
class TestDelegation:
|
||||
def test_roundtrip(self):
|
||||
d = Delegation(
|
||||
depth=1,
|
||||
max_depth=3,
|
||||
chain=[DelegationEntry(delegator="a", jti="j1", sig="sig1")],
|
||||
)
|
||||
as_dict = d.to_dict()
|
||||
d2 = Delegation.from_dict(as_dict)
|
||||
assert d.depth == d2.depth
|
||||
assert len(d2.chain) == 1
|
||||
|
||||
|
||||
class TestACTMandate:
|
||||
def test_validate_success(self, mandate):
|
||||
mandate.validate()
|
||||
|
||||
def test_validate_missing_iss(self, base_time):
|
||||
m = ACTMandate(
|
||||
alg="EdDSA", kid="k", iss="", sub="b", aud="b",
|
||||
iat=base_time, exp=base_time + 900,
|
||||
task=TaskClaim(purpose="t"), cap=[Capability(action="x.y")],
|
||||
)
|
||||
with pytest.raises(ACTValidationError, match="iss"):
|
||||
m.validate()
|
||||
|
||||
def test_validate_forbidden_alg(self, base_time):
|
||||
m = ACTMandate(
|
||||
alg="HS256", kid="k", iss="a", sub="b", aud="b",
|
||||
iat=base_time, exp=base_time + 900,
|
||||
task=TaskClaim(purpose="t"), cap=[Capability(action="x.y")],
|
||||
)
|
||||
with pytest.raises(ACTValidationError):
|
||||
m.validate()
|
||||
|
||||
def test_validate_alg_none(self, base_time):
|
||||
m = ACTMandate(
|
||||
alg="none", kid="k", iss="a", sub="b", aud="b",
|
||||
iat=base_time, exp=base_time + 900,
|
||||
task=TaskClaim(purpose="t"), cap=[Capability(action="x.y")],
|
||||
)
|
||||
with pytest.raises(ACTValidationError):
|
||||
m.validate()
|
||||
|
||||
def test_to_claims_includes_optional(self, base_time):
|
||||
m = ACTMandate(
|
||||
alg="EdDSA", kid="k", iss="a", sub="b", aud="b",
|
||||
iat=base_time, exp=base_time + 900,
|
||||
task=TaskClaim(purpose="t"), cap=[Capability(action="x.y")],
|
||||
wid="w-1",
|
||||
oversight=Oversight(requires_approval_for=["x.y"]),
|
||||
)
|
||||
claims = m.to_claims()
|
||||
assert claims["wid"] == "w-1"
|
||||
assert "oversight" in claims
|
||||
|
||||
def test_is_phase2(self, mandate):
|
||||
assert mandate.is_phase2() is False
|
||||
|
||||
def test_from_claims_rejects_phase2(self):
|
||||
with pytest.raises(ACTPhaseError):
|
||||
ACTMandate.from_claims(
|
||||
{"alg": "EdDSA", "typ": "act+jwt", "kid": "k"},
|
||||
{"exec_act": "x", "iss": "a", "sub": "b", "aud": "b",
|
||||
"iat": 1, "exp": 2, "jti": "j",
|
||||
"task": {"purpose": "t"}, "cap": [{"action": "x"}]},
|
||||
)
|
||||
|
||||
|
||||
class TestACTRecord:
|
||||
def test_from_mandate(self, mandate):
|
||||
r = ACTRecord.from_mandate(
|
||||
mandate, kid="sub-key", exec_act="read.data",
|
||||
par=[], status="completed",
|
||||
)
|
||||
assert r.iss == mandate.iss
|
||||
assert r.exec_act == "read.data"
|
||||
assert r.kid == "sub-key"
|
||||
|
||||
def test_validate_bad_status(self, mandate):
|
||||
r = ACTRecord.from_mandate(
|
||||
mandate, kid="k", exec_act="read.data",
|
||||
par=[], exec_ts=mandate.iat + 100, status="invalid",
|
||||
)
|
||||
with pytest.raises(ACTValidationError, match="status"):
|
||||
r.validate()
|
||||
|
||||
def test_is_phase2(self, mandate):
|
||||
r = ACTRecord.from_mandate(
|
||||
mandate, kid="k", exec_act="read.data",
|
||||
par=[], status="completed",
|
||||
)
|
||||
assert r.is_phase2() is True
|
||||
|
||||
def test_from_claims_rejects_phase1(self):
|
||||
with pytest.raises(ACTPhaseError):
|
||||
ACTRecord.from_claims(
|
||||
{"alg": "EdDSA", "typ": "act+jwt", "kid": "k"},
|
||||
{"iss": "a", "sub": "b", "aud": "b",
|
||||
"iat": 1, "exp": 2, "jti": "j",
|
||||
"task": {"purpose": "t"}, "cap": [{"action": "x"}]},
|
||||
)
|
||||
|
||||
|
||||
class TestJWSSerialization:
|
||||
def test_decode_invalid_parts(self):
|
||||
with pytest.raises(ACTValidationError):
|
||||
decode_jws("only.two")
|
||||
|
||||
def test_decode_invalid_header(self):
|
||||
with pytest.raises(ACTValidationError):
|
||||
decode_jws("!!!.cGF5bG9hZA.c2ln")
|
||||
|
||||
def test_decode_wrong_typ(self):
|
||||
header = _b64url_encode(json.dumps({"alg": "EdDSA", "typ": "jwt", "kid": "k"}).encode())
|
||||
payload = _b64url_encode(json.dumps({"iss": "a"}).encode())
|
||||
sig = _b64url_encode(b"sig")
|
||||
with pytest.raises(ACTValidationError, match="typ"):
|
||||
decode_jws(f"{header}.{payload}.{sig}")
|
||||
|
||||
def test_parse_token_phase1(self, mandate):
|
||||
from act.crypto import generate_ed25519_keypair, sign
|
||||
priv, pub = generate_ed25519_keypair()
|
||||
sig = sign(priv, mandate.signing_input())
|
||||
compact = encode_jws(mandate, sig)
|
||||
parsed = parse_token(compact)
|
||||
assert isinstance(parsed, ACTMandate)
|
||||
|
||||
def test_parse_token_phase2(self, mandate):
|
||||
from act.crypto import generate_ed25519_keypair, sign
|
||||
priv, pub = generate_ed25519_keypair()
|
||||
record = ACTRecord.from_mandate(
|
||||
mandate, kid="k", exec_act="read.data",
|
||||
par=[], status="completed",
|
||||
)
|
||||
sig = sign(priv, record.signing_input())
|
||||
compact = encode_jws(record, sig)
|
||||
parsed = parse_token(compact)
|
||||
assert isinstance(parsed, ACTRecord)
|
||||
35
workspace/act/tests/test_vectors.py
Normal file
35
workspace/act/tests/test_vectors.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Tests for act.vectors module — Appendix B test vectors."""
|
||||
|
||||
import pytest
|
||||
|
||||
from act.vectors import generate_vectors, validate_vectors
|
||||
|
||||
|
||||
class TestVectorGeneration:
|
||||
def test_generates_15_vectors(self):
|
||||
vectors, ctx = generate_vectors()
|
||||
assert len(vectors) == 15
|
||||
|
||||
def test_vector_ids(self):
|
||||
vectors, _ = generate_vectors()
|
||||
ids = [v.id for v in vectors]
|
||||
expected = [f"B.{i}" for i in range(1, 16)]
|
||||
assert ids == expected
|
||||
|
||||
def test_valid_vectors_have_compact(self):
|
||||
vectors, _ = generate_vectors()
|
||||
for v in vectors:
|
||||
if v.valid and v.id != "B.7":
|
||||
assert v.compact, f"{v.id} should have compact"
|
||||
|
||||
def test_invalid_vectors_have_exception(self):
|
||||
vectors, _ = generate_vectors()
|
||||
for v in vectors:
|
||||
if not v.valid:
|
||||
assert v.expected_exception is not None, \
|
||||
f"{v.id} should have expected_exception"
|
||||
|
||||
|
||||
class TestVectorValidation:
|
||||
def test_all_vectors_pass(self):
|
||||
assert validate_vectors() is True
|
||||
191
workspace/act/tests/test_verify.py
Normal file
191
workspace/act/tests/test_verify.py
Normal file
@@ -0,0 +1,191 @@
|
||||
"""Tests for act.verify module."""
|
||||
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from act.crypto import (
|
||||
ACTKeyResolver,
|
||||
KeyRegistry,
|
||||
generate_ed25519_keypair,
|
||||
sign,
|
||||
)
|
||||
from act.errors import (
|
||||
ACTAudienceMismatchError,
|
||||
ACTCapabilityError,
|
||||
ACTExpiredError,
|
||||
ACTPhaseError,
|
||||
ACTSignatureError,
|
||||
ACTValidationError,
|
||||
)
|
||||
from act.ledger import ACTLedger
|
||||
from act.lifecycle import transition_to_record
|
||||
from act.token import (
|
||||
ACTMandate,
|
||||
ACTRecord,
|
||||
Capability,
|
||||
Delegation,
|
||||
TaskClaim,
|
||||
encode_jws,
|
||||
)
|
||||
from act.verify import ACTVerifier
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def setup():
|
||||
iss_priv, iss_pub = generate_ed25519_keypair()
|
||||
sub_priv, sub_pub = generate_ed25519_keypair()
|
||||
registry = KeyRegistry()
|
||||
registry.register("iss-key", iss_pub)
|
||||
registry.register("sub-key", sub_pub)
|
||||
resolver = ACTKeyResolver(registry=registry)
|
||||
base_time = 1772064000
|
||||
return {
|
||||
"iss_priv": iss_priv, "iss_pub": iss_pub,
|
||||
"sub_priv": sub_priv, "sub_pub": sub_pub,
|
||||
"registry": registry, "resolver": resolver,
|
||||
"base_time": base_time,
|
||||
}
|
||||
|
||||
|
||||
def make_mandate(setup, **overrides):
|
||||
bt = setup["base_time"]
|
||||
defaults = dict(
|
||||
alg="EdDSA", kid="iss-key",
|
||||
iss="agent-issuer", sub="agent-subject",
|
||||
aud="agent-subject",
|
||||
iat=bt, exp=bt + 900,
|
||||
jti=str(uuid.uuid4()),
|
||||
task=TaskClaim(purpose="test"),
|
||||
cap=[Capability(action="read.data")],
|
||||
)
|
||||
defaults.update(overrides)
|
||||
return ACTMandate(**defaults)
|
||||
|
||||
|
||||
def sign_mandate(mandate, priv_key):
|
||||
sig = sign(priv_key, mandate.signing_input())
|
||||
return encode_jws(mandate, sig)
|
||||
|
||||
|
||||
class TestVerifyMandate:
|
||||
def test_valid_mandate(self, setup):
|
||||
verifier = ACTVerifier(
|
||||
setup["resolver"],
|
||||
verifier_id="agent-subject",
|
||||
trusted_issuers={"agent-issuer"},
|
||||
)
|
||||
mandate = make_mandate(setup)
|
||||
compact = sign_mandate(mandate, setup["iss_priv"])
|
||||
result = verifier.verify_mandate(compact, now=setup["base_time"] + 100)
|
||||
assert result.iss == "agent-issuer"
|
||||
|
||||
def test_expired(self, setup):
|
||||
verifier = ACTVerifier(setup["resolver"], verifier_id="agent-subject")
|
||||
mandate = make_mandate(setup)
|
||||
compact = sign_mandate(mandate, setup["iss_priv"])
|
||||
with pytest.raises(ACTExpiredError):
|
||||
verifier.verify_mandate(compact, now=setup["base_time"] + 2000)
|
||||
|
||||
def test_wrong_audience(self, setup):
|
||||
verifier = ACTVerifier(
|
||||
setup["resolver"], verifier_id="other-agent",
|
||||
trusted_issuers={"agent-issuer"},
|
||||
)
|
||||
mandate = make_mandate(setup)
|
||||
compact = sign_mandate(mandate, setup["iss_priv"])
|
||||
with pytest.raises(ACTAudienceMismatchError):
|
||||
verifier.verify_mandate(
|
||||
compact, now=setup["base_time"] + 100, check_sub=False,
|
||||
)
|
||||
|
||||
def test_untrusted_issuer(self, setup):
|
||||
verifier = ACTVerifier(
|
||||
setup["resolver"], verifier_id="agent-subject",
|
||||
trusted_issuers={"trusted-only"},
|
||||
)
|
||||
mandate = make_mandate(setup)
|
||||
compact = sign_mandate(mandate, setup["iss_priv"])
|
||||
with pytest.raises(ACTValidationError, match="not trusted"):
|
||||
verifier.verify_mandate(compact, now=setup["base_time"] + 100)
|
||||
|
||||
def test_signature_failure(self, setup):
|
||||
verifier = ACTVerifier(setup["resolver"], verifier_id="agent-subject")
|
||||
mandate = make_mandate(setup)
|
||||
compact = sign_mandate(mandate, setup["iss_priv"])
|
||||
# Tamper with signature
|
||||
parts = compact.split(".")
|
||||
parts[2] = parts[2][:-4] + "XXXX"
|
||||
tampered = ".".join(parts)
|
||||
with pytest.raises(ACTSignatureError):
|
||||
verifier.verify_mandate(tampered, now=setup["base_time"] + 100)
|
||||
|
||||
def test_phase2_as_mandate(self, setup):
|
||||
verifier = ACTVerifier(setup["resolver"])
|
||||
mandate = make_mandate(setup)
|
||||
record, compact = transition_to_record(
|
||||
mandate, sub_kid="sub-key", sub_private_key=setup["sub_priv"],
|
||||
exec_act="read.data", par=[], status="completed",
|
||||
exec_ts=setup["base_time"] + 100,
|
||||
)
|
||||
with pytest.raises(ACTPhaseError):
|
||||
verifier.verify_mandate(compact, now=setup["base_time"] + 100)
|
||||
|
||||
def test_future_iat(self, setup):
|
||||
verifier = ACTVerifier(setup["resolver"], verifier_id="agent-subject")
|
||||
bt = setup["base_time"]
|
||||
mandate = make_mandate(setup, iat=bt + 1000, exp=bt + 2000)
|
||||
compact = sign_mandate(mandate, setup["iss_priv"])
|
||||
with pytest.raises(ACTValidationError, match="future"):
|
||||
verifier.verify_mandate(compact, now=bt)
|
||||
|
||||
|
||||
class TestVerifyRecord:
|
||||
def test_valid_record(self, setup):
|
||||
verifier = ACTVerifier(
|
||||
setup["resolver"],
|
||||
verifier_id="agent-subject",
|
||||
trusted_issuers={"agent-issuer"},
|
||||
)
|
||||
mandate = make_mandate(setup)
|
||||
record, compact = transition_to_record(
|
||||
mandate, sub_kid="sub-key", sub_private_key=setup["sub_priv"],
|
||||
exec_act="read.data", par=[],
|
||||
exec_ts=setup["base_time"] + 100, status="completed",
|
||||
)
|
||||
result = verifier.verify_record(
|
||||
compact, now=setup["base_time"] + 200, check_aud=False,
|
||||
)
|
||||
assert result.exec_act == "read.data"
|
||||
|
||||
def test_wrong_signer(self, setup):
|
||||
verifier = ACTVerifier(setup["resolver"])
|
||||
mandate = make_mandate(setup)
|
||||
record = ACTRecord.from_mandate(
|
||||
mandate, kid="sub-key", exec_act="read.data",
|
||||
par=[], exec_ts=setup["base_time"] + 100, status="completed",
|
||||
)
|
||||
# Sign with iss key instead of sub key
|
||||
sig = sign(setup["iss_priv"], record.signing_input())
|
||||
compact = encode_jws(record, sig)
|
||||
with pytest.raises(ACTSignatureError):
|
||||
verifier.verify_record(compact, now=setup["base_time"] + 200)
|
||||
|
||||
def test_with_dag_validation(self, setup):
|
||||
verifier = ACTVerifier(
|
||||
setup["resolver"], verifier_id="agent-subject",
|
||||
trusted_issuers={"agent-issuer"},
|
||||
)
|
||||
ledger = ACTLedger()
|
||||
mandate = make_mandate(setup)
|
||||
record, compact = transition_to_record(
|
||||
mandate, sub_kid="sub-key", sub_private_key=setup["sub_priv"],
|
||||
exec_act="read.data", par=[],
|
||||
exec_ts=setup["base_time"] + 100, status="completed",
|
||||
)
|
||||
result = verifier.verify_record(
|
||||
compact, store=ledger,
|
||||
now=setup["base_time"] + 200, check_aud=False,
|
||||
)
|
||||
assert result.status == "completed"
|
||||
Reference in New Issue
Block a user