feat: add institutional governance and recovery contracts
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from govoplan_core.core.object_storage import (
|
||||
LocalFilesystemStorageBackend,
|
||||
S3StorageBackend,
|
||||
StorageBackendError,
|
||||
StorageObjectMissing,
|
||||
normalize_storage_key,
|
||||
)
|
||||
|
||||
|
||||
class _S3Error(RuntimeError):
|
||||
def __init__(self, code: str, status: int) -> None:
|
||||
super().__init__(code)
|
||||
self.response = {
|
||||
"Error": {"Code": code},
|
||||
"ResponseMetadata": {"HTTPStatusCode": status},
|
||||
}
|
||||
|
||||
|
||||
class _FakeS3Client:
|
||||
def __init__(self) -> None:
|
||||
self.objects: dict[str, bytes] = {}
|
||||
self.head_error: Exception | None = None
|
||||
|
||||
def put_object(self, *, Key: str, Body: bytes, **_kwargs) -> None:
|
||||
self.objects[Key] = Body
|
||||
|
||||
def get_object(self, *, Key: str, **_kwargs):
|
||||
try:
|
||||
payload = self.objects[Key]
|
||||
except KeyError as exc:
|
||||
raise _S3Error("NoSuchKey", 404) from exc
|
||||
return {"Body": BytesIO(payload), "ContentLength": len(payload)}
|
||||
|
||||
def head_object(self, *, Key: str, **_kwargs):
|
||||
if self.head_error is not None:
|
||||
raise self.head_error
|
||||
try:
|
||||
payload = self.objects[Key]
|
||||
except KeyError as exc:
|
||||
raise _S3Error("NotFound", 404) from exc
|
||||
return {"ContentLength": len(payload)}
|
||||
|
||||
def delete_object(self, *, Key: str, **_kwargs) -> None:
|
||||
self.objects.pop(Key, None)
|
||||
|
||||
|
||||
class ObjectStorageTests(unittest.TestCase):
|
||||
def test_local_backend_round_trip_pagination_and_safe_keys(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-object-store-") as directory:
|
||||
backend = LocalFilesystemStorageBackend(Path(directory))
|
||||
backend.put_bytes("campaign/a.eml", b"a")
|
||||
backend.put_bytes("campaign/b.eml", b"bb")
|
||||
|
||||
first = backend.list_objects(prefix="campaign/", limit=1)
|
||||
second = backend.list_objects(
|
||||
prefix="campaign/",
|
||||
after=first.next_cursor,
|
||||
limit=1,
|
||||
)
|
||||
|
||||
self.assertEqual(b"a", backend.get_bytes("campaign/a.eml"))
|
||||
self.assertEqual(1, backend.stat("campaign/a.eml").size_bytes)
|
||||
self.assertEqual(
|
||||
("campaign/a.eml",), tuple(item.key for item in first.objects)
|
||||
)
|
||||
self.assertEqual("campaign/a.eml", first.next_cursor)
|
||||
self.assertEqual(
|
||||
("campaign/b.eml",), tuple(item.key for item in second.objects)
|
||||
)
|
||||
self.assertIsNone(second.next_cursor)
|
||||
with self.assertRaises(StorageBackendError):
|
||||
normalize_storage_key("../outside")
|
||||
with self.assertRaises(StorageObjectMissing):
|
||||
backend.get_bytes("campaign/missing.eml")
|
||||
|
||||
def test_local_listing_is_globally_sorted_and_ignores_symlinks(self) -> None:
|
||||
with (
|
||||
tempfile.TemporaryDirectory(prefix="govoplan-object-store-") as directory,
|
||||
tempfile.TemporaryDirectory(prefix="govoplan-object-outside-") as outside,
|
||||
):
|
||||
root = Path(directory)
|
||||
backend = LocalFilesystemStorageBackend(root)
|
||||
backend.put_bytes("a/x.txt", b"nested")
|
||||
backend.put_bytes("a.txt", b"sibling")
|
||||
backend.put_bytes("b.txt", b"last")
|
||||
outside_file = Path(outside) / "secret.txt"
|
||||
outside_file.write_bytes(b"outside")
|
||||
(root / "linked-file.txt").symlink_to(outside_file)
|
||||
(root / "linked-directory").symlink_to(
|
||||
Path(outside), target_is_directory=True
|
||||
)
|
||||
|
||||
first = backend.list_objects(prefix="", limit=2)
|
||||
second = backend.list_objects(
|
||||
prefix="",
|
||||
after=first.next_cursor,
|
||||
limit=2,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
("a.txt", "a/x.txt"),
|
||||
tuple(item.key for item in first.objects),
|
||||
)
|
||||
self.assertEqual("a/x.txt", first.next_cursor)
|
||||
self.assertEqual(
|
||||
("b.txt",),
|
||||
tuple(item.key for item in second.objects),
|
||||
)
|
||||
self.assertIsNone(second.next_cursor)
|
||||
with self.assertRaises(StorageBackendError):
|
||||
backend.exists("../outside")
|
||||
|
||||
def test_s3_backend_distinguishes_missing_objects_from_backend_failure(
|
||||
self,
|
||||
) -> None:
|
||||
backend = S3StorageBackend(
|
||||
bucket="files",
|
||||
endpoint_url="http://garage:3900",
|
||||
region_name="garage",
|
||||
access_key_id="key",
|
||||
secret_access_key="secret",
|
||||
deployment_managed=True,
|
||||
)
|
||||
client = _FakeS3Client()
|
||||
backend._client = client
|
||||
|
||||
backend.put_bytes("campaign/message.eml", b"message/rfc822")
|
||||
self.assertTrue(backend.exists("campaign/message.eml"))
|
||||
self.assertEqual(b"message/rfc822", backend.get_bytes("campaign/message.eml"))
|
||||
self.assertFalse(backend.exists("campaign/missing.eml"))
|
||||
|
||||
client.head_error = _S3Error("AccessDenied", 403)
|
||||
with self.assertRaises(StorageBackendError):
|
||||
backend.exists("campaign/message.eml")
|
||||
|
||||
def test_external_s3_requires_explicit_https_deployment_trust(self) -> None:
|
||||
trusted = S3StorageBackend(
|
||||
bucket="files",
|
||||
endpoint_url="https://s3.internal.example.test",
|
||||
region_name="eu-test-1",
|
||||
access_key_id="key",
|
||||
secret_access_key="secret",
|
||||
endpoint_trusted=True,
|
||||
)
|
||||
trusted._client = _FakeS3Client()
|
||||
trusted.put_bytes("files/document.txt", b"document")
|
||||
self.assertEqual(b"document", trusted.get_bytes("files/document.txt"))
|
||||
|
||||
rejected = S3StorageBackend(
|
||||
bucket="files",
|
||||
endpoint_url="http://s3.internal.example.test",
|
||||
region_name="eu-test-1",
|
||||
access_key_id="key",
|
||||
secret_access_key="secret",
|
||||
endpoint_trusted=True,
|
||||
)
|
||||
with self.assertRaisesRegex(StorageBackendError, "HTTPS origin"):
|
||||
rejected.client
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user