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