from __future__ import annotations import io from types import ModuleType import sys import unittest from unittest.mock import MagicMock, PropertyMock, patch from govoplan_files.backend.storage.backends import S3StorageBackend, StorageBackendError def _backend( *, endpoint_url: str = "https://objects.example.test", deployment_managed: bool = False, ) -> S3StorageBackend: return S3StorageBackend( bucket="files", endpoint_url=endpoint_url, region_name="test", access_key_id="access", secret_access_key="secret", deployment_managed=deployment_managed, ) class S3StorageBackendTests(unittest.TestCase): def test_sdk_transport_fails_closed_in_public_and_private_modes(self) -> None: for allow_private, address in ((False, "93.184.216.34"), (True, "10.0.0.5")): with self.subTest(allow_private=allow_private), patch.dict( "os.environ", { "APP_ENV": "production", "GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS": str(allow_private).lower(), }, ), patch( "govoplan_core.security.outbound_http.socket.getaddrinfo", return_value=[(2, 1, 6, "", (address, 443))], ), self.assertRaisesRegex(StorageBackendError, "until that transport supports.*DNS/IP pinning"): _backend().client def test_installer_managed_garage_uses_exact_endpoint_and_path_style(self) -> None: boto3 = ModuleType("boto3") boto3.client = MagicMock(return_value=object()) botocore = ModuleType("botocore") botocore.__path__ = [] botocore_config = ModuleType("botocore.config") class Config: def __init__(self, **values): self.values = values botocore_config.Config = Config with patch.dict( sys.modules, { "boto3": boto3, "botocore": botocore, "botocore.config": botocore_config, }, ): _backend( endpoint_url="http://garage:3900", deployment_managed=True, ).client _args, kwargs = boto3.client.call_args self.assertEqual("http://garage:3900", kwargs["endpoint_url"]) self.assertEqual( {"s3": {"addressing_style": "path"}}, kwargs["config"].values, ) def test_installer_managed_garage_rejects_any_other_endpoint(self) -> None: with self.assertRaisesRegex( StorageBackendError, "restricted to http://garage:3900", ): _backend( endpoint_url="http://other-s3:3900", deployment_managed=True, ).client def test_get_bytes_rejects_declared_oversize_object_without_reading(self) -> None: body = MagicMock() client = MagicMock() client.get_object.return_value = {"ContentLength": 6, "Body": body} with patch.dict("os.environ", {"GOVOPLAN_CONNECTOR_MAX_FILE_TRANSFER_BYTES": "5"}), patch.object( S3StorageBackend, "client", new_callable=PropertyMock, return_value=client, ), self.assertRaisesRegex(StorageBackendError, "deployment limit"): _backend().get_bytes("large.bin") body.read.assert_not_called() body.close.assert_called_once_with() def test_iter_bytes_rejects_undeclared_oversize_object(self) -> None: client = MagicMock() client.get_object.return_value = {"Body": io.BytesIO(b"123456")} with patch.dict("os.environ", {"GOVOPLAN_CONNECTOR_MAX_FILE_TRANSFER_BYTES": "5"}), patch.object( S3StorageBackend, "client", new_callable=PropertyMock, return_value=client, ), self.assertRaisesRegex(StorageBackendError, "deployment limit"): list(_backend().iter_bytes("large.bin", chunk_size=3)) def test_iter_bytes_closes_streaming_body_when_consumer_stops_early(self) -> None: body = MagicMock() body.read.side_effect = (b"123", b"456", b"") client = MagicMock() client.get_object.return_value = {"ContentLength": 6, "Body": body} with patch.dict("os.environ", {"GOVOPLAN_CONNECTOR_MAX_FILE_TRANSFER_BYTES": "10"}), patch.object( S3StorageBackend, "client", new_callable=PropertyMock, return_value=client, ): chunks = _backend().iter_bytes("object.bin", chunk_size=3) self.assertEqual(b"123", next(chunks)) chunks.close() body.close.assert_called_once_with() if __name__ == "__main__": unittest.main()