feat: add draft data, gap analysis report, and workspace config
This commit is contained in:
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)
|
||||
Reference in New Issue
Block a user