bastion agent 0.1.14
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
"""Unit tests for the bastion agent manifest loader. Run: python3 -m unittest discover ."""
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import yaml
|
||||
|
||||
from .manifest import (
|
||||
ManifestError,
|
||||
load_manifest,
|
||||
signing_digest,
|
||||
signing_payload,
|
||||
validate_document,
|
||||
)
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
EXAMPLE = os.path.join(HERE, "..", "manifests", "home-documents.yaml")
|
||||
|
||||
MINIMAL = {
|
||||
"freeagent": 1,
|
||||
"id": "docs",
|
||||
"owner": {"host": "owner-host"},
|
||||
"scope": {"roots": ["~/Documents"]},
|
||||
"mask": ["read"],
|
||||
"stages": ["walk"],
|
||||
"sweep": "manual",
|
||||
"sink": {"catalog": "local", "vectors": "local", "plaintext": "never"},
|
||||
}
|
||||
|
||||
FULL = {
|
||||
"freeagent": 1,
|
||||
"id": "docs",
|
||||
"owner": {"host": "owner-host"},
|
||||
"scope": {"roots": ["/srv/a", "~/b"], "exclude": ["**/.git/**"]},
|
||||
"mask": ["read", "search", "index", "summarize"],
|
||||
"stages": ["walk", "extract", "sanitize", "chunk", "embed", "catalog", "seal"],
|
||||
"sanitize": {"mode": "sterilize", "verify": True},
|
||||
"embed": {"model": "m", "dim": 1024, "chunk": {"size_tokens": 512, "overlap_tokens": 64}},
|
||||
"summarize": {"model": "s", "per": ["file"]},
|
||||
"sweep": "daily",
|
||||
"sink": {"catalog": "host", "vectors": "local", "plaintext": "never", "seal_to": ["k1"]},
|
||||
"grants": [{"host": "other-host", "ops": ["search", "read_range"], "until": "2026-12-31"}],
|
||||
}
|
||||
|
||||
|
||||
def errors_of(doc, **kw):
|
||||
try:
|
||||
validate_document(doc, **kw)
|
||||
except ManifestError as exc:
|
||||
return exc.errors
|
||||
return []
|
||||
|
||||
|
||||
def with_(base, **changes):
|
||||
doc = copy.deepcopy(base)
|
||||
for k, v in changes.items():
|
||||
if v is None:
|
||||
doc.pop(k, None)
|
||||
else:
|
||||
doc[k] = v
|
||||
return doc
|
||||
|
||||
|
||||
class ValidTests(unittest.TestCase):
|
||||
def test_minimal(self):
|
||||
m = validate_document(MINIMAL)
|
||||
self.assertEqual(m.stages, ("walk",))
|
||||
self.assertEqual(m.scope.max_file_bytes, 256 * 1024 * 1024)
|
||||
self.assertIsNone(m.sanitize)
|
||||
self.assertIsNone(m.embed)
|
||||
self.assertEqual(m.grants, ())
|
||||
self.assertFalse(m.signed)
|
||||
|
||||
def test_full(self):
|
||||
m = validate_document(FULL)
|
||||
self.assertEqual(m.sanitize.scope, "docs") # defaults to the manifest id
|
||||
self.assertTrue(m.sanitize.verify)
|
||||
self.assertEqual(m.embed.chunk_overlap_tokens, 64)
|
||||
self.assertEqual(m.summarize.per, ("file",))
|
||||
self.assertEqual(m.sink.seal_to, ("k1",))
|
||||
self.assertEqual(m.grants[0].host, "other-host")
|
||||
self.assertEqual(m.grants[0].until.isoformat(), "2026-12-31")
|
||||
|
||||
def test_example_file_loads(self):
|
||||
m = load_manifest(EXAMPLE)
|
||||
self.assertEqual(m.id, "home-documents")
|
||||
self.assertEqual(m.stages[-1], "seal")
|
||||
self.assertEqual(len(m.grants), 1)
|
||||
|
||||
def test_yaml_date_grant(self):
|
||||
# PyYAML parses a bare 2026-12-31 as a date; both forms are accepted
|
||||
doc = yaml.safe_load("until: 2026-12-31")
|
||||
g = with_(FULL, grants=[{"host": "h", "ops": ["search"], "until": doc["until"]}])
|
||||
self.assertEqual(errors_of(g), [])
|
||||
|
||||
|
||||
class DoctrineTests(unittest.TestCase):
|
||||
"""The rules that exist because of what the product promises."""
|
||||
|
||||
def test_plaintext_is_an_invariant(self):
|
||||
doc = with_(MINIMAL, sink={"catalog": "local", "vectors": "local", "plaintext": "host"})
|
||||
self.assertTrue(any("invariant" in e for e in errors_of(doc)))
|
||||
|
||||
def test_vectors_local_only(self):
|
||||
doc = with_(MINIMAL, sink={"catalog": "local", "vectors": "host", "plaintext": "never"})
|
||||
self.assertTrue(any("vectors" in e for e in errors_of(doc)))
|
||||
|
||||
def test_read_is_mandatory(self):
|
||||
self.assertTrue(any("mandatory" in e for e in errors_of(with_(MINIMAL, mask=["search"]))))
|
||||
|
||||
def test_write_does_not_exist(self):
|
||||
self.assertTrue(any("mask" in e for e in errors_of(with_(MINIMAL, mask=["read", "write"]))))
|
||||
|
||||
def test_stage_needs_mask_op(self):
|
||||
doc = with_(FULL, mask=["read", "search", "summarize"])
|
||||
errs = errors_of(doc)
|
||||
self.assertTrue(any("'embed' requires 'index'" in e for e in errs))
|
||||
self.assertTrue(any("'catalog' requires 'index'" in e for e in errs))
|
||||
|
||||
def test_host_catalog_needs_seal(self):
|
||||
doc = with_(FULL, stages=["walk", "extract", "sanitize", "chunk", "embed", "catalog"],
|
||||
grants=None, sink={"catalog": "host", "vectors": "local", "plaintext": "never"})
|
||||
self.assertTrue(any("unsealed" in e for e in errors_of(doc)))
|
||||
|
||||
def test_grant_needs_seal(self):
|
||||
doc = with_(FULL, stages=["walk", "extract", "sanitize", "chunk", "embed", "catalog"],
|
||||
sink={"catalog": "local", "vectors": "local", "plaintext": "never"})
|
||||
self.assertTrue(any("grants: any grant requires the 'seal'" in e for e in errors_of(doc)))
|
||||
|
||||
def test_grant_search_needs_mask_search(self):
|
||||
doc = with_(FULL, mask=["read", "index", "summarize"])
|
||||
self.assertTrue(any("op 'search' requires 'search' in mask" in e for e in errors_of(doc)))
|
||||
|
||||
def test_grant_to_self_refused(self):
|
||||
doc = with_(FULL, grants=[{"host": "owner-host", "ops": ["search"], "until": "2026-12-31"}])
|
||||
self.assertTrue(any("owner" in e for e in errors_of(doc)))
|
||||
|
||||
def test_require_signed(self):
|
||||
self.assertTrue(any("signature: required" in e
|
||||
for e in errors_of(MINIMAL, require_signed=True)))
|
||||
signed = with_(MINIMAL, signature={"alg": "ecdsa-p256-sha256", "cert": "c.crt",
|
||||
"value": "AAAA"})
|
||||
self.assertEqual(errors_of(signed, require_signed=True), [])
|
||||
|
||||
|
||||
class StageTests(unittest.TestCase):
|
||||
def test_walk_first(self):
|
||||
self.assertTrue(any("first" in e for e in errors_of(with_(MINIMAL, stages=["extract"]))))
|
||||
|
||||
def test_canonical_order(self):
|
||||
doc = with_(FULL, stages=["walk", "chunk", "extract", "sanitize", "embed", "catalog", "seal"])
|
||||
self.assertTrue(any("canonical order" in e for e in errors_of(doc)))
|
||||
|
||||
def test_prerequisite_stage(self):
|
||||
doc = with_(MINIMAL, mask=["read", "index"], stages=["walk", "catalog"])
|
||||
self.assertTrue(any("'catalog' requires 'sanitize'" in e for e in errors_of(doc)))
|
||||
# chunk and catalog read the sanitized store: extract alone is not enough
|
||||
doc = with_(MINIMAL, mask=["read", "index"], stages=["walk", "extract", "catalog"])
|
||||
self.assertTrue(any("'catalog' requires 'sanitize'" in e for e in errors_of(doc)))
|
||||
doc = with_(FULL, stages=["walk", "extract", "chunk", "embed", "catalog", "seal"], sanitize=None)
|
||||
self.assertTrue(any("'chunk' requires 'sanitize'" in e for e in errors_of(doc)))
|
||||
|
||||
def test_unknown_stage(self):
|
||||
self.assertTrue(errors_of(with_(MINIMAL, stages=["walk", "upload"])))
|
||||
|
||||
def test_section_iff_stage(self):
|
||||
errs = errors_of(with_(MINIMAL, sanitize={"mode": "flag"}))
|
||||
self.assertTrue(any("not in stages" in e for e in errs))
|
||||
errs = errors_of(with_(MINIMAL, stages=["walk", "extract", "sanitize"]))
|
||||
self.assertTrue(any("sanitize: section required" in e for e in errs))
|
||||
errs = errors_of(with_(MINIMAL, mask=["read", "index"], sanitize={"mode": "flag"},
|
||||
stages=["walk", "extract", "sanitize", "chunk", "embed"]))
|
||||
self.assertTrue(any("embed: section required" in e for e in errs))
|
||||
|
||||
def test_summarize_needs_mask_and_catalog(self):
|
||||
doc = with_(MINIMAL, summarize={"model": "s", "per": ["file"]})
|
||||
errs = errors_of(doc)
|
||||
self.assertTrue(any("'summarize' is not in mask" in e for e in errs))
|
||||
self.assertTrue(any("requires the 'catalog' stage" in e for e in errs))
|
||||
|
||||
|
||||
class FieldTests(unittest.TestCase):
|
||||
def test_unknown_keys_everywhere(self):
|
||||
self.assertTrue(any("unknown key 'extra'" in e for e in errors_of(with_(MINIMAL, extra=1))))
|
||||
doc = with_(MINIMAL, scope={"roots": ["/a"], "excludes": []})
|
||||
self.assertTrue(any("unknown key 'excludes'" in e for e in errors_of(doc)))
|
||||
|
||||
def test_relative_root_refused(self):
|
||||
doc = with_(MINIMAL, scope={"roots": ["Documents"]})
|
||||
self.assertTrue(any("absolute" in e for e in errors_of(doc)))
|
||||
self.assertTrue(errors_of(with_(MINIMAL, scope={"roots": ["C:relative"]})))
|
||||
|
||||
def test_windows_and_unc_roots_accepted(self):
|
||||
for root in (r"C:\Users\me\Documents", "D:/data", r"\\nas\share\docs", "~", "~/x", "/x"):
|
||||
self.assertEqual(errors_of(with_(MINIMAL, scope={"roots": [root]})), [], root)
|
||||
|
||||
def test_schema_version(self):
|
||||
self.assertTrue(any("schema version" in e for e in errors_of(with_(MINIMAL, freeagent=2))))
|
||||
|
||||
def test_slugs(self):
|
||||
self.assertTrue(errors_of(with_(MINIMAL, id="Home Docs")))
|
||||
self.assertTrue(errors_of(with_(MINIMAL, owner={"host": "Owner"})))
|
||||
|
||||
def test_sweep_vocabulary(self):
|
||||
self.assertTrue(any("sweep" in e for e in errors_of(with_(MINIMAL, sweep="weekly"))))
|
||||
|
||||
def test_embed_overlap_smaller_than_size(self):
|
||||
doc = with_(FULL, embed={"model": "m", "dim": 8,
|
||||
"chunk": {"size_tokens": 64, "overlap_tokens": 64}})
|
||||
self.assertTrue(any("smaller than size_tokens" in e for e in errors_of(doc)))
|
||||
|
||||
def test_flag_mode_rejects_verify(self):
|
||||
doc = with_(FULL, sanitize={"mode": "flag", "verify": True})
|
||||
self.assertTrue(any("only valid with mode 'sterilize'" in e for e in errors_of(doc)))
|
||||
|
||||
def test_bad_grant_date(self):
|
||||
doc = with_(FULL, grants=[{"host": "h", "ops": ["search"], "until": "2026-02-30"}])
|
||||
self.assertTrue(any("real YYYY-MM-DD" in e for e in errors_of(doc)))
|
||||
|
||||
def test_bad_signature_base64(self):
|
||||
doc = with_(MINIMAL, signature={"alg": "ecdsa-p256-sha256", "cert": "c", "value": "*!*"})
|
||||
self.assertTrue(any("base64" in e for e in errors_of(doc)))
|
||||
|
||||
def test_all_errors_collected(self):
|
||||
doc = with_(MINIMAL, freeagent=9, id="BAD", sweep="never", mask=["search"])
|
||||
self.assertGreaterEqual(len(errors_of(doc)), 4)
|
||||
|
||||
|
||||
class SigningTests(unittest.TestCase):
|
||||
def test_payload_excludes_signature_and_is_canonical(self):
|
||||
signed = with_(FULL, signature={"alg": "ecdsa-p256-sha256", "cert": "c", "value": "AAAA"})
|
||||
self.assertEqual(signing_payload(signed), signing_payload(FULL))
|
||||
# key order in the source must not change the bytes
|
||||
reordered = dict(reversed(list(FULL.items())))
|
||||
self.assertEqual(signing_payload(reordered), signing_payload(FULL))
|
||||
self.assertNotIn(b" ", signing_payload(FULL))
|
||||
|
||||
def test_digest_matches_payload(self):
|
||||
self.assertEqual(signing_digest(FULL),
|
||||
hashlib.sha256(signing_payload(FULL)).hexdigest())
|
||||
|
||||
def test_yaml_date_and_quoted_date_sign_identically(self):
|
||||
bare = yaml.safe_load("until: 2026-12-31")["until"]
|
||||
a = with_(FULL, grants=[{"host": "h", "ops": ["search"], "until": bare}])
|
||||
b = with_(FULL, grants=[{"host": "h", "ops": ["search"], "until": "2026-12-31"}])
|
||||
self.assertEqual(signing_payload(a), signing_payload(b))
|
||||
|
||||
def test_any_field_change_changes_digest(self):
|
||||
self.assertNotEqual(signing_digest(FULL), signing_digest(with_(FULL, sweep="manual")))
|
||||
|
||||
|
||||
class FileTests(unittest.TestCase):
|
||||
def test_missing_file(self):
|
||||
with self.assertRaises(ManifestError):
|
||||
load_manifest("/nonexistent/manifest.yaml")
|
||||
|
||||
def test_invalid_yaml(self):
|
||||
fd, path = tempfile.mkstemp(suffix=".yaml")
|
||||
with os.fdopen(fd, "w") as fh:
|
||||
fh.write("freeagent: [1\n")
|
||||
try:
|
||||
with self.assertRaises(ManifestError) as cm:
|
||||
load_manifest(path)
|
||||
self.assertIn("not valid YAML", cm.exception.errors[0])
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user