feat(files): surface configured handbook tasks
This commit is contained in:
190
tests/test_documentation.py
Normal file
190
tests/test_documentation.py
Normal file
@@ -0,0 +1,190 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.core.modules import DocumentationContext
|
||||
from govoplan_files.backend.documentation import documentation_topics
|
||||
from govoplan_files.backend.storage.connector_profiles import ConnectorProfile
|
||||
|
||||
|
||||
class _Principal:
|
||||
tenant_id = "tenant-1"
|
||||
user = SimpleNamespace(id="user-1")
|
||||
group_ids = frozenset({"group-1"})
|
||||
|
||||
def __init__(self, scopes: set[str]) -> None:
|
||||
self.scopes = frozenset(scopes)
|
||||
|
||||
def has(self, scope: str) -> bool:
|
||||
return scope in self.scopes
|
||||
|
||||
|
||||
class FilesRuntimeDocumentationTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.session = Session()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.session.close()
|
||||
|
||||
def context(
|
||||
self,
|
||||
scopes: set[str],
|
||||
*,
|
||||
settings: object | None = None,
|
||||
documentation_type: str = "user",
|
||||
) -> DocumentationContext:
|
||||
return DocumentationContext(
|
||||
registry=object(),
|
||||
principal=_Principal(scopes),
|
||||
settings=settings,
|
||||
session=self.session,
|
||||
documentation_type=documentation_type, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
def test_upload_tasks_state_exact_current_safe_limits(self) -> None:
|
||||
settings = SimpleNamespace(
|
||||
file_upload_max_bytes=7 * 1024 * 1024,
|
||||
file_upload_zip_max_bytes=19 * 1024 * 1024,
|
||||
)
|
||||
with patch(
|
||||
"govoplan_files.backend.documentation.visible_connector_profiles_for_actor",
|
||||
return_value=[],
|
||||
):
|
||||
topics = {
|
||||
topic.id: topic
|
||||
for topic in documentation_topics(
|
||||
self.context(
|
||||
{"files:file:read", "files:file:upload"}, settings=settings
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
upload = topics["files.workflow.upload-managed-files"]
|
||||
self.assertIn("7 MiB (7,340,032 bytes)", upload.body)
|
||||
self.assertEqual(["files.list"], upload.metadata["help_contexts"])
|
||||
self.assertEqual(
|
||||
{"files:file:read", "files:file:upload"},
|
||||
set(upload.conditions[0].required_scopes),
|
||||
)
|
||||
|
||||
archive = topics["files.workflow.upload-and-unpack-zip"]
|
||||
self.assertIn("19 MiB (19,922,944 bytes)", archive.body)
|
||||
self.assertIn("7 MiB (7,340,032 bytes)", archive.body)
|
||||
self.assertIn("1,000", archive.body)
|
||||
self.assertIn("Actual extracted bytes are counted", archive.body)
|
||||
|
||||
def test_connector_task_requires_authority_and_a_visible_usable_profile(
|
||||
self,
|
||||
) -> None:
|
||||
usable = ConnectorProfile(
|
||||
id="private-profile-id",
|
||||
label="Private profile label",
|
||||
provider="webdav",
|
||||
scope_type="tenant",
|
||||
scope_id="tenant-1",
|
||||
endpoint_url="https://private.example.invalid/secret-root",
|
||||
base_path="/secret-root",
|
||||
credential_mode="anonymous",
|
||||
)
|
||||
context = self.context({"files:file:read", "files:file:upload"})
|
||||
with patch(
|
||||
"govoplan_files.backend.documentation.visible_connector_profiles_for_actor",
|
||||
return_value=[usable],
|
||||
) as visible:
|
||||
topics = {topic.id: topic for topic in documentation_topics(context)}
|
||||
|
||||
self.assertIn("files.workflow.import-managed-snapshot", topics)
|
||||
self.assertNotIn("files.connector-import-unavailable", topics)
|
||||
task = topics["files.workflow.import-managed-snapshot"]
|
||||
self.assertEqual("configured", task.layer)
|
||||
self.assertEqual(
|
||||
["files.list", "files.connector-import"], task.metadata["help_contexts"]
|
||||
)
|
||||
self.assertIn("re-authorized", task.body)
|
||||
self.assertNotIn("private-profile-id", repr(task))
|
||||
self.assertNotIn("private.example.invalid", repr(task))
|
||||
self.assertNotIn("secret-root", repr(task))
|
||||
visible.assert_called_once()
|
||||
self.assertEqual(("group-1",), tuple(visible.call_args.kwargs["group_ids"]))
|
||||
|
||||
without_authority = {
|
||||
topic.id: topic
|
||||
for topic in documentation_topics(self.context({"files:file:read"}))
|
||||
}
|
||||
self.assertNotIn("files.workflow.import-managed-snapshot", without_authority)
|
||||
self.assertIn(
|
||||
"both permission to view Files and permission to upload",
|
||||
without_authority["files.connector-import-unavailable"].body,
|
||||
)
|
||||
|
||||
def test_connector_limitation_is_precise_but_never_exposes_profile_internals(
|
||||
self,
|
||||
) -> None:
|
||||
blocked = ConnectorProfile(
|
||||
id="sensitive-profile-id",
|
||||
label="Sensitive label",
|
||||
provider="s3",
|
||||
scope_type="tenant",
|
||||
scope_id="tenant-1",
|
||||
endpoint_url="https://internal.example.invalid/private",
|
||||
base_path="/classified",
|
||||
credential_mode="basic",
|
||||
password_value="credential-secret",
|
||||
)
|
||||
with patch(
|
||||
"govoplan_files.backend.documentation.visible_connector_profiles_for_actor",
|
||||
return_value=[blocked],
|
||||
):
|
||||
topics = {
|
||||
topic.id: topic
|
||||
for topic in documentation_topics(
|
||||
self.context({"files:file:read", "files:file:upload"})
|
||||
)
|
||||
}
|
||||
|
||||
self.assertNotIn("files.workflow.import-managed-snapshot", topics)
|
||||
limitation = topics["files.connector-import-unavailable"]
|
||||
self.assertEqual("available", limitation.layer)
|
||||
self.assertIn("pinning-safe provider", limitation.body)
|
||||
payload = repr(limitation)
|
||||
for secret in (
|
||||
"sensitive-profile-id",
|
||||
"Sensitive label",
|
||||
"internal.example.invalid",
|
||||
"classified",
|
||||
"credential-secret",
|
||||
):
|
||||
self.assertNotIn(secret, payload)
|
||||
|
||||
def test_connector_evaluation_errors_fail_closed_without_error_details(
|
||||
self,
|
||||
) -> None:
|
||||
with patch(
|
||||
"govoplan_files.backend.documentation.visible_connector_profiles_for_actor",
|
||||
side_effect=RuntimeError("private endpoint /root credential-id"),
|
||||
):
|
||||
topics = {
|
||||
topic.id: topic
|
||||
for topic in documentation_topics(
|
||||
self.context({"files:file:read", "files:file:upload"})
|
||||
)
|
||||
}
|
||||
|
||||
self.assertNotIn("files.workflow.import-managed-snapshot", topics)
|
||||
limitation = topics["files.connector-import-unavailable"]
|
||||
self.assertIn("could not be safely evaluated", limitation.body)
|
||||
self.assertNotIn("private endpoint", repr(limitation))
|
||||
self.assertNotIn("credential-id", repr(limitation))
|
||||
|
||||
def test_runtime_provider_only_emits_user_documentation(self) -> None:
|
||||
self.assertEqual(
|
||||
(), documentation_topics(self.context(set(), documentation_type="admin"))
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -3,14 +3,22 @@ from __future__ import annotations
|
||||
import unittest
|
||||
|
||||
|
||||
TOPIC_IDS = {
|
||||
"files.workflow.upload-organize-and-share",
|
||||
"files.workflow.import-managed-snapshot",
|
||||
STATIC_TOPIC_IDS = {
|
||||
"files.workflow.organize-managed-files",
|
||||
"files.workflow.find-and-download-files",
|
||||
"files.workflow.share-managed-files",
|
||||
"files.workflow.delete-managed-files",
|
||||
"files.governed-connectors-and-provenance",
|
||||
"files.reference.integrity-recovery-and-fail-closed-transports",
|
||||
"files.reference.snapshot-provenance-and-capabilities",
|
||||
"files.assurance.process-and-release-readiness",
|
||||
}
|
||||
RUNTIME_TOPIC_IDS = {
|
||||
"files.workflow.upload-managed-files",
|
||||
"files.workflow.upload-and-unpack-zip",
|
||||
"files.workflow.import-managed-snapshot",
|
||||
"files.connector-import-unavailable",
|
||||
}
|
||||
HANDBOOK_HREF = "govoplan-files/docs/FILES_HANDBOOK.md"
|
||||
|
||||
|
||||
@@ -19,64 +27,98 @@ class FilesManifestDocumentationTests(unittest.TestCase):
|
||||
def setUpClass(cls) -> None:
|
||||
from govoplan_files.backend.manifest import manifest
|
||||
|
||||
cls.manifest = manifest
|
||||
cls.topics = {topic.id: topic for topic in manifest.documentation}
|
||||
|
||||
def topic(self, topic_id: str):
|
||||
return self.topics[topic_id]
|
||||
|
||||
def test_adaptive_topics_have_role_scope_module_and_link_contracts(self) -> None:
|
||||
self.assertEqual(TOPIC_IDS, set(self.topics))
|
||||
self.assertEqual({"admin", "user"}, {item for topic in self.topics.values() for item in topic.documentation_types})
|
||||
def test_static_topics_have_role_scope_module_and_link_contracts(self) -> None:
|
||||
self.assertEqual(STATIC_TOPIC_IDS, set(self.topics))
|
||||
self.assertEqual(
|
||||
{"admin", "user"},
|
||||
{
|
||||
item
|
||||
for topic in self.topics.values()
|
||||
for item in topic.documentation_types
|
||||
},
|
||||
)
|
||||
|
||||
for topic in self.topics.values():
|
||||
with self.subTest(topic=topic.id):
|
||||
self.assertTrue(topic.audience)
|
||||
self.assertTrue(topic.conditions)
|
||||
self.assertTrue(any("files" in condition.required_modules for condition in topic.conditions))
|
||||
self.assertTrue(any(condition.any_scopes or condition.required_scopes for condition in topic.conditions))
|
||||
self.assertTrue(
|
||||
any(
|
||||
"files" in condition.required_modules
|
||||
for condition in topic.conditions
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
any(
|
||||
condition.any_scopes or condition.required_scopes
|
||||
for condition in topic.conditions
|
||||
)
|
||||
)
|
||||
self.assertIn("runtime", {link.kind for link in topic.links})
|
||||
self.assertIn("api", {link.kind for link in topic.links})
|
||||
self.assertIn(HANDBOOK_HREF, {link.href for link in topic.links if link.kind == "repository"})
|
||||
self.assertIn(
|
||||
HANDBOOK_HREF,
|
||||
{link.href for link in topic.links if link.kind == "repository"},
|
||||
)
|
||||
|
||||
def test_import_topic_is_a_complete_user_workflow(self) -> None:
|
||||
topic = self.topic("files.workflow.import-managed-snapshot")
|
||||
|
||||
self.assertEqual(("user",), topic.documentation_types)
|
||||
self.assertEqual("workflow", topic.metadata["kind"])
|
||||
self.assertEqual("/files", topic.metadata["route"])
|
||||
self.assertEqual("Files", topic.metadata["screen"])
|
||||
self.assertEqual(
|
||||
{"files:file:read", "files:file:upload"},
|
||||
set(topic.conditions[0].required_scopes),
|
||||
self.assertIn(
|
||||
"documentation_topics",
|
||||
{provider.__name__ for provider in self.manifest.documentation_providers},
|
||||
)
|
||||
for key in ("prerequisites", "steps", "outcome", "verification"):
|
||||
self.assertTrue(topic.metadata[key])
|
||||
self.assertIn("never mutate the remote source", topic.body)
|
||||
self.assertIn("/api/v1/files/connectors/profiles/{profile_id}/import", {link.href for link in topic.links})
|
||||
|
||||
def test_managed_file_workflow_covers_upload_organization_and_current_share_boundary(self) -> None:
|
||||
topic = self.topic("files.workflow.upload-organize-and-share")
|
||||
|
||||
self.assertEqual(("user",), topic.documentation_types)
|
||||
self.assertEqual("workflow", topic.metadata["kind"])
|
||||
self.assertEqual(
|
||||
{
|
||||
def test_user_tasks_are_independently_authorized_and_contextual(self) -> None:
|
||||
expected_scopes = {
|
||||
"files.workflow.organize-managed-files": {
|
||||
"files:file:read",
|
||||
"files:file:upload",
|
||||
"files:file:organize",
|
||||
},
|
||||
"files.workflow.find-and-download-files": {
|
||||
"files:file:read",
|
||||
"files:file:download",
|
||||
},
|
||||
"files.workflow.share-managed-files": {
|
||||
"files:file:read",
|
||||
"files:file:share",
|
||||
},
|
||||
set(topic.conditions[0].required_scopes),
|
||||
)
|
||||
for key in ("prerequisites", "steps", "outcome", "verification"):
|
||||
self.assertTrue(topic.metadata[key])
|
||||
self.assertIn("does not yet provide a general share editor", topic.body)
|
||||
self.assertIn("no share-revocation route", topic.body)
|
||||
self.assertIn("/api/v1/files/upload", {link.href for link in topic.links})
|
||||
self.assertIn("/api/v1/files/transfer", {link.href for link in topic.links})
|
||||
self.assertIn("/api/v1/files/{file_id}/shares", {link.href for link in topic.links})
|
||||
"files.workflow.delete-managed-files": {
|
||||
"files:file:read",
|
||||
"files:file:delete",
|
||||
},
|
||||
}
|
||||
for topic_id, scopes in expected_scopes.items():
|
||||
with self.subTest(topic=topic_id):
|
||||
topic = self.topic(topic_id)
|
||||
self.assertEqual(("user",), topic.documentation_types)
|
||||
self.assertEqual("workflow", topic.metadata["kind"])
|
||||
self.assertEqual(scopes, set(topic.conditions[0].required_scopes))
|
||||
self.assertEqual(["files.list"], topic.metadata["help_contexts"])
|
||||
for key in ("prerequisites", "steps", "outcome", "verification"):
|
||||
self.assertTrue(topic.metadata[key])
|
||||
|
||||
def test_admin_topic_covers_policy_redaction_and_atomic_credential_deletion(self) -> None:
|
||||
def test_share_and_delete_tasks_state_current_boundaries(self) -> None:
|
||||
share = self.topic("files.workflow.share-managed-files")
|
||||
self.assertEqual("available", share.layer)
|
||||
self.assertIn("does not yet provide a general share editor", share.body)
|
||||
self.assertIn("no share-revocation route", share.body)
|
||||
self.assertIn(
|
||||
"/api/v1/files/{file_id}/shares", {link.href for link in share.links}
|
||||
)
|
||||
|
||||
delete = self.topic("files.workflow.delete-managed-files")
|
||||
self.assertIn("Soft-delete", delete.summary)
|
||||
self.assertIn("no self-service restore or hard-purge", delete.body)
|
||||
self.assertTrue(
|
||||
any("not a hard purge" in item for item in delete.metadata["limitations"])
|
||||
)
|
||||
|
||||
def test_admin_topic_covers_policy_redaction_and_atomic_credential_deletion(
|
||||
self,
|
||||
) -> None:
|
||||
topic = self.topic("files.governed-connectors-and-provenance")
|
||||
|
||||
self.assertEqual(("admin",), topic.documentation_types)
|
||||
@@ -86,12 +128,24 @@ class FilesManifestDocumentationTests(unittest.TestCase):
|
||||
self.assertIn("deny rules win", topic.body)
|
||||
self.assertIn("redact secret values", topic.body)
|
||||
self.assertIn("same transaction", topic.body)
|
||||
self.assertTrue(any("non-owned external references" in item for item in topic.metadata["security_invariants"]))
|
||||
self.assertIn("/api/v1/files/connectors/policies/tenant", {link.href for link in topic.links})
|
||||
self.assertIn("/api/v1/files/connectors/credentials", {link.href for link in topic.links})
|
||||
self.assertTrue(
|
||||
any(
|
||||
"non-owned external references" in item
|
||||
for item in topic.metadata["security_invariants"]
|
||||
)
|
||||
)
|
||||
self.assertIn(
|
||||
"/api/v1/files/connectors/policies/tenant",
|
||||
{link.href for link in topic.links},
|
||||
)
|
||||
self.assertIn(
|
||||
"/api/v1/files/connectors/credentials", {link.href for link in topic.links}
|
||||
)
|
||||
|
||||
def test_operator_topic_covers_recovery_and_fail_closed_s3_smb(self) -> None:
|
||||
topic = self.topic("files.reference.integrity-recovery-and-fail-closed-transports")
|
||||
topic = self.topic(
|
||||
"files.reference.integrity-recovery-and-fail-closed-transports"
|
||||
)
|
||||
|
||||
self.assertEqual(("admin",), topic.documentation_types)
|
||||
self.assertEqual("reference", topic.metadata["kind"])
|
||||
@@ -102,7 +156,9 @@ class FilesManifestDocumentationTests(unittest.TestCase):
|
||||
self.assertIn("does not remove backend blob objects", topic.body)
|
||||
self.assertIn("MASTER_KEY_B64", topic.metadata["recovery_unit"])
|
||||
self.assertTrue(topic.metadata["verification"])
|
||||
self.assertIn("GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS", topic.configuration_keys)
|
||||
self.assertIn(
|
||||
"GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS", topic.configuration_keys
|
||||
)
|
||||
|
||||
def test_integrator_topic_exposes_capability_and_provenance_boundary(self) -> None:
|
||||
topic = self.topic("files.reference.snapshot-provenance-and-capabilities")
|
||||
@@ -117,11 +173,14 @@ class FilesManifestDocumentationTests(unittest.TestCase):
|
||||
self.assertIn("exact asset, version, blob, checksum", topic.body)
|
||||
self.assertIn("collaboration", topic.body)
|
||||
|
||||
def test_process_and_release_assurance_exposes_gates_evidence_and_limits(self) -> None:
|
||||
def test_process_and_release_assurance_exposes_gates_evidence_and_limits(
|
||||
self,
|
||||
) -> None:
|
||||
topic = self.topic("files.assurance.process-and-release-readiness")
|
||||
|
||||
self.assertEqual(("admin", "user"), topic.documentation_types)
|
||||
self.assertEqual("workflow", topic.metadata["kind"])
|
||||
self.assertEqual(["files.list"], topic.metadata["help_contexts"])
|
||||
self.assertIn("process_owner", topic.audience)
|
||||
self.assertIn("release_manager", topic.audience)
|
||||
self.assertIn("versions align", topic.body)
|
||||
@@ -129,16 +188,22 @@ class FilesManifestDocumentationTests(unittest.TestCase):
|
||||
self.assertIn("no enforced retention or legal hold", topic.body)
|
||||
for key in ("prerequisites", "steps", "outcome", "verification"):
|
||||
self.assertTrue(topic.metadata[key])
|
||||
self.assertTrue(any("meta-repository security" in step for step in topic.metadata["steps"]))
|
||||
self.assertTrue(
|
||||
any("meta-repository security" in step for step in topic.metadata["steps"])
|
||||
)
|
||||
self.assertTrue(any("fails closed" in step for step in topic.metadata["steps"]))
|
||||
self.assertTrue(any("SHA-256" in step for step in topic.metadata["steps"]))
|
||||
self.assertIn(HANDBOOK_HREF, {link.href for link in topic.links if link.kind == "repository"})
|
||||
|
||||
def test_internal_related_topic_ids_resolve(self) -> None:
|
||||
known_ids = STATIC_TOPIC_IDS | RUNTIME_TOPIC_IDS
|
||||
for topic in self.topics.values():
|
||||
for related_id in topic.metadata.get("related_topic_ids", []):
|
||||
if related_id.startswith("files."):
|
||||
self.assertIn(related_id, TOPIC_IDS, f"{topic.id} refers to missing topic {related_id}")
|
||||
self.assertIn(
|
||||
related_id,
|
||||
known_ids,
|
||||
f"{topic.id} refers to missing topic {related_id}",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user