273 lines
13 KiB
Python
273 lines
13 KiB
Python
import asyncio
|
|||
|
|
import hashlib
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import httpx
|
||
|
|
import yaml
|
||
|
|
|
||
|
|
from gewu.adapters.git_repository import GitCanonicalRepository
|
||
|
|
from gewu.adapters.sqlite_identity import SQLiteIdentityStore
|
||
|
|
from gewu.adapters.text_extractor import TextMarkdownExtractor
|
||
|
|
from gewu.application.reading import CanonicalNodeReader
|
||
|
|
from gewu.application.uploads import LocalArtifactStore
|
||
|
|
from gewu.bootstrap.app import create_app
|
||
|
|
from tests.domain.proposal_fixtures import NODE, SOURCE_REVISION
|
||
|
|
|
||
|
|
|
||
|
|
def test_authenticated_scoped_canonical_node_read(tmp_path: Path) -> None:
|
||
|
|
async def exercise() -> None:
|
||
|
|
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
|
||
|
|
project_key = hashlib.sha256(NODE.project_id.encode()).hexdigest()
|
||
|
|
node_key = hashlib.sha256(NODE.node_id.encode()).hexdigest()
|
||
|
|
node_path = f"projects/{project_key}/nodes/{node_key}.yaml"
|
||
|
|
source_path = (
|
||
|
|
f"sources/{hashlib.sha256(SOURCE_REVISION.source_id.encode()).hexdigest()}/"
|
||
|
|
f"{hashlib.sha256(SOURCE_REVISION.revision_id.encode()).hexdigest()}.yaml"
|
||
|
|
)
|
||
|
|
artifact_store = LocalArtifactStore(tmp_path / "artifacts")
|
||
|
|
source_bytes = b"# Archived source\nA precise citation.\n"
|
||
|
|
source_digest = hashlib.sha256(source_bytes).hexdigest()
|
||
|
|
upload_id = artifact_store.begin(
|
||
|
|
filename="source.md",
|
||
|
|
media_type="text/markdown",
|
||
|
|
byte_length=len(source_bytes),
|
||
|
|
expected_sha256=source_digest,
|
||
|
|
owner_id="agent-pilot",
|
||
|
|
project_id=NODE.project_id,
|
||
|
|
).upload_id
|
||
|
|
artifact_store.put_chunk(
|
||
|
|
upload_id,
|
||
|
|
offset=0,
|
||
|
|
content=source_bytes,
|
||
|
|
chunk_sha256=source_digest,
|
||
|
|
owner_id="agent-pilot",
|
||
|
|
)
|
||
|
|
artifact_store.finalize(upload_id, owner_id="agent-pilot")
|
||
|
|
extraction = TextMarkdownExtractor().extract(
|
||
|
|
SOURCE_REVISION.revision_id, source_bytes, "text/markdown"
|
||
|
|
)
|
||
|
|
artifact_store.save_extraction(
|
||
|
|
extraction, owner_id="agent-pilot", project_id=NODE.project_id
|
||
|
|
)
|
||
|
|
archived_source = SOURCE_REVISION.model_copy(
|
||
|
|
update={"artifact_sha256": source_digest, "byte_length": len(source_bytes)}
|
||
|
|
)
|
||
|
|
repository.publish_files(
|
||
|
|
expected_head=repository.head(),
|
||
|
|
files={node_path: yaml.safe_dump(NODE.model_dump(mode="json")).encode()},
|
||
|
|
message="Publish test node",
|
||
|
|
index_directory=tmp_path / "publishing",
|
||
|
|
)
|
||
|
|
updated_node = NODE.model_copy(update={"summary": "A revised accepted summary."})
|
||
|
|
restricted_source = SOURCE_REVISION.model_copy(
|
||
|
|
update={
|
||
|
|
"source_id": "video:private",
|
||
|
|
"revision_id": "video:private:r1",
|
||
|
|
"ingested_for": ("interactive-video",),
|
||
|
|
}
|
||
|
|
)
|
||
|
|
restricted_source_path = (
|
||
|
|
f"sources/{hashlib.sha256(restricted_source.source_id.encode()).hexdigest()}/"
|
||
|
|
f"{hashlib.sha256(restricted_source.revision_id.encode()).hexdigest()}.yaml"
|
||
|
|
)
|
||
|
|
reference_source = SOURCE_REVISION.model_copy(
|
||
|
|
update={
|
||
|
|
"source_id": "dome:reference-only",
|
||
|
|
"revision_id": "dome:reference-only:r1",
|
||
|
|
"artifact_sha256": None,
|
||
|
|
"media_type": "application/octet-stream",
|
||
|
|
"byte_length": 0,
|
||
|
|
"original_url": "https://research.example/unavailable-paper",
|
||
|
|
"ingested_for": (NODE.project_id,),
|
||
|
|
"archive_status": "reference-only",
|
||
|
|
}
|
||
|
|
)
|
||
|
|
reference_source_path = (
|
||
|
|
f"sources/{hashlib.sha256(reference_source.source_id.encode()).hexdigest()}/"
|
||
|
|
f"{hashlib.sha256(reference_source.revision_id.encode()).hexdigest()}.yaml"
|
||
|
|
)
|
||
|
|
repository.publish_files(
|
||
|
|
expected_head=repository.head(),
|
||
|
|
files={
|
||
|
|
node_path: yaml.safe_dump(updated_node.model_dump(mode="json")).encode(),
|
||
|
|
source_path: yaml.safe_dump(archived_source.model_dump(mode="json")).encode(),
|
||
|
|
restricted_source_path: yaml.safe_dump(
|
||
|
|
restricted_source.model_dump(mode="json")
|
||
|
|
).encode(),
|
||
|
|
reference_source_path: yaml.safe_dump(
|
||
|
|
reference_source.model_dump(mode="json")
|
||
|
|
).encode(),
|
||
|
|
},
|
||
|
|
message="Revise node and register source",
|
||
|
|
index_directory=tmp_path / "publishing",
|
||
|
|
)
|
||
|
|
repository.publish_files(
|
||
|
|
expected_head=repository.head(),
|
||
|
|
files={"metadata/unrelated.yaml": b"accepted but unrelated\n"},
|
||
|
|
message="Advance repository without changing the node",
|
||
|
|
index_directory=tmp_path / "publishing",
|
||
|
|
)
|
||
|
|
identities = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
|
||
|
|
token = identities.provision(
|
||
|
|
credential_id="agent-key",
|
||
|
|
principal_id="agent-pilot",
|
||
|
|
roles=("agent",),
|
||
|
|
project_ids=(NODE.project_id,),
|
||
|
|
)
|
||
|
|
app = create_app(
|
||
|
|
identity_provider=identities,
|
||
|
|
node_reader=CanonicalNodeReader(repository),
|
||
|
|
artifact_store=artifact_store,
|
||
|
|
)
|
||
|
|
async with httpx.AsyncClient(
|
||
|
|
transport=httpx.ASGITransport(app=app), base_url="http://gewu.test"
|
||
|
|
) as client:
|
||
|
|
path = f"/v1/projects/{NODE.project_id}/nodes/{NODE.node_id}"
|
||
|
|
assert (await client.get(path)).status_code == 401
|
||
|
|
malformed_id = await client.get(
|
||
|
|
f"/v1/projects/{NODE.project_id}/nodes/not%20a%20record-id",
|
||
|
|
headers={"Authorization": f"Bearer {token}"},
|
||
|
|
)
|
||
|
|
assert malformed_id.status_code == 422
|
||
|
|
denied = await client.get(
|
||
|
|
f"/v1/projects/interactive-video/nodes/{NODE.node_id}",
|
||
|
|
headers={"Authorization": f"Bearer {token}"},
|
||
|
|
)
|
||
|
|
assert denied.status_code == 403
|
||
|
|
accepted = await client.get(path, headers={"Authorization": f"Bearer {token}"})
|
||
|
|
assert accepted.status_code == 200
|
||
|
|
envelope = accepted.json()
|
||
|
|
assert envelope["retrieval_mode"] == "canonical"
|
||
|
|
assert envelope["canonical_revision"] == repository.head()
|
||
|
|
assert envelope["data"][0]["node_id"] == NODE.node_id
|
||
|
|
assert envelope["citations"][0]["quote"] == NODE.claims[0].evidence[0].quote
|
||
|
|
history = await client.get(
|
||
|
|
f"{path}/history", headers={"Authorization": f"Bearer {token}"}
|
||
|
|
)
|
||
|
|
assert history.status_code == 200
|
||
|
|
history_data = history.json()["data"]
|
||
|
|
assert len(history_data) == 2
|
||
|
|
assert history_data[0]["node"]["summary"] == updated_node.summary
|
||
|
|
latest_node_revision = history_data[0]["revision"]
|
||
|
|
assert latest_node_revision != repository.head()
|
||
|
|
latest_file_version = await client.get(
|
||
|
|
f"{path}?revision={latest_node_revision}",
|
||
|
|
headers={"Authorization": f"Bearer {token}"},
|
||
|
|
)
|
||
|
|
assert latest_file_version.status_code == 200
|
||
|
|
assert latest_file_version.json()["canonical_revision"] == latest_node_revision
|
||
|
|
assert latest_file_version.json()["data"][0]["summary"] == updated_node.summary
|
||
|
|
historical_revision = history_data[-1]["revision"]
|
||
|
|
historical = await client.get(
|
||
|
|
f"{path}?revision={historical_revision}",
|
||
|
|
headers={"Authorization": f"Bearer {token}"},
|
||
|
|
)
|
||
|
|
assert historical.status_code == 200, historical.text
|
||
|
|
assert historical.json()["canonical_revision"] == historical_revision
|
||
|
|
assert historical.json()["data"][0]["summary"] == NODE.summary
|
||
|
|
assert historical.json()["citations"][0]["quote"] == NODE.claims[0].evidence[0].quote
|
||
|
|
malformed_revision = await client.get(
|
||
|
|
f"{path}?revision=not-a-commit",
|
||
|
|
headers={"Authorization": f"Bearer {token}"},
|
||
|
|
)
|
||
|
|
assert malformed_revision.status_code == 422
|
||
|
|
unknown_revision = await client.get(
|
||
|
|
f"{path}?revision={'a' * 40}",
|
||
|
|
headers={"Authorization": f"Bearer {token}"},
|
||
|
|
)
|
||
|
|
assert unknown_revision.status_code == 404
|
||
|
|
source = await client.get(
|
||
|
|
f"/v1/projects/{NODE.project_id}/sources/{SOURCE_REVISION.revision_id}",
|
||
|
|
headers={"Authorization": f"Bearer {token}"},
|
||
|
|
)
|
||
|
|
assert source.status_code == 200
|
||
|
|
assert source.json()["data"][0]["revision_id"] == SOURCE_REVISION.revision_id
|
||
|
|
reference_path = (
|
||
|
|
f"/v1/projects/{NODE.project_id}/sources/{reference_source.revision_id}"
|
||
|
|
)
|
||
|
|
reference = await client.get(
|
||
|
|
reference_path, headers={"Authorization": f"Bearer {token}"}
|
||
|
|
)
|
||
|
|
assert reference.status_code == 200
|
||
|
|
assert reference.json()["data"][0]["artifact_sha256"] is None
|
||
|
|
diagnostics_path = "/v1/diagnostics?projects=dome-performance"
|
||
|
|
assert (await client.get("/v1/diagnostics")).status_code == 401
|
||
|
|
diagnostic_response = await client.get(
|
||
|
|
diagnostics_path, headers={"Authorization": f"Bearer {token}"}
|
||
|
|
)
|
||
|
|
assert diagnostic_response.status_code == 200
|
||
|
|
diagnostic_body = diagnostic_response.json()
|
||
|
|
catalog_path = Path(__file__).resolve().parents[2] / "schema/v1/catalog.json"
|
||
|
|
assert (
|
||
|
|
diagnostic_body["catalog_sha256"]
|
||
|
|
== hashlib.sha256(catalog_path.read_bytes()).hexdigest()
|
||
|
|
)
|
||
|
|
assert diagnostic_body["selected_scope"]["projects"] == [NODE.project_id]
|
||
|
|
assert diagnostic_body["index_state"] == "not_configured"
|
||
|
|
assert diagnostic_body["max_upload_bytes"] == 50 * 1024 * 1024
|
||
|
|
assert "get_source_artifact" in diagnostic_body["authorized_operations"]
|
||
|
|
assert token not in diagnostic_response.text
|
||
|
|
assert "principal_id" not in diagnostic_body
|
||
|
|
forbidden_diagnostic_scope = await client.get(
|
||
|
|
"/v1/diagnostics?projects=interactive-video",
|
||
|
|
headers={"Authorization": f"Bearer {token}"},
|
||
|
|
)
|
||
|
|
assert forbidden_diagnostic_scope.status_code == 403
|
||
|
|
forbidden_shared_scope = await client.get(
|
||
|
|
diagnostics_path + "&include_shared=true",
|
||
|
|
headers={"Authorization": f"Bearer {token}"},
|
||
|
|
)
|
||
|
|
assert forbidden_shared_scope.status_code == 403
|
||
|
|
artifact_path = (
|
||
|
|
f"/v1/projects/{NODE.project_id}/sources/{SOURCE_REVISION.revision_id}/artifact"
|
||
|
|
)
|
||
|
|
assert (await client.get(artifact_path)).status_code == 401
|
||
|
|
archived = await client.get(artifact_path, headers={"Authorization": f"Bearer {token}"})
|
||
|
|
assert archived.status_code == 200
|
||
|
|
assert archived.content == source_bytes
|
||
|
|
assert archived.headers["etag"] == f'"{source_digest}"'
|
||
|
|
assert archived.headers["x-content-type-options"] == "nosniff"
|
||
|
|
assert archived.headers["cache-control"] == "private, no-store"
|
||
|
|
assert archived.headers["content-security-policy"] == "default-src 'none'; sandbox"
|
||
|
|
missing_artifact = await client.get(
|
||
|
|
f"{reference_path}/artifact", headers={"Authorization": f"Bearer {token}"}
|
||
|
|
)
|
||
|
|
assert missing_artifact.status_code == 404
|
||
|
|
block = extraction.blocks[0]
|
||
|
|
offset = block.normalized_text.index("precise")
|
||
|
|
location_path = (
|
||
|
|
f"/v1/projects/{NODE.project_id}/sources/{SOURCE_REVISION.revision_id}"
|
||
|
|
f"/extractions/{extraction.extraction_id}/blocks/{block.block_id}"
|
||
|
|
)
|
||
|
|
location = await client.get(
|
||
|
|
location_path,
|
||
|
|
params={"offset": offset, "length": len("precise")},
|
||
|
|
headers={"Authorization": f"Bearer {token}"},
|
||
|
|
)
|
||
|
|
assert location.status_code == 200
|
||
|
|
assert location.json()["normalized_text"] == "precise"
|
||
|
|
assert location.json()["original_text"] == "precise"
|
||
|
|
denied_location = await client.get(
|
||
|
|
location_path,
|
||
|
|
params={"offset": offset, "length": len("precise")},
|
||
|
|
)
|
||
|
|
assert denied_location.status_code == 401
|
||
|
|
restricted = await client.get(
|
||
|
|
f"/v1/projects/{NODE.project_id}/sources/{restricted_source.revision_id}",
|
||
|
|
headers={"Authorization": f"Bearer {token}"},
|
||
|
|
)
|
||
|
|
assert restricted.status_code == 404
|
||
|
|
restricted_artifact = await client.get(
|
||
|
|
f"/v1/projects/{NODE.project_id}/sources/{restricted_source.revision_id}/artifact",
|
||
|
|
headers={"Authorization": f"Bearer {token}"},
|
||
|
|
)
|
||
|
|
assert restricted_artifact.status_code == 404
|
||
|
|
assert identities.revoke("agent-key")
|
||
|
|
assert (
|
||
|
|
await client.get(path, headers={"Authorization": f"Bearer {token}"})
|
||
|
|
).status_code == 401
|
||
|
|
assert (
|
||
|
|
await client.get(artifact_path, headers={"Authorization": f"Bearer {token}"})
|
||
|
|
).status_code == 401
|
||
|
|
|
||
|
|
asyncio.run(exercise())
|