Adds the seven §10.2 hook-point modules + five env vars so Astrolabe Cloud can offload document processing to the external document-processor / embedding gateway. Purely additive: with every setting unset the server behaves exactly as today, so self-hosters are unaffected (Deck #92). Hook points (all default to current monolith behavior): - config: EMBEDDING_PROVIDER, INGEST_MODE, STATUS_BACKEND, COLLECTION_METADATA_SOURCE, FACT_EVENT_EMITTER (+ supporting settings), validated in Settings.__post_init__ (fail-fast STATUS_BACKEND=local with INGEST_MODE=external); shared canonical.py. - vector/payload_keys.py + acl_hash.py: cross-impl NAMESPACE/point_id (§2.2) and BLAKE2b-128 ACL hash (§11), pinned by fixtures shared with the document-processor repo. - embedding/gateway_client.py: OpenAI-compatible GatewayProvider authenticating via M2M OIDC client-credentials (separate realm); manual-only registry entry. - vector/collection_metadata.py: sentinel-point / API metadata source with env fallback. - vector/queue/: hexagonal ingest producer ports + memory/NATS adapters (Postgres seam); INGEST_MODE=external publishes mcp.ingest.requested.{tenant} instead of the in-memory stream and skips the in-process processor pool. The lifespan becomes a composition root across both deployment branches. - vector/queue/status.py: STATUS_BACKEND=bus subscriber feeding a StatusStore the vector-sync status endpoint reads. - admin/payload_backfill.py: POST /api/v1/admin/payload-backfill (admin scope); processor writes the new payload keys; query-side ACL pre-filter gated behind ACL_PREFILTER_ENABLED (default off). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
72 lines
2.0 KiB
Python
72 lines
2.0 KiB
Python
"""ACL hash spec tests (design §11) + cross-impl corpus.
|
|
|
|
The corpus fixture is checked into both repos; the processor runs a mirror of
|
|
``test_corpus`` against the same file. Divergence is caught in CI.
|
|
"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from nextcloud_mcp_server.acl_hash import (
|
|
PUBLIC_PRINCIPAL,
|
|
accessible_hash_set,
|
|
compute_acl_hash,
|
|
compute_principal_hash,
|
|
)
|
|
|
|
CORPUS = json.loads(
|
|
(Path(__file__).parent.parent / "fixtures" / "acl_hash_corpus.json").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"case", CORPUS["cases"], ids=[c["name"] for c in CORPUS["cases"]]
|
|
)
|
|
def test_corpus(case):
|
|
share_set = [tuple(p) for p in case["share_set"]]
|
|
assert compute_acl_hash(share_set) == case["expected"]
|
|
|
|
|
|
def test_blake2b_128_bit_length():
|
|
assert len(compute_principal_hash("user", "alice")) == 32
|
|
|
|
|
|
def test_case_is_preserved_not_folded():
|
|
assert compute_principal_hash("user", "Alice") != compute_principal_hash(
|
|
"user", "alice"
|
|
)
|
|
|
|
|
|
def test_nfc_normalization():
|
|
# decomposed 'café' (e + U+0301) hashes the same as precomposed 'café'.
|
|
assert compute_principal_hash("user", "café") == compute_principal_hash(
|
|
"user", "café"
|
|
)
|
|
|
|
|
|
def test_invalid_principal_type_rejected():
|
|
with pytest.raises(ValueError, match="principal_type must be one of"):
|
|
compute_principal_hash("link", "sometoken")
|
|
|
|
|
|
def test_accessible_set_always_includes_public():
|
|
s = accessible_hash_set("alice", [])
|
|
assert compute_principal_hash(*PUBLIC_PRINCIPAL) in s
|
|
|
|
|
|
def test_accessible_set_membership_matches_share():
|
|
# A document shared with a group the requester holds is admitted.
|
|
doc = compute_acl_hash([("group", "engineering")])
|
|
accessible = accessible_hash_set("bob", ["engineering", "all-staff"])
|
|
assert any(h in accessible for h in doc)
|
|
|
|
|
|
def test_accessible_set_excludes_unheld_group():
|
|
doc = compute_acl_hash([("group", "finance")])
|
|
accessible = accessible_hash_set("bob", ["engineering"])
|
|
assert not any(h in accessible for h in doc)
|