gabriel / musehub public
test_minio_backend.py python
226 lines 9.4 KB
Raw
sha256:eead4146ec6c9905a097a89f8dbffa3d5c5e8ef9c1acd0e8a5b2a93b0084d273 Mpack content-addressability Human 6 days ago
1 """MinIO — BlobBackend as the only storage backend.
2
3 Tier 1 — Unit (no network)
4 - get_backend() raises RuntimeError when no bucket is configured
5 - get_backend() returns BlobBackend when blob_storage_bucket is set
6 - get_backend() returns BlobBackend when aws_s3_asset_bucket is set
7 - LocalBackend is not importable (deleted)
8
9 Tier 2 — Integration (MinIO required)
10 - BlobBackend.put/get/exists/delete round-trip against a live MinIO container
11 - put is idempotent (second write is safe)
12 - get returns None for missing object
13 - exists returns False for missing object
14
15 MinIO assumed at http://localhost:9000 with credentials minioadmin/minioadmin.
16 Tests in TestBlobBackendMinIO are skipped automatically when MinIO is unreachable.
17 """
18 from __future__ import annotations
19
20 import importlib
21 import secrets
22 from unittest.mock import patch
23
24 import pytest
25
26 from muse.core.types import long_id
27
28
29 # ─── helpers ──────────────────────────────────────────────────────────────────
30
31 MINIO_ENDPOINT = "http://localhost:9000"
32 MINIO_BUCKET = "muse-objects"
33 MINIO_ACCESS_KEY = "minioadmin"
34 MINIO_SECRET_KEY = "minioadmin"
35
36
37 def _oid() -> str:
38 return long_id(secrets.token_hex(32))
39
40
41 def _minio_backend() -> None:
42 from musehub.storage.backends import BlobBackend
43 return BlobBackend(
44 bucket=MINIO_BUCKET,
45 endpoint_url=MINIO_ENDPOINT,
46 access_key_id=MINIO_ACCESS_KEY,
47 secret_access_key=MINIO_SECRET_KEY,
48 region="us-east-1",
49 )
50
51
52 def _minio_reachable() -> bool:
53 try:
54 import urllib.request
55 urllib.request.urlopen(f"{MINIO_ENDPOINT}/minio/health/live", timeout=2)
56 return True
57 except Exception:
58 return False
59
60
61 requires_minio = pytest.mark.skipif(
62 not _minio_reachable(),
63 reason="MinIO not reachable at localhost:9000 — start docker compose first",
64 )
65
66
67 # ═══════════════════════════════════════════════════════════════════════════════
68 # Tier 1 — Unit: get_backend() contract after LocalBackend is deleted
69 # ═══════════════════════════════════════════════════════════════════════════════
70
71
72 class TestBlobBackendNaming:
73 def test_blob_backend_importable(self) -> None:
74 from musehub.storage.backends import BlobBackend # noqa: F401
75
76 def test_blob_backend_exported_from_storage(self) -> None:
77 import musehub.storage as mod
78 assert hasattr(mod, "BlobBackend")
79
80 def test_s3_backend_name_deleted(self) -> None:
81 """S3Backend must not exist — the name leaks the transport protocol."""
82 import musehub.storage.backends as mod
83 assert not hasattr(mod, "S3Backend"), (
84 "S3Backend still exists — rename it to BlobBackend"
85 )
86
87 def test_get_backend_impl_returns_blob_backend(self) -> None:
88 # get_backend() is conftest-patched to MemoryBackend; the selection logic
89 # under test lives in _get_backend_impl() (see its docstring).
90 from musehub.storage.backends import BlobBackend, _get_backend_impl
91 from unittest.mock import patch
92 with patch("musehub.storage.backends.settings") as mock_settings:
93 mock_settings.blob_storage_bucket = "muse-objects"
94 mock_settings.blob_storage_endpoint = MINIO_ENDPOINT
95 mock_settings.blob_storage_access_key_id = MINIO_ACCESS_KEY
96 mock_settings.blob_storage_secret_access_key = MINIO_SECRET_KEY
97 mock_settings.blob_storage_region = "us-east-1"
98 result = _get_backend_impl()
99 assert isinstance(result, BlobBackend)
100
101
102 class TestGetBackendNoFallback:
103 def test_raises_when_no_bucket_configured(self) -> None:
104 """get_backend() must raise RuntimeError when neither blob_storage_bucket nor
105 aws_s3_asset_bucket is set. No silent LocalBackend fallback."""
106 from musehub.storage.backends import _get_backend_impl
107 with patch("musehub.storage.backends.settings") as mock_settings:
108 mock_settings.blob_storage_bucket = None
109 mock_settings.aws_s3_asset_bucket = None
110 with pytest.raises(RuntimeError, match="No storage backend configured"):
111 _get_backend_impl()
112
113 def test_returns_blob_backend_when_blob_storage_bucket_set(self) -> None:
114 from musehub.storage.backends import BlobBackend, _get_backend_impl
115 with patch("musehub.storage.backends.settings") as mock_settings:
116 mock_settings.blob_storage_bucket = "muse-objects"
117 mock_settings.blob_storage_endpoint = MINIO_ENDPOINT
118 mock_settings.blob_storage_access_key_id = MINIO_ACCESS_KEY
119 mock_settings.blob_storage_secret_access_key = MINIO_SECRET_KEY
120 mock_settings.blob_storage_region = "auto"
121 result = _get_backend_impl()
122 assert isinstance(result, BlobBackend)
123
124 def test_returns_blob_backend_when_aws_bucket_set(self) -> None:
125 from musehub.storage.backends import BlobBackend, _get_backend_impl
126 with patch("musehub.storage.backends.settings") as mock_settings:
127 mock_settings.blob_storage_bucket = None
128 mock_settings.aws_s3_asset_bucket = "my-bucket"
129 mock_settings.aws_region = "us-east-1"
130 mock_settings.blob_storage_endpoint = None
131 mock_settings.blob_storage_access_key_id = None
132 mock_settings.blob_storage_secret_access_key = None
133 result = _get_backend_impl()
134 assert isinstance(result, BlobBackend)
135
136 def test_local_backend_is_deleted(self) -> None:
137 """LocalBackend must not exist in the module after deletion."""
138 import musehub.storage.backends as mod
139 assert not hasattr(mod, "LocalBackend"), (
140 "LocalBackend still exists in musehub.storage.backends — delete it"
141 )
142
143 def test_local_backend_not_in_storage_init(self) -> None:
144 """LocalBackend must not be exported from musehub.storage."""
145 import musehub.storage as mod
146 assert not hasattr(mod, "LocalBackend"), (
147 "LocalBackend still exported from musehub.storage — remove it from __all__"
148 )
149
150
151 # ═══════════════════════════════════════════════════════════════════════════════
152 # Tier 2 — Integration: S3Backend against live MinIO
153 # ═══════════════════════════════════════════════════════════════════════════════
154
155
156 class TestS3BackendMinIO:
157 @requires_minio
158 async def test_put_get_round_trip(self) -> None:
159 backend = _minio_backend()
160 oid = _oid()
161 data = b"hello minio " + secrets.token_bytes(32)
162 await backend.put(oid, data)
163 result = await backend.get(oid)
164 assert result == data
165
166 @requires_minio
167 async def test_exists_false_before_put(self) -> None:
168 backend = _minio_backend()
169 oid = _oid()
170 assert await backend.exists(oid) is False
171
172 @requires_minio
173 async def test_exists_true_after_put(self) -> None:
174 backend = _minio_backend()
175 oid = _oid()
176 await backend.put(oid, b"exists-check")
177 assert await backend.exists(oid) is True
178
179 @requires_minio
180 async def test_put_is_idempotent(self) -> None:
181 backend = _minio_backend()
182 oid = _oid()
183 data = b"idempotent-data"
184 await backend.put(oid, data)
185 await backend.put(oid, data)
186 result = await backend.get(oid)
187 assert result == data
188
189 @requires_minio
190 async def test_get_returns_none_for_missing(self) -> None:
191 backend = _minio_backend()
192 oid = _oid()
193 result = await backend.get(oid)
194 assert result is None
195
196 @requires_minio
197 async def test_delete_removes_object(self) -> None:
198 backend = _minio_backend()
199 oid = _oid()
200 await backend.put(oid, b"to-be-deleted")
201 assert await backend.exists(oid) is True
202 await backend.delete(oid)
203 assert await backend.exists(oid) is False
204
205 @requires_minio
206 async def test_uri_for_uses_s3_scheme(self) -> None:
207 backend = _minio_backend()
208 oid = _oid()
209 uri = backend.uri_for(oid)
210 assert uri.startswith(f"s3://{MINIO_BUCKET}/")
211
212 @requires_minio
213 async def test_get_backend_with_minio_env_vars(self) -> None:
214 """get_backend() wired to MinIO env vars returns a working S3Backend."""
215 from musehub.storage.backends import get_backend
216 with patch("musehub.storage.backends.settings") as mock_settings:
217 mock_settings.blob_storage_bucket = MINIO_BUCKET
218 mock_settings.blob_storage_endpoint = MINIO_ENDPOINT
219 mock_settings.blob_storage_access_key_id = MINIO_ACCESS_KEY
220 mock_settings.blob_storage_secret_access_key = MINIO_SECRET_KEY
221 mock_settings.blob_storage_region = "us-east-1"
222 backend = get_backend()
223
224 oid = _oid()
225 await backend.put(oid, b"via-get-backend")
226 assert await backend.get(oid) == b"via-get-backend"
File History 2 commits
sha256:eead4146ec6c9905a097a89f8dbffa3d5c5e8ef9c1acd0e8a5b2a93b0084d273 Mpack content-addressability Human 6 days ago
sha256:ef10830ce231e0a20efcb0e2586cb879471247e916616e6fdd0d51df459e2595 fix: typing audit — 0 violations, 0 untyped defs across all… Sonnet 4.6 minor 19 days ago