gabriel / musehub public
test_minio_backend.py python
224 lines 9.2 KB
Raw
sha256:eead4146ec6c9905a097a89f8dbffa3d5c5e8ef9c1acd0e8a5b2a93b0084d273 Mpack content-addressability Human 7 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_returns_blob_backend(self) -> None:
88 from musehub.storage.backends import BlobBackend, get_backend
89 from unittest.mock import patch
90 with patch("musehub.storage.backends.settings") as mock_settings:
91 mock_settings.blob_storage_bucket = "muse-objects"
92 mock_settings.blob_storage_endpoint = MINIO_ENDPOINT
93 mock_settings.blob_storage_access_key_id = MINIO_ACCESS_KEY
94 mock_settings.blob_storage_secret_access_key = MINIO_SECRET_KEY
95 mock_settings.blob_storage_region = "us-east-1"
96 result = get_backend()
97 assert isinstance(result, BlobBackend)
98
99
100 class TestGetBackendNoFallback:
101 def test_raises_when_no_bucket_configured(self) -> None:
102 """get_backend() must raise RuntimeError when neither blob_storage_bucket nor
103 aws_s3_asset_bucket is set. No silent LocalBackend fallback."""
104 from musehub.storage.backends import _get_backend_impl
105 with patch("musehub.storage.backends.settings") as mock_settings:
106 mock_settings.blob_storage_bucket = None
107 mock_settings.aws_s3_asset_bucket = None
108 with pytest.raises(RuntimeError, match="No storage backend configured"):
109 _get_backend_impl()
110
111 def test_returns_blob_backend_when_blob_storage_bucket_set(self) -> None:
112 from musehub.storage.backends import BlobBackend, get_backend
113 with patch("musehub.storage.backends.settings") as mock_settings:
114 mock_settings.blob_storage_bucket = "muse-objects"
115 mock_settings.blob_storage_endpoint = MINIO_ENDPOINT
116 mock_settings.blob_storage_access_key_id = MINIO_ACCESS_KEY
117 mock_settings.blob_storage_secret_access_key = MINIO_SECRET_KEY
118 mock_settings.blob_storage_region = "auto"
119 result = get_backend()
120 assert isinstance(result, BlobBackend)
121
122 def test_returns_blob_backend_when_aws_bucket_set(self) -> None:
123 from musehub.storage.backends import BlobBackend, get_backend
124 with patch("musehub.storage.backends.settings") as mock_settings:
125 mock_settings.blob_storage_bucket = None
126 mock_settings.aws_s3_asset_bucket = "my-bucket"
127 mock_settings.aws_region = "us-east-1"
128 mock_settings.blob_storage_endpoint = None
129 mock_settings.blob_storage_access_key_id = None
130 mock_settings.blob_storage_secret_access_key = None
131 result = get_backend()
132 assert isinstance(result, BlobBackend)
133
134 def test_local_backend_is_deleted(self) -> None:
135 """LocalBackend must not exist in the module after deletion."""
136 import musehub.storage.backends as mod
137 assert not hasattr(mod, "LocalBackend"), (
138 "LocalBackend still exists in musehub.storage.backends — delete it"
139 )
140
141 def test_local_backend_not_in_storage_init(self) -> None:
142 """LocalBackend must not be exported from musehub.storage."""
143 import musehub.storage as mod
144 assert not hasattr(mod, "LocalBackend"), (
145 "LocalBackend still exported from musehub.storage — remove it from __all__"
146 )
147
148
149 # ═══════════════════════════════════════════════════════════════════════════════
150 # Tier 2 — Integration: S3Backend against live MinIO
151 # ═══════════════════════════════════════════════════════════════════════════════
152
153
154 class TestS3BackendMinIO:
155 @requires_minio
156 async def test_put_get_round_trip(self) -> None:
157 backend = _minio_backend()
158 oid = _oid()
159 data = b"hello minio " + secrets.token_bytes(32)
160 await backend.put(oid, data)
161 result = await backend.get(oid)
162 assert result == data
163
164 @requires_minio
165 async def test_exists_false_before_put(self) -> None:
166 backend = _minio_backend()
167 oid = _oid()
168 assert await backend.exists(oid) is False
169
170 @requires_minio
171 async def test_exists_true_after_put(self) -> None:
172 backend = _minio_backend()
173 oid = _oid()
174 await backend.put(oid, b"exists-check")
175 assert await backend.exists(oid) is True
176
177 @requires_minio
178 async def test_put_is_idempotent(self) -> None:
179 backend = _minio_backend()
180 oid = _oid()
181 data = b"idempotent-data"
182 await backend.put(oid, data)
183 await backend.put(oid, data)
184 result = await backend.get(oid)
185 assert result == data
186
187 @requires_minio
188 async def test_get_returns_none_for_missing(self) -> None:
189 backend = _minio_backend()
190 oid = _oid()
191 result = await backend.get(oid)
192 assert result is None
193
194 @requires_minio
195 async def test_delete_removes_object(self) -> None:
196 backend = _minio_backend()
197 oid = _oid()
198 await backend.put(oid, b"to-be-deleted")
199 assert await backend.exists(oid) is True
200 await backend.delete(oid)
201 assert await backend.exists(oid) is False
202
203 @requires_minio
204 async def test_uri_for_uses_s3_scheme(self) -> None:
205 backend = _minio_backend()
206 oid = _oid()
207 uri = backend.uri_for(oid)
208 assert uri.startswith(f"s3://{MINIO_BUCKET}/")
209
210 @requires_minio
211 async def test_get_backend_with_minio_env_vars(self) -> None:
212 """get_backend() wired to MinIO env vars returns a working S3Backend."""
213 from musehub.storage.backends import get_backend
214 with patch("musehub.storage.backends.settings") as mock_settings:
215 mock_settings.blob_storage_bucket = MINIO_BUCKET
216 mock_settings.blob_storage_endpoint = MINIO_ENDPOINT
217 mock_settings.blob_storage_access_key_id = MINIO_ACCESS_KEY
218 mock_settings.blob_storage_secret_access_key = MINIO_SECRET_KEY
219 mock_settings.blob_storage_region = "us-east-1"
220 backend = get_backend()
221
222 oid = _oid()
223 await backend.put(oid, b"via-get-backend")
224 assert await backend.get(oid) == b"via-get-backend"
File History 2 commits
sha256:eead4146ec6c9905a097a89f8dbffa3d5c5e8ef9c1acd0e8a5b2a93b0084d273 Mpack content-addressability Human 7 days ago
sha256:ef10830ce231e0a20efcb0e2586cb879471247e916616e6fdd0d51df459e2595 fix: typing audit — 0 violations, 0 untyped defs across all… Sonnet 4.6 minor 20 days ago